From e3892ae4e25dcce34c3066b7b3c9a036250a4446 Mon Sep 17 00:00:00 2001 From: Vincent Beraudier Date: Tue, 29 Nov 2022 10:54:35 +0100 Subject: [PATCH] release 2.24.232 --- CHANGELOG.rst | 8 +- docplex/mp/cplex_engine.py | 6 +- docplex/mp/solution.py | 16 +- docplex/version.py | 4 +- docs/2.24.232/CHANGELOG.html | 898 ++ docs/2.24.232/README.md.html | 191 + .../_images/equ_mathProgr12_default.png | Bin 0 -> 9915 bytes .../_images/equ_mathProgr23_default.gif | Bin 0 -> 5041 bytes docs/2.24.232/_images/ln_docloud-engine.png | Bin 0 -> 7291 bytes docs/2.24.232/_static/ajax-loader.gif | Bin 0 -> 673 bytes docs/2.24.232/_static/background_b01.png | Bin 0 -> 78 bytes docs/2.24.232/_static/basic.css | 676 + docs/2.24.232/_static/bizstyle.css | 490 + docs/2.24.232/_static/bizstyle.js | 41 + docs/2.24.232/_static/comment-bright.png | Bin 0 -> 756 bytes docs/2.24.232/_static/comment-close.png | Bin 0 -> 829 bytes docs/2.24.232/_static/comment.png | Bin 0 -> 641 bytes docs/2.24.232/_static/css3-mediaqueries.js | 1 + .../2.24.232/_static/css3-mediaqueries_src.js | 1104 ++ docs/2.24.232/_static/doctools.js | 315 + .../2.24.232/_static/documentation_options.js | 10 + docs/2.24.232/_static/down-pressed.png | Bin 0 -> 222 bytes docs/2.24.232/_static/down.png | Bin 0 -> 202 bytes docs/2.24.232/_static/file.png | Bin 0 -> 286 bytes docs/2.24.232/_static/jquery-3.2.1.js | 10253 ++++++++++++++++ docs/2.24.232/_static/jquery.js | 4 + docs/2.24.232/_static/language_data.js | 297 + docs/2.24.232/_static/minus.png | Bin 0 -> 90 bytes docs/2.24.232/_static/plus.png | Bin 0 -> 90 bytes docs/2.24.232/_static/pygments.css | 74 + docs/2.24.232/_static/searchtools.js | 481 + docs/2.24.232/_static/underscore-1.3.1.js | 999 ++ docs/2.24.232/_static/underscore.js | 31 + docs/2.24.232/_static/up-pressed.png | Bin 0 -> 214 bytes docs/2.24.232/_static/up.png | Bin 0 -> 203 bytes docs/2.24.232/_static/websupport.js | 808 ++ docs/2.24.232/cp.html | 247 + docs/2.24.232/cp/.doctrees/README.md.doctree | Bin 0 -> 7908 bytes .../cp/.doctrees/basic.color.py.doctree | Bin 0 -> 6639 bytes .../.doctrees/basic.golomb_ruler.py.doctree | Bin 0 -> 8375 bytes .../cp/.doctrees/basic.n_queen.py.doctree | Bin 0 -> 7625 bytes .../cp/.doctrees/basic.truck_fleet.py.doctree | Bin 0 -> 19707 bytes .../cp/.doctrees/creating_model.doctree | Bin 0 -> 139394 bytes .../.doctrees/docplex.cp.blackbox.py.doctree | Bin 0 -> 120640 bytes .../cp/.doctrees/docplex.cp.config.py.doctree | Bin 0 -> 42959 bytes .../docplex.cp.expression.py.doctree | Bin 0 -> 407228 bytes .../.doctrees/docplex.cp.function.py.doctree | Bin 0 -> 61838 bytes .../docplex.cp.fzn.fzn_parser.py.doctree | Bin 0 -> 64032 bytes .../cp/.doctrees/docplex.cp.model.py.doctree | Bin 0 -> 351969 bytes .../.doctrees/docplex.cp.modeler.py.doctree | Bin 0 -> 998179 bytes .../docplex.cp.parameters.py.doctree | Bin 0 -> 337949 bytes .../.doctrees/docplex.cp.solution.py.doctree | Bin 0 -> 438275 bytes .../docplex.cp.solver.cpo_callback.py.doctree | Bin 0 -> 15852 bytes .../docplex.cp.solver.solver.py.doctree | Bin 0 -> 175897 bytes ...cplex.cp.solver.solver_listener.py.doctree | Bin 0 -> 115252 bytes .../cp/.doctrees/docplex.cp.utils.py.doctree | Bin 0 -> 353670 bytes .../docplex.cp.utils_visu.py.doctree | Bin 0 -> 84354 bytes .../docplex.util.environment.py.doctree | Bin 0 -> 238855 bytes docs/2.24.232/cp/.doctrees/environment.pickle | Bin 0 -> 1245445 bytes .../cp/.doctrees/getting_started.doctree | Bin 0 -> 20556 bytes docs/2.24.232/cp/.doctrees/index.doctree | Bin 0 -> 12483 bytes docs/2.24.232/cp/.doctrees/refman.doctree | Bin 0 -> 21606 bytes docs/2.24.232/cp/.doctrees/samples.doctree | Bin 0 -> 46468 bytes .../cp/.doctrees/visu.flow_shop.py.doctree | Bin 0 -> 9457 bytes .../visu.house_building_calendar.py.doctree | Bin 0 -> 13181 bytes .../visu.house_building_optional.py.doctree | Bin 0 -> 12539 bytes .../.doctrees/visu.job_shop_basic.py.doctree | Bin 0 -> 11359 bytes .../cp/.doctrees/visu.open_shop.py.doctree | Bin 0 -> 10299 bytes .../cp/.doctrees/visu.rcpsp.py.doctree | Bin 0 -> 11397 bytes .../cp/.doctrees/visu.setup_times.py.doctree | Bin 0 -> 14181 bytes .../.doctrees/visu.squaring_square.py.doctree | Bin 0 -> 10257 bytes docs/2.24.232/cp/README.md.html | 209 + .../cp/_modules/docplex/cp/blackbox.html | 777 ++ .../cp/_modules/docplex/cp/config.html | 856 ++ .../cp/_modules/docplex/cp/expression.html | 3129 +++++ .../cp/_modules/docplex/cp/function.html | 695 ++ .../_modules/docplex/cp/fzn/fzn_parser.html | 2210 ++++ .../cp/_modules/docplex/cp/model.html | 1923 +++ .../cp/_modules/docplex/cp/modeler.html | 3910 ++++++ .../cp/_modules/docplex/cp/parameters.html | 1359 ++ .../cp/_modules/docplex/cp/solution.html | 2871 +++++ .../docplex/cp/solver/cpo_callback.html | 159 + .../cp/_modules/docplex/cp/solver/solver.html | 1759 +++ .../docplex/cp/solver/solver_listener.html | 799 ++ .../cp/_modules/docplex/cp/utils.html | 2400 ++++ .../cp/_modules/docplex/cp/utils_visu.html | 1790 +++ .../cp/_modules/docplex/util/environment.html | 1250 ++ docs/2.24.232/cp/_modules/index.html | 99 + docs/2.24.232/cp/_static/ajax-loader.gif | Bin 0 -> 673 bytes docs/2.24.232/cp/_static/background_b01.png | Bin 0 -> 78 bytes docs/2.24.232/cp/_static/basic.css | 676 + docs/2.24.232/cp/_static/bizstyle.css | 490 + docs/2.24.232/cp/_static/bizstyle.js | 41 + docs/2.24.232/cp/_static/comment-bright.png | Bin 0 -> 756 bytes docs/2.24.232/cp/_static/comment-close.png | Bin 0 -> 829 bytes docs/2.24.232/cp/_static/comment.png | Bin 0 -> 641 bytes docs/2.24.232/cp/_static/css3-mediaqueries.js | 1 + .../cp/_static/css3-mediaqueries_src.js | 1104 ++ docs/2.24.232/cp/_static/doctools.js | 315 + .../cp/_static/documentation_options.js | 10 + docs/2.24.232/cp/_static/down-pressed.png | Bin 0 -> 222 bytes docs/2.24.232/cp/_static/down.png | Bin 0 -> 202 bytes docs/2.24.232/cp/_static/file.png | Bin 0 -> 286 bytes docs/2.24.232/cp/_static/jquery-3.2.1.js | 10253 ++++++++++++++++ docs/2.24.232/cp/_static/jquery.js | 4 + docs/2.24.232/cp/_static/language_data.js | 297 + docs/2.24.232/cp/_static/minus.png | Bin 0 -> 90 bytes docs/2.24.232/cp/_static/plus.png | Bin 0 -> 90 bytes docs/2.24.232/cp/_static/pygments.css | 74 + docs/2.24.232/cp/_static/searchtools.js | 481 + docs/2.24.232/cp/_static/underscore-1.3.1.js | 999 ++ docs/2.24.232/cp/_static/underscore.js | 31 + docs/2.24.232/cp/_static/up-pressed.png | Bin 0 -> 214 bytes docs/2.24.232/cp/_static/up.png | Bin 0 -> 203 bytes docs/2.24.232/cp/_static/websupport.js | 808 ++ docs/2.24.232/cp/basic.color.py.html | 199 + docs/2.24.232/cp/basic.golomb_ruler.py.html | 251 + docs/2.24.232/cp/basic.n_queen.py.html | 235 + docs/2.24.232/cp/basic.truck_fleet.py.html | 441 + docs/2.24.232/cp/creating_model.html | 786 ++ docs/2.24.232/cp/docplex.cp.blackbox.py.html | 620 + docs/2.24.232/cp/docplex.cp.config.py.html | 318 + .../2.24.232/cp/docplex.cp.expression.py.html | 2012 +++ docs/2.24.232/cp/docplex.cp.function.py.html | 333 + .../cp/docplex.cp.fzn.fzn_parser.py.html | 373 + docs/2.24.232/cp/docplex.cp.model.py.html | 1620 +++ docs/2.24.232/cp/docplex.cp.modeler.py.html | 4116 +++++++ .../2.24.232/cp/docplex.cp.parameters.py.html | 1360 ++ docs/2.24.232/cp/docplex.cp.solution.py.html | 2216 ++++ .../cp/docplex.cp.solver.cpo_callback.py.html | 158 + .../cp/docplex.cp.solver.solver.py.html | 859 ++ .../docplex.cp.solver.solver_listener.py.html | 565 + docs/2.24.232/cp/docplex.cp.utils.py.html | 1953 +++ .../2.24.232/cp/docplex.cp.utils_visu.py.html | 485 + .../cp/docplex.util.environment.py.html | 1203 ++ docs/2.24.232/cp/genindex.html | 2177 ++++ docs/2.24.232/cp/getting_started.html | 191 + docs/2.24.232/cp/index.html | 195 + docs/2.24.232/cp/objects.inv | Bin 0 -> 6691 bytes docs/2.24.232/cp/py-modindex.html | 179 + docs/2.24.232/cp/refman.html | 277 + docs/2.24.232/cp/samples.html | 309 + docs/2.24.232/cp/search.html | 102 + docs/2.24.232/cp/searchindex.js | 1 + docs/2.24.232/cp/visu.flow_shop.py.html | 247 + .../cp/visu.house_building_calendar.py.html | 403 + .../cp/visu.house_building_optional.py.html | 365 + docs/2.24.232/cp/visu.job_shop_basic.py.html | 289 + docs/2.24.232/cp/visu.open_shop.py.html | 259 + docs/2.24.232/cp/visu.rcpsp.py.html | 303 + docs/2.24.232/cp/visu.setup_times.py.html | 401 + docs/2.24.232/cp/visu.squaring_square.py.html | 269 + docs/2.24.232/genindex.html | 130 + docs/2.24.232/getting_started.html | 192 + docs/2.24.232/getting_started_python.html | 224 + docs/2.24.232/index.html | 226 + docs/2.24.232/mp.html | 261 + docs/2.24.232/mp/README.md.html | 210 + docs/2.24.232/mp/_images/boxes_35_0.png | Bin 0 -> 21326 bytes docs/2.24.232/mp/_images/boxes_37_0.png | Bin 0 -> 21244 bytes docs/2.24.232/mp/_images/boxes_38_0.png | Bin 0 -> 20904 bytes docs/2.24.232/mp/_images/boxes_39_1.png | Bin 0 -> 5604 bytes .../mp/_images/mining_pandas_61_0.png | Bin 0 -> 8545 bytes .../mp/_images/mining_pandas_70_0.png | Bin 0 -> 9568 bytes .../mp/_images/nurses_pandas_72_1.png | Bin 0 -> 5470 bytes .../mp/_images/nurses_pandas_74_1.png | Bin 0 -> 5650 bytes .../mp/_images/nurses_pandas_90_1.png | Bin 0 -> 4193 bytes .../mp/_images/nurses_pandas_95_1.png | Bin 0 -> 4137 bytes .../mp/_images/nurses_scheduling_47_0.png | Bin 0 -> 53028 bytes .../2.24.232/mp/_images/oil_blending_41_0.png | Bin 0 -> 13782 bytes .../2.24.232/mp/_images/oil_blending_43_0.png | Bin 0 -> 9817 bytes .../2.24.232/mp/_images/oil_blending_46_0.png | Bin 0 -> 11542 bytes docs/2.24.232/mp/_images/ucp_pandas_20_2.png | Bin 0 -> 43286 bytes docs/2.24.232/mp/_images/ucp_pandas_54_1.png | Bin 0 -> 40795 bytes docs/2.24.232/mp/_images/ucp_pandas_56_1.png | Bin 0 -> 43913 bytes docs/2.24.232/mp/_images/ucp_pandas_58_1.png | Bin 0 -> 6116 bytes docs/2.24.232/mp/_images/ucp_pandas_60_1.png | Bin 0 -> 16480 bytes docs/2.24.232/mp/_images/ucp_pandas_62_1.png | Bin 0 -> 15419 bytes docs/2.24.232/mp/_images/ucp_pandas_75_0.png | Bin 0 -> 7940 bytes docs/2.24.232/mp/_images/warehouse_38_0.png | Bin 0 -> 11350 bytes docs/2.24.232/mp/_images/warehouse_40_0.png | Bin 0 -> 12758 bytes docs/2.24.232/mp/_images/warehouse_47_0.png | Bin 0 -> 9942 bytes .../mp/_modules/docplex/mp/basic.html | 605 + .../docplex/mp/callbacks/cb_mixin.html | 345 + .../_modules/docplex/mp/conflict_refiner.html | 525 + .../mp/_modules/docplex/mp/constants.html | 745 ++ .../mp/_modules/docplex/mp/constr.html | 1873 +++ .../mp/_modules/docplex/mp/context.html | 861 ++ .../2.24.232/mp/_modules/docplex/mp/dvar.html | 762 ++ .../mp/_modules/docplex/mp/environment.html | 556 + .../mp/_modules/docplex/mp/error_handler.html | 391 + docs/2.24.232/mp/_modules/docplex/mp/kpi.html | 302 + .../mp/_modules/docplex/mp/linear.html | 1585 +++ .../mp/_modules/docplex/mp/model.html | 7275 +++++++++++ .../mp/_modules/docplex/mp/model_reader.html | 864 ++ .../mp/_modules/docplex/mp/model_stats.html | 334 + .../docplex/mp/params/parameters.html | 1034 ++ .../mp/_modules/docplex/mp/priority.html | 222 + .../mp/_modules/docplex/mp/progress.html | 755 ++ .../mp/_modules/docplex/mp/publish.html | 368 + docs/2.24.232/mp/_modules/docplex/mp/pwl.html | 965 ++ .../2.24.232/mp/_modules/docplex/mp/quad.html | 908 ++ .../mp/_modules/docplex/mp/relax_linear.html | 280 + .../mp/_modules/docplex/mp/relaxer.html | 758 ++ .../mp/_modules/docplex/mp/sdetails.html | 501 + .../mp/_modules/docplex/mp/solution.html | 1559 +++ .../mp/_modules/docplex/mp/sosvarset.html | 230 + .../mp/_modules/docplex/mp/vartype.html | 421 + .../mp/_modules/docplex/mp/with_funcs.html | 219 + .../mp/_modules/docplex/util/csv_utils.html | 146 + .../mp/_modules/docplex/util/environment.html | 1251 ++ .../_modules/docplex/util/logging_utils.html | 165 + docs/2.24.232/mp/_modules/index.html | 115 + docs/2.24.232/mp/_static/ajax-loader.gif | Bin 0 -> 673 bytes docs/2.24.232/mp/_static/background_b01.png | Bin 0 -> 78 bytes docs/2.24.232/mp/_static/basic.css | 676 + docs/2.24.232/mp/_static/bizstyle.css | 490 + docs/2.24.232/mp/_static/bizstyle.js | 41 + docs/2.24.232/mp/_static/comment-bright.png | Bin 0 -> 756 bytes docs/2.24.232/mp/_static/comment-close.png | Bin 0 -> 829 bytes docs/2.24.232/mp/_static/comment.png | Bin 0 -> 641 bytes docs/2.24.232/mp/_static/css3-mediaqueries.js | 1 + .../mp/_static/css3-mediaqueries_src.js | 1104 ++ docs/2.24.232/mp/_static/doctools.js | 315 + .../mp/_static/documentation_options.js | 10 + docs/2.24.232/mp/_static/down-pressed.png | Bin 0 -> 222 bytes docs/2.24.232/mp/_static/down.png | Bin 0 -> 202 bytes docs/2.24.232/mp/_static/file.png | Bin 0 -> 286 bytes docs/2.24.232/mp/_static/jquery-3.2.1.js | 10253 ++++++++++++++++ docs/2.24.232/mp/_static/jquery.js | 4 + docs/2.24.232/mp/_static/language_data.js | 297 + docs/2.24.232/mp/_static/minus.png | Bin 0 -> 90 bytes docs/2.24.232/mp/_static/plus.png | Bin 0 -> 90 bytes docs/2.24.232/mp/_static/pygments.css | 74 + docs/2.24.232/mp/_static/searchtools.js | 481 + docs/2.24.232/mp/_static/underscore-1.3.1.js | 999 ++ docs/2.24.232/mp/_static/underscore.js | 31 + docs/2.24.232/mp/_static/up-pressed.png | Bin 0 -> 214 bytes docs/2.24.232/mp/_static/up.png | Bin 0 -> 203 bytes docs/2.24.232/mp/_static/websupport.js | 808 ++ docs/2.24.232/mp/boxes.html | 397 + docs/2.24.232/mp/chicago_coffee_shops.html | 411 + docs/2.24.232/mp/creating_model.html | 323 + docs/2.24.232/mp/cutstock.html | 666 + docs/2.24.232/mp/diet.html | 358 + docs/2.24.232/mp/docplex.mp.basic.html | 238 + .../mp/docplex.mp.callbacks.cb_mixin.html | 360 + .../mp/docplex.mp.conflict_refiner.html | 361 + docs/2.24.232/mp/docplex.mp.constants.html | 334 + docs/2.24.232/mp/docplex.mp.constr.html | 670 + docs/2.24.232/mp/docplex.mp.context.html | 358 + docs/2.24.232/mp/docplex.mp.dvar.html | 369 + docs/2.24.232/mp/docplex.mp.environment.html | 214 + .../2.24.232/mp/docplex.mp.error_handler.html | 131 + docs/2.24.232/mp/docplex.mp.kpi.html | 241 + docs/2.24.232/mp/docplex.mp.linear.html | 625 + docs/2.24.232/mp/docplex.mp.model.html | 5151 ++++++++ docs/2.24.232/mp/docplex.mp.model_reader.html | 242 + docs/2.24.232/mp/docplex.mp.model_stats.html | 230 + .../mp/docplex.mp.params.parameters.html | 760 ++ docs/2.24.232/mp/docplex.mp.priority.html | 118 + docs/2.24.232/mp/docplex.mp.progress.html | 604 + docs/2.24.232/mp/docplex.mp.publish.html | 163 + docs/2.24.232/mp/docplex.mp.pwl.html | 279 + docs/2.24.232/mp/docplex.mp.quad.html | 286 + docs/2.24.232/mp/docplex.mp.relax_linear.html | 147 + docs/2.24.232/mp/docplex.mp.relaxer.html | 313 + docs/2.24.232/mp/docplex.mp.sdetails.html | 287 + docs/2.24.232/mp/docplex.mp.solution.html | 988 ++ docs/2.24.232/mp/docplex.mp.sosvarset.html | 173 + docs/2.24.232/mp/docplex.mp.vartype.html | 327 + docs/2.24.232/mp/docplex.mp.with_funcs.html | 214 + docs/2.24.232/mp/docplex.mp.worker_utils.html | 111 + docs/2.24.232/mp/docplex.util.csv_utils.html | 136 + .../2.24.232/mp/docplex.util.environment.html | 1214 ++ .../mp/docplex.util.logging_utils.html | 173 + docs/2.24.232/mp/genindex.html | 2049 +++ docs/2.24.232/mp/getting_started.html | 231 + docs/2.24.232/mp/getting_started_python.html | 264 + docs/2.24.232/mp/index.html | 187 + docs/2.24.232/mp/lagrangian_relaxation.html | 379 + docs/2.24.232/mp/load_balancing.html | 609 + docs/2.24.232/mp/marketing_campaign.html | 790 ++ docs/2.24.232/mp/mining_pandas.html | 1034 ++ docs/2.24.232/mp/nurses.html | 1173 ++ docs/2.24.232/mp/nurses_multiobj.html | 1211 ++ docs/2.24.232/mp/nurses_pandas.html | 3418 ++++++ docs/2.24.232/mp/nurses_scheduling.html | 729 ++ docs/2.24.232/mp/objects.inv | Bin 0 -> 6925 bytes docs/2.24.232/mp/oil_blending.html | 621 + docs/2.24.232/mp/production.html | 334 + docs/2.24.232/mp/py-modindex.html | 260 + docs/2.24.232/mp/refman.html | 220 + docs/2.24.232/mp/samples.html | 231 + docs/2.24.232/mp/search.html | 103 + docs/2.24.232/mp/searchindex.js | 1 + docs/2.24.232/mp/sport_scheduling.html | 399 + docs/2.24.232/mp/sports_scheduling.html | 676 + docs/2.24.232/mp/support.html | 124 + docs/2.24.232/mp/troubleshooting.html | 132 + docs/2.24.232/mp/ucp_pandas.html | 1418 +++ docs/2.24.232/mp/warehouse.html | 522 + docs/2.24.232/mp_vs_cp.html | 170 + docs/2.24.232/objects.inv | Bin 0 -> 502 bytes docs/2.24.232/search.html | 96 + docs/2.24.232/searchindex.js | 1 + docs/2.24.232/support.html | 105 + docs/CHANGELOG.html | 14 +- docs/cp/.doctrees/environment.pickle | Bin 1245445 -> 1245445 bytes docs/mp/_modules/docplex/mp/solution.html | 16 +- docs/mp/docplex.mp.solution.html | 4 +- docs/searchindex.js | 2 +- setup.py | 2 +- 313 files changed, 164026 insertions(+), 21 deletions(-) create mode 100644 docs/2.24.232/CHANGELOG.html create mode 100644 docs/2.24.232/README.md.html create mode 100644 docs/2.24.232/_images/equ_mathProgr12_default.png create mode 100644 docs/2.24.232/_images/equ_mathProgr23_default.gif create mode 100644 docs/2.24.232/_images/ln_docloud-engine.png create mode 100644 docs/2.24.232/_static/ajax-loader.gif create mode 100644 docs/2.24.232/_static/background_b01.png create mode 100644 docs/2.24.232/_static/basic.css create mode 100644 docs/2.24.232/_static/bizstyle.css create mode 100644 docs/2.24.232/_static/bizstyle.js create mode 100644 docs/2.24.232/_static/comment-bright.png create mode 100644 docs/2.24.232/_static/comment-close.png create mode 100644 docs/2.24.232/_static/comment.png create mode 100644 docs/2.24.232/_static/css3-mediaqueries.js create mode 100644 docs/2.24.232/_static/css3-mediaqueries_src.js create mode 100644 docs/2.24.232/_static/doctools.js create mode 100644 docs/2.24.232/_static/documentation_options.js create mode 100644 docs/2.24.232/_static/down-pressed.png create mode 100644 docs/2.24.232/_static/down.png create mode 100644 docs/2.24.232/_static/file.png create mode 100644 docs/2.24.232/_static/jquery-3.2.1.js create mode 100644 docs/2.24.232/_static/jquery.js create mode 100644 docs/2.24.232/_static/language_data.js create mode 100644 docs/2.24.232/_static/minus.png create mode 100644 docs/2.24.232/_static/plus.png create mode 100644 docs/2.24.232/_static/pygments.css create mode 100644 docs/2.24.232/_static/searchtools.js create mode 100644 docs/2.24.232/_static/underscore-1.3.1.js create mode 100644 docs/2.24.232/_static/underscore.js create mode 100644 docs/2.24.232/_static/up-pressed.png create mode 100644 docs/2.24.232/_static/up.png create mode 100644 docs/2.24.232/_static/websupport.js create mode 100644 docs/2.24.232/cp.html create mode 100644 docs/2.24.232/cp/.doctrees/README.md.doctree create mode 100644 docs/2.24.232/cp/.doctrees/basic.color.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/basic.golomb_ruler.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/basic.n_queen.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/basic.truck_fleet.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/creating_model.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.blackbox.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.config.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.expression.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.function.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.fzn.fzn_parser.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.model.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.modeler.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.parameters.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.solution.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.solver.cpo_callback.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.solver.solver.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.solver.solver_listener.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.utils.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.cp.utils_visu.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/docplex.util.environment.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/environment.pickle create mode 100644 docs/2.24.232/cp/.doctrees/getting_started.doctree create mode 100644 docs/2.24.232/cp/.doctrees/index.doctree create mode 100644 docs/2.24.232/cp/.doctrees/refman.doctree create mode 100644 docs/2.24.232/cp/.doctrees/samples.doctree create mode 100644 docs/2.24.232/cp/.doctrees/visu.flow_shop.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/visu.house_building_calendar.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/visu.house_building_optional.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/visu.job_shop_basic.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/visu.open_shop.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/visu.rcpsp.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/visu.setup_times.py.doctree create mode 100644 docs/2.24.232/cp/.doctrees/visu.squaring_square.py.doctree create mode 100644 docs/2.24.232/cp/README.md.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/blackbox.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/config.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/expression.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/function.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/fzn/fzn_parser.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/model.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/modeler.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/parameters.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/solution.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/solver/cpo_callback.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/solver/solver.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/solver/solver_listener.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/utils.html create mode 100644 docs/2.24.232/cp/_modules/docplex/cp/utils_visu.html create mode 100644 docs/2.24.232/cp/_modules/docplex/util/environment.html create mode 100644 docs/2.24.232/cp/_modules/index.html create mode 100644 docs/2.24.232/cp/_static/ajax-loader.gif create mode 100644 docs/2.24.232/cp/_static/background_b01.png create mode 100644 docs/2.24.232/cp/_static/basic.css create mode 100644 docs/2.24.232/cp/_static/bizstyle.css create mode 100644 docs/2.24.232/cp/_static/bizstyle.js create mode 100644 docs/2.24.232/cp/_static/comment-bright.png create mode 100644 docs/2.24.232/cp/_static/comment-close.png create mode 100644 docs/2.24.232/cp/_static/comment.png create mode 100644 docs/2.24.232/cp/_static/css3-mediaqueries.js create mode 100644 docs/2.24.232/cp/_static/css3-mediaqueries_src.js create mode 100644 docs/2.24.232/cp/_static/doctools.js create mode 100644 docs/2.24.232/cp/_static/documentation_options.js create mode 100644 docs/2.24.232/cp/_static/down-pressed.png create mode 100644 docs/2.24.232/cp/_static/down.png create mode 100644 docs/2.24.232/cp/_static/file.png create mode 100644 docs/2.24.232/cp/_static/jquery-3.2.1.js create mode 100644 docs/2.24.232/cp/_static/jquery.js create mode 100644 docs/2.24.232/cp/_static/language_data.js create mode 100644 docs/2.24.232/cp/_static/minus.png create mode 100644 docs/2.24.232/cp/_static/plus.png create mode 100644 docs/2.24.232/cp/_static/pygments.css create mode 100644 docs/2.24.232/cp/_static/searchtools.js create mode 100644 docs/2.24.232/cp/_static/underscore-1.3.1.js create mode 100644 docs/2.24.232/cp/_static/underscore.js create mode 100644 docs/2.24.232/cp/_static/up-pressed.png create mode 100644 docs/2.24.232/cp/_static/up.png create mode 100644 docs/2.24.232/cp/_static/websupport.js create mode 100644 docs/2.24.232/cp/basic.color.py.html create mode 100644 docs/2.24.232/cp/basic.golomb_ruler.py.html create mode 100644 docs/2.24.232/cp/basic.n_queen.py.html create mode 100644 docs/2.24.232/cp/basic.truck_fleet.py.html create mode 100644 docs/2.24.232/cp/creating_model.html create mode 100644 docs/2.24.232/cp/docplex.cp.blackbox.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.config.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.expression.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.function.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.fzn.fzn_parser.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.model.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.modeler.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.parameters.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.solution.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.solver.cpo_callback.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.solver.solver.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.solver.solver_listener.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.utils.py.html create mode 100644 docs/2.24.232/cp/docplex.cp.utils_visu.py.html create mode 100644 docs/2.24.232/cp/docplex.util.environment.py.html create mode 100644 docs/2.24.232/cp/genindex.html create mode 100644 docs/2.24.232/cp/getting_started.html create mode 100644 docs/2.24.232/cp/index.html create mode 100644 docs/2.24.232/cp/objects.inv create mode 100644 docs/2.24.232/cp/py-modindex.html create mode 100644 docs/2.24.232/cp/refman.html create mode 100644 docs/2.24.232/cp/samples.html create mode 100644 docs/2.24.232/cp/search.html create mode 100644 docs/2.24.232/cp/searchindex.js create mode 100644 docs/2.24.232/cp/visu.flow_shop.py.html create mode 100644 docs/2.24.232/cp/visu.house_building_calendar.py.html create mode 100644 docs/2.24.232/cp/visu.house_building_optional.py.html create mode 100644 docs/2.24.232/cp/visu.job_shop_basic.py.html create mode 100644 docs/2.24.232/cp/visu.open_shop.py.html create mode 100644 docs/2.24.232/cp/visu.rcpsp.py.html create mode 100644 docs/2.24.232/cp/visu.setup_times.py.html create mode 100644 docs/2.24.232/cp/visu.squaring_square.py.html create mode 100644 docs/2.24.232/genindex.html create mode 100644 docs/2.24.232/getting_started.html create mode 100644 docs/2.24.232/getting_started_python.html create mode 100644 docs/2.24.232/index.html create mode 100644 docs/2.24.232/mp.html create mode 100644 docs/2.24.232/mp/README.md.html create mode 100644 docs/2.24.232/mp/_images/boxes_35_0.png create mode 100644 docs/2.24.232/mp/_images/boxes_37_0.png create mode 100644 docs/2.24.232/mp/_images/boxes_38_0.png create mode 100644 docs/2.24.232/mp/_images/boxes_39_1.png create mode 100644 docs/2.24.232/mp/_images/mining_pandas_61_0.png create mode 100644 docs/2.24.232/mp/_images/mining_pandas_70_0.png create mode 100644 docs/2.24.232/mp/_images/nurses_pandas_72_1.png create mode 100644 docs/2.24.232/mp/_images/nurses_pandas_74_1.png create mode 100644 docs/2.24.232/mp/_images/nurses_pandas_90_1.png create mode 100644 docs/2.24.232/mp/_images/nurses_pandas_95_1.png create mode 100644 docs/2.24.232/mp/_images/nurses_scheduling_47_0.png create mode 100644 docs/2.24.232/mp/_images/oil_blending_41_0.png create mode 100644 docs/2.24.232/mp/_images/oil_blending_43_0.png create mode 100644 docs/2.24.232/mp/_images/oil_blending_46_0.png create mode 100644 docs/2.24.232/mp/_images/ucp_pandas_20_2.png create mode 100644 docs/2.24.232/mp/_images/ucp_pandas_54_1.png create mode 100644 docs/2.24.232/mp/_images/ucp_pandas_56_1.png create mode 100644 docs/2.24.232/mp/_images/ucp_pandas_58_1.png create mode 100644 docs/2.24.232/mp/_images/ucp_pandas_60_1.png create mode 100644 docs/2.24.232/mp/_images/ucp_pandas_62_1.png create mode 100644 docs/2.24.232/mp/_images/ucp_pandas_75_0.png create mode 100644 docs/2.24.232/mp/_images/warehouse_38_0.png create mode 100644 docs/2.24.232/mp/_images/warehouse_40_0.png create mode 100644 docs/2.24.232/mp/_images/warehouse_47_0.png create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/basic.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/callbacks/cb_mixin.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/conflict_refiner.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/constants.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/constr.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/context.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/dvar.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/environment.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/error_handler.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/kpi.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/linear.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/model.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/model_reader.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/model_stats.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/params/parameters.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/priority.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/progress.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/publish.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/pwl.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/quad.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/relax_linear.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/relaxer.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/sdetails.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/solution.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/sosvarset.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/vartype.html create mode 100644 docs/2.24.232/mp/_modules/docplex/mp/with_funcs.html create mode 100644 docs/2.24.232/mp/_modules/docplex/util/csv_utils.html create mode 100644 docs/2.24.232/mp/_modules/docplex/util/environment.html create mode 100644 docs/2.24.232/mp/_modules/docplex/util/logging_utils.html create mode 100644 docs/2.24.232/mp/_modules/index.html create mode 100644 docs/2.24.232/mp/_static/ajax-loader.gif create mode 100644 docs/2.24.232/mp/_static/background_b01.png create mode 100644 docs/2.24.232/mp/_static/basic.css create mode 100644 docs/2.24.232/mp/_static/bizstyle.css create mode 100644 docs/2.24.232/mp/_static/bizstyle.js create mode 100644 docs/2.24.232/mp/_static/comment-bright.png create mode 100644 docs/2.24.232/mp/_static/comment-close.png create mode 100644 docs/2.24.232/mp/_static/comment.png create mode 100644 docs/2.24.232/mp/_static/css3-mediaqueries.js create mode 100644 docs/2.24.232/mp/_static/css3-mediaqueries_src.js create mode 100644 docs/2.24.232/mp/_static/doctools.js create mode 100644 docs/2.24.232/mp/_static/documentation_options.js create mode 100644 docs/2.24.232/mp/_static/down-pressed.png create mode 100644 docs/2.24.232/mp/_static/down.png create mode 100644 docs/2.24.232/mp/_static/file.png create mode 100644 docs/2.24.232/mp/_static/jquery-3.2.1.js create mode 100644 docs/2.24.232/mp/_static/jquery.js create mode 100644 docs/2.24.232/mp/_static/language_data.js create mode 100644 docs/2.24.232/mp/_static/minus.png create mode 100644 docs/2.24.232/mp/_static/plus.png create mode 100644 docs/2.24.232/mp/_static/pygments.css create mode 100644 docs/2.24.232/mp/_static/searchtools.js create mode 100644 docs/2.24.232/mp/_static/underscore-1.3.1.js create mode 100644 docs/2.24.232/mp/_static/underscore.js create mode 100644 docs/2.24.232/mp/_static/up-pressed.png create mode 100644 docs/2.24.232/mp/_static/up.png create mode 100644 docs/2.24.232/mp/_static/websupport.js create mode 100644 docs/2.24.232/mp/boxes.html create mode 100644 docs/2.24.232/mp/chicago_coffee_shops.html create mode 100644 docs/2.24.232/mp/creating_model.html create mode 100644 docs/2.24.232/mp/cutstock.html create mode 100644 docs/2.24.232/mp/diet.html create mode 100644 docs/2.24.232/mp/docplex.mp.basic.html create mode 100644 docs/2.24.232/mp/docplex.mp.callbacks.cb_mixin.html create mode 100644 docs/2.24.232/mp/docplex.mp.conflict_refiner.html create mode 100644 docs/2.24.232/mp/docplex.mp.constants.html create mode 100644 docs/2.24.232/mp/docplex.mp.constr.html create mode 100644 docs/2.24.232/mp/docplex.mp.context.html create mode 100644 docs/2.24.232/mp/docplex.mp.dvar.html create mode 100644 docs/2.24.232/mp/docplex.mp.environment.html create mode 100644 docs/2.24.232/mp/docplex.mp.error_handler.html create mode 100644 docs/2.24.232/mp/docplex.mp.kpi.html create mode 100644 docs/2.24.232/mp/docplex.mp.linear.html create mode 100644 docs/2.24.232/mp/docplex.mp.model.html create mode 100644 docs/2.24.232/mp/docplex.mp.model_reader.html create mode 100644 docs/2.24.232/mp/docplex.mp.model_stats.html create mode 100644 docs/2.24.232/mp/docplex.mp.params.parameters.html create mode 100644 docs/2.24.232/mp/docplex.mp.priority.html create mode 100644 docs/2.24.232/mp/docplex.mp.progress.html create mode 100644 docs/2.24.232/mp/docplex.mp.publish.html create mode 100644 docs/2.24.232/mp/docplex.mp.pwl.html create mode 100644 docs/2.24.232/mp/docplex.mp.quad.html create mode 100644 docs/2.24.232/mp/docplex.mp.relax_linear.html create mode 100644 docs/2.24.232/mp/docplex.mp.relaxer.html create mode 100644 docs/2.24.232/mp/docplex.mp.sdetails.html create mode 100644 docs/2.24.232/mp/docplex.mp.solution.html create mode 100644 docs/2.24.232/mp/docplex.mp.sosvarset.html create mode 100644 docs/2.24.232/mp/docplex.mp.vartype.html create mode 100644 docs/2.24.232/mp/docplex.mp.with_funcs.html create mode 100644 docs/2.24.232/mp/docplex.mp.worker_utils.html create mode 100644 docs/2.24.232/mp/docplex.util.csv_utils.html create mode 100644 docs/2.24.232/mp/docplex.util.environment.html create mode 100644 docs/2.24.232/mp/docplex.util.logging_utils.html create mode 100644 docs/2.24.232/mp/genindex.html create mode 100644 docs/2.24.232/mp/getting_started.html create mode 100644 docs/2.24.232/mp/getting_started_python.html create mode 100644 docs/2.24.232/mp/index.html create mode 100644 docs/2.24.232/mp/lagrangian_relaxation.html create mode 100644 docs/2.24.232/mp/load_balancing.html create mode 100644 docs/2.24.232/mp/marketing_campaign.html create mode 100644 docs/2.24.232/mp/mining_pandas.html create mode 100644 docs/2.24.232/mp/nurses.html create mode 100644 docs/2.24.232/mp/nurses_multiobj.html create mode 100644 docs/2.24.232/mp/nurses_pandas.html create mode 100644 docs/2.24.232/mp/nurses_scheduling.html create mode 100644 docs/2.24.232/mp/objects.inv create mode 100644 docs/2.24.232/mp/oil_blending.html create mode 100644 docs/2.24.232/mp/production.html create mode 100644 docs/2.24.232/mp/py-modindex.html create mode 100644 docs/2.24.232/mp/refman.html create mode 100644 docs/2.24.232/mp/samples.html create mode 100644 docs/2.24.232/mp/search.html create mode 100644 docs/2.24.232/mp/searchindex.js create mode 100644 docs/2.24.232/mp/sport_scheduling.html create mode 100644 docs/2.24.232/mp/sports_scheduling.html create mode 100644 docs/2.24.232/mp/support.html create mode 100644 docs/2.24.232/mp/troubleshooting.html create mode 100644 docs/2.24.232/mp/ucp_pandas.html create mode 100644 docs/2.24.232/mp/warehouse.html create mode 100644 docs/2.24.232/mp_vs_cp.html create mode 100644 docs/2.24.232/objects.inv create mode 100644 docs/2.24.232/search.html create mode 100644 docs/2.24.232/searchindex.js create mode 100644 docs/2.24.232/support.html diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b2d3df9..5ceb1b5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,14 @@ Changelog --------- -Changed in 2.24.231: +Changed in 2.24.232: ```````````````````` +* Optional CPLEX import for qiskit +* Fix in cut getter + +Changed in 2.24.231 (2022.11): +`````````````````````````````` + * Support for CPLEX Optimization Studio 22.1.1 runtimes. * Cleanup of the code related to Watson Machine Learning. * In ``docplex.mp``: diff --git a/docplex/mp/cplex_engine.py b/docplex/mp/cplex_engine.py index 809f5d4..c576ab2 100644 --- a/docplex/mp/cplex_engine.py +++ b/docplex/mp/cplex_engine.py @@ -11,7 +11,11 @@ import numbers import sys -from cplex._internal._subinterfaces import CutType +try: + from cplex._internal._subinterfaces import CutType +except: + CutType = list + from docplex.mp.engine import IEngine from docplex.mp.utils import DOcplexException, is_string from docplex.mp.constants import ConflictStatus diff --git a/docplex/mp/solution.py b/docplex/mp/solution.py index bdbe4c6..3d51146 100644 --- a/docplex/mp/solution.py +++ b/docplex/mp/solution.py @@ -7,7 +7,10 @@ import sys import copy -from cplex._internal._subinterfaces import CutType +try: + from cplex._internal._subinterfaces import CutType +except: + CutType = list try: # pragma: no cover from itertools import zip_longest as izip_longest @@ -849,18 +852,21 @@ def get_num_cuts(self, cut_type): """ Returns the number of cuts for a specific type. :param cut_type: a cut type. - :return: the number of cuts associated to this type of cut. + :return: the number of cuts associated to this type of cut. 0 if CPLEX is not present """ cut_type_instance = CutType() if cut_type in cut_type_instance: - return self.get_cuts()[cut_type] - handle_error(logger=self.model, error="raise", msg="Cut type does not exist") + cuts = self.get_cuts() + name = cut_type_instance[cut_type] + return cuts[name] + return 0 + def get_cuts(self): """ Returns the number of cuts under the form of a dict(type -> number). - :return: the number of cuts under the form of a dict(type -> number). + :return: the number of cuts under the form of a dict(type -> number). Empty dict if CPLEX is not present. """ m = self.model self.ensure_cuts(m, m.get_engine()) diff --git a/docplex/version.py b/docplex/version.py index 2704133..b4f33e8 100644 --- a/docplex/version.py +++ b/docplex/version.py @@ -9,8 +9,8 @@ # See script tools/gen_version.py docplex_version_major = 2 docplex_version_minor = 24 -docplex_version_micro = 231 -docplex_version_string = '2.24.231' +docplex_version_micro = 232 +docplex_version_string = '2.24.232' latest_cplex_major = 22 latest_cplex_minor = 1 diff --git a/docs/2.24.232/CHANGELOG.html b/docs/2.24.232/CHANGELOG.html new file mode 100644 index 0000000..a9122a3 --- /dev/null +++ b/docs/2.24.232/CHANGELOG.html @@ -0,0 +1,898 @@ + + + + + + + + + Changelog — IBM® Decision Optimization CPLEX® Modeling for Python (DOcplex) V2.24 documentation + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

Changelog

+
+

Changed in 2.24.232:

+
    +
  • Optional CPLEX import for qiskit
  • +
  • Fix in cut getter
  • +
+
+
+

Changed in 2.24.231 (2022.11):

+
    +
  • Support for CPLEX Optimization Studio 22.1.1 runtimes.
  • +
  • Cleanup of the code related to Watson Machine Learning.
  • +
  • +
    In docplex.mp:
    +
      +
    • Support for sensitivity analysis in Solution.
    • +
    • Fixes in Solution handling.
    • +
    +
    +
    +
  • +
  • In docplex.cp: +* Support for ‘inferred’ statement. +* Support for ‘sub_circuit’ constraint. +* Fixed conditional module importation in sched_jobshop_blackbox.py
  • +
+
+
+

Changed in 2.23.222 (2022.03):

+
    +
  • Support for CPLEX Optimization Studio 22.1 runtimes.
  • +
  • +
    In docplex.mp:
    +
      +
    • Support of parameter sets for multi objective optimization.
    • +
    • Bug fix for multi objective optimization.
    • +
    • Correct documentation urls to current CPLEX offering.
    • +
    • Removal of the deprecated docloud_context.
    • +
    • Performance improvements for model building with 22.1
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • Added a new module blackbox to allow the new blackbox function feature
    • +
    • Wrote full documentation on blackbox functions
    • +
    • Added a new SearchType ‘Neighborhood’
    • +
    • Fixed a bug in modeler.same_common_subsequence
    • +
    • Fixed problem of order of computation of actual solving parameters.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.22.213 (2021.09):

+
    +
  • Removed zeppelin examples
  • +
  • +
    In docplex.mp:
    +
      +
    • Added a variant of Model.sum() with variable number of arguments: Model.sums()
    • +
    • Removed all Python2 compatibility code
    • +
    • Added deterministic time in solve details.
    • +
    • Linear relaxer now relaxes SOS variable sets (linear sum relaxation)
    • +
    • Fixed a bug on Model.solve_with_goals() with quadratic sub-objectives
    • +
    • Fixed a bug in SolveSolution.kpi_value_by_name
    • +
    • Fixed a bug in SolveSolution.get_value_dict() about precision filtering
    • +
    • Fixed a bug when updating a constraint rhs with a NaN value now raises exception.
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • +
      Add new methods get_objective_value(), get_objective_bound() and get_objective_gap() on CpoModelSolution
      +
      and CpoSolveResult to access directly the first objective, bound or gap.
      +
      +
    • +
    • Support real call to abort_search() instead of killing the solver.
    • +
    • Add a method get_parameters() to CpoSolver to retrieve actual solving parameters.
    • +
    • Fix problem of order of computation of actual solving parameters.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.21.207 (2021.06):

+
    +
  • +
    In docplex.mp:
    +
      +
    • +
      Add support for importing solution from a different model, provided variables
      +
      can be matched between both models (Model.import_solution)
      +
      +
    • +
    • +
      Added contextual function to temporarily modify aspects of the model:
      +
      model_parameters to change parameters in a block, model_objective to set a temporary objective in a block.
      +
      +
    • +
    • Naming expressions is now deprecated, use a temporary variable if needed.
    • +
    • +
      Display of expressions in constraints is customizable: use a space separator (or not),
      +
      set a maximum length for very long expression.
      +
      +
    • +
    • Fixed documentation references to class Var, moved from linear to dvar
    • +
    • Fixed a bug in LP export for multi-objective models with no priorities passed
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • Replace context attribute solve_with_start_next by solve_with_search_next, keeping ascending compatibility.
    • +
    • Support solver parameter ModelAnonymizer to generate random names for all model elements (except KPIs) in the CPO file format.
    • +
    • Optimize generation of CPO expressions from Python expressions
    • +
    • Interval variable solutions tuples are now named tuples.
    • +
    • Method CpoModel.add() now accepts multiple expressions (or lists of expressions) to add to the model.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.20.204 (2021.02):

+
    +
  • +
    In docplex.mp:
    +
      +
    • Add support for exporting solutions and solution pools to SOL format.
    • +
    • +
      Add fast methods for changing batches of variable bounds:
      +
      Model.change_var_lower_bounds, Model.change_var_upper_bounds
      +
      +
    • +
    • Reset random seed value for cplex 12.10 , was different from COS release value.
    • +
    • Improved performance of variable creation
    • +
    • Removed a warning about accessing a deprecated “solve_status” in solve.
    • +
    • +
      docplex.mp.AdvModel now has checker enabled by default to avoid Python errors.
      +
      Is is up to the user to disable type-checking to get maximum performance.
      +
      +
    • +
    • Fixed a bug about not printing updated variable bounds in MPS and SAV
    • +
    • Changed the default rounding behavior: solution values are not rounded by default.
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • Fix problems on step function operations.
    • +
    • Add status in conflict refiner solution object.
    • +
    • Fix a bug to not send and receive conflict in CPO format if no conflict.
    • +
    • Enhance printing of model solution.
    • +
    • The method search_next(), returns only new solutions of the model. +In particular, no new solution is returned if solve status just change from Feasible to Optimal.
    • +
    • Rework completely model statistics (CpoModelStatistics)
    • +
    • Add a parameter add_cpo to the write method of CpoRefineConflictResult
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.19.202 (2020.12):

+
    +
  • +
    In docplex.mp:
    +
      +
    • Exporting models to SAV.gz format is now supported.
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • Add a new method remove_expressions() to CpoModel that removes a list of expressions in one shot.
    • +
    • Update documentation to describe how to set private solver parameters.
    • +
    • Add functions to export and import context as flat file.
    • +
    • Fix a bug on the string representation of calls to constant() modeling function.
    • +
    • Add JSON filter in case of double identical value for objective.
    • +
    • Add functions to export and import context as flat file.
    • +
    • Fix a bug on the string representation of calls to constant() modeling function.
    • +
    • Add JSON filter in case of double value for objective.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.18.200 (2020.11.#3):

+
    +
  • +
    In docplex.mp:
    +
      +
    • Latest supported CPLEX Optimization Studio is now 20.1
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.17.196 (2020.11.#2):

+
    +
  • +
    In docplex.mp:
    +
      +
    • Fixed a bug with pickling: edition of a constraint in a pickled model raised an error
    • +
    • Fixed a bug with pickling: models with piecewise-linear constraints could not be pickled
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • Add environment variable DOCPLEX_CP_CONTEXT to modify configuration
    • +
    • Add new module check_list that print a report on execution environment
    • +
    • Remove DOcloud from documentation (including code)
    • +
    • Rework customization of configuration and better support of default directory
    • +
    • Add new configuration parameter model.sort_names to drive sort of variable declarations in CPO file format.
    • +
    • Fix a problem that may crash Python in case of abort_search with local solve.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.16.196 (2020.11):

+
    +
  • +
    In docplex.mp:
    +
      +
    • add Model.add_quadratic_constraints() to add a batch of quadratic constraints
    • +
    • add Model.populate_solution_pool() for a native support of solurtion pools
    • +
    • support of CPLEX 20.1
    • +
    • compatible with Python 3.8 (only with CPLEX 20.1)
    • +
    • enable changing absolute and relative tolerances for multi-objectives
    • +
    • Optimization of Model.if_then: when condition is of the form b==1 (or 0), no additional boolean +variable is generated
    • +
    • For solving, docplex.mp now uses the cplex module if it has been installed. If not, docplex.mp +checks for the latest installed version of CPLEX Optimization Studio (COS) (using the CPLEX_STUDIO_DIRXXX +environment variables) and use the cplex if a COS is found, unless the configuration of the engine +states otherwise.
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • Add pngfile= parameter to visu.show() method to store in a PNG file instead of display on screen.
    • +
    • Make parameters and solver infos also available in refine_conflict solution.
    • +
    • Add a IntegerDomain class used to represent domain of integer variables, with a specific __str__ method
    • +
    • Add new functions ceil(), floor(), trunc(), round() and sgn()
    • +
    • Remove all warnings generated by Python 38
    • +
    • Optimize creation of add expressions with CumulExpr and zero
    • +
    • Implement configurable factorization of common model expressions when generating CPO format
    • +
    • Add checking of scal_prod() array sizes at modeling time
    • +
    • Add strict_lexicographic() and checking of strict_lexicographic() and lexicographic() array sizes at modeling time.
    • +
    • Add failure explanation as new method explain_failure() allowing to log failure tags or get details on one or several failures.
    • +
    • Enhance management of local solve sub-process timeout with detailed error and configurable timeout delay
    • +
    • For solving, docplex.cp now uses the cpoptimizer executable if it has been installed. If not, docplex.cp +checks for the latest installed version of CPLEX Optimization Studio (COS) (using the CPLEX_STUDIO_DIRXXX +environment variables) and use the cpoptimizer if a COS is found, unless the configuration of the engine +states otherwise.
    • +
    • Support last optimal solution in search/next sequence
    • +
    • Support of solver parameters in all next() solutions
    • +
    • Add solver version in process info attached to a run result
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.15.194 (2020.07):

+
    +
  • +
    In docplex.mp:
    +
      +
    • add Model.quadratic_dual_slacks()
    • +
    • Fixed a bug in multi-objectives: objectives were incorrectly rounded
    • +
    • Fixed a bug in Model.report(): multiple objective values were not displayed()
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • Add conflict in CPO format in refine conflict result
    • +
    • Fix problem when parsing KPIs section of a CPO model
    • +
    • Add method add_constraint() to model for compatibility with docplex.mp
    • +
    • Comment method get_fail_status() of SolveResult as deprecated.
    • +
    • Fix problem of wrong import of deque in collections.abc
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.14.186 (2020.05):

+
    +
  • Updated tracking events in Watson studio notebooks.
  • +
  • +
    In docplex.mp:
    +
      +
    • Model.solve() will not use solve on cloud unless agent is specifically set to ‘docloud`.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.13.184 (2020.03):

+
    +
  • Removed dependency to the docloud package. Now you need to explicitely install the package using pip install docloud to use DOcplexcloud.
  • +
  • +
    In docplex.mp:
    +
      +
    • added Model.export_as_mps_string(), Model.export_as_sav_string()
    • +
    • +
      fixed a bug with dettime_limit: solving with a deterministic time limit
      +
      was mis-interpreted as a solve failure, returning None.
      +
      +
    • +
    • fixed bug on cplexcloud solve: number of nodes processed was always zero.
    • +
    • repeated solves incorrectly restarted from start of search, now start from where the last solve stopped.
    • +
    • added keyword argument ‘time_limit’ to Model.solve() to set a temporary time limit.
    • +
    • added new method SolveSolution.is_valid_solution()
    • +
    • fixed a bug in ModelReader: ranged constraints bounds were inverted when reading from SAV or MPS.
    • +
    • fixed a bug in Model.set_lex_multiobj(): arguments abstols, reltols were ignored.
    • +
    • added proper type-checking for Model.add_indicator_constraints()
    • +
    • added docplex.mp.check_list/py to check local installation.
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • Enable reading of #line directives when parsing a CPO file
    • +
    • Remove parameter LogSearchTags from public parameters
    • +
    • Fix a minor problem concerning compilation of KPI expressions in CPO format
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.12.182 (2019.12):

+
    +
  • +
    In docplex.mp:
    +
      +
    • Added a LinearRelaxer class to make a linearized copy of a MIP model (if possible). +see class docplex.mp.relax_linear.LinearRelaxer
    • +
    • Conflict refiner default behavior is now identical to CPLEX interactive +(the new behavior is much faster).
    • +
    • Bug fixed: expressions of the form k*x did not notify constraints when modified.
    • +
    • Fixed: message “ignored keyword argument” was incorrectly printed when setting +cts_by_name=True in model constructor.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.11.176 (2019.11):

+
    +
  • Added support for CPLEX 12.10
  • +
  • +
    In docplex.mp:
    +
      +
    • Logical expressions, binary variables, and constraints can now be freely nested with logical operators.
    • +
    • Fixed a print of ‘CPLEX Error 1217’ in log for multi-objective problems.
    • +
    • Fixed a bug when setting log_output to a file name: file was created, but empty.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.10.155 (2019.08):

+
    +
  • Fixed bug in logical_and() when result var is set to 0.
  • +
+
+
+

Changed in 2.10.154 (2019.07):

+
    +
  • Fixed TypeError occuring in python 3.7 in progressData initialization.
  • +
+
+
+

Changed in 2.10.151 (2019.07):

+
    +
  • +
    In docplex.mp:
    +
      +
    • fixed a bug in ModelReader when reading SAV files with no names
    • +
    • fixed a bug in mip starts, which prevented mip starts with piecewise +functions to work properly.
    • +
    • fixed bug on Model.add_indicators() using comprehensions (len() was called).
    • +
    • Added support for the ‘!=’ (not equals) operator in expressions.
    • +
    • Clarified four types of checker: on, off, numeric and full. +Pass checker=<name> at model creation to specify which checker is used.
    • +
    • fixed a bug in solution JSON encoder for nonconvex QP problems.
    • +
    • Add direct support for lazy constraints, see Model.add_lazy_constraints()
    • +
    • Add direct support for user cuts, see Model.add_user_cut_constraints()
    • +
    • Get basis status of variables in LP problems, see Var.basis_status
    • +
    • Read MIP start files (MST format)
    • +
    • Allow to set the effort level for a MIP solution.
    • +
    • Read basis status files (in BAS format)
    • +
    • Read variable priority orders (in ORD format)
    • +
    • fixed bug in functional KPIs, solution argument was not passed on.
    • +
    • Enable constraint name dictionary at Model creation time: Model(cts_by_name=True)
    • +
    • Multi-objective is now pickled correctly
    • +
    • Multi-objective is now copied in Model.copy()
    • +
    • Wrote full documentation on progress listeners
    • +
    • Added Model.set_lp_start_basis() to provide an initial basis for LP problems.
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • When trying to access a solution member that does not exists, an exception is thrown instead of returning None.
    • +
    • Add a new map_solution function that replace in a Python object all model expressions by their value in a solve result.
    • +
    • In CPO parser, fix a problem reading #line statements in startingPoint section.
    • +
    • In CPO parser, skip experimental section ‘expressions’ in ‘startingPoint’ section.
    • +
    • Simplify writing of interval variable domains reduced to a single value.
    • +
    • Adding a second objective function now raises an exception
    • +
    • Add new experimental local solve with a shared library.
    • +
    • Enable iterators to specify the domain of an integer variable
    • +
    • Add global methods get_version_info() and get_solver_verion() in docplex.cp.solver.
    • +
    • By default, generate CPO model without explicit format version.
    • +
    • Add a method reset() on CpoParameters object.
    • +
    • Modeling method allowed_assignments() and forbidden_assignments() can now accept an empty list of tuples.
    • +
    • On CpoModelSolution object, add a function map_solution() thar replace variables by their value in a python object.
    • +
    • Add parser for LP models
    • +
    • Add possibility to import CPO, MZN and LP models in gzip and zip format.
    • +
    • Enhance management of unexpected errors thrown by cpoptimizer.exe
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.9.141 (2019.03):

+
    +
  • +
    In docplex.mp:
    +
      +
    • Removed links to rawgit.com as this service is going end of life.
    • +
    • Model.solve_lexicographic() is deprecated. This method should be used +to perform lexicographic solve with COS 12.8, but with COS 12.9, +Model.set_multi_objective() should be used for solving problems +with multiple objectives.
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • Add KPIs supported by CPO Solver 12.9
    • +
    • Update CPO parser to read KPIs section for format 12.9
    • +
    • Add new examples with KPIs.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.8.125 (2018.10):

+
    +
  • Solving with solver agent ‘docloud’ is deprecated. +Models are now preferably solved with local solver, or the python source can be submitted to DOcplexcloud solve service. +See https://ibm.biz/BdYhhK.
  • +
  • +
    In docplex.mp:
    +
      +
    • solve_lexicographic is being deprecated. In a future version, a new api will be available to support multi-objectives.
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • Fix problem with boolean indicators in no_overlap(), always_constant() and always_equal().
    • +
    • Allow model solution to be used directly as a starting point (ignores what is not integer or interval var).
    • +
    • Add methods domain_min(), domain_max(), domain_iterator() and domain_contains() on both CpoIntVar and CpoIntVarSolution.
    • +
    • Default solver agent is now ‘local’ instead of ‘docloud’. All examples modified consequently.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.7.113 (2018.07):

+
    +
  • +
    In docplex.mp:
    +
      +
    • Multiplying a constant expression by a quadratic expression raised an exception. Now returns the +product of the quadratic expression and the constant value.
    • +
    • Model.solve_lexicographic() on cloud now send the previous pass solution as a MIP start (for MIP problems)
    • +
    • The slack of quadratic constraints always returned zero. Now returns the correct value.
    • +
    • Accessing the dual (or slack) of a constraint that is not added to the model returned zero; now it raises an exception. A constraint must belong to a model to return a valid dual (or slack) value
    • +
    • Range constraints with infeasible domain (i.e. lb > ub) did not fail to solve. Now they raise a modeling exception.
    • +
    • Multiplying two absolute value expressions raised an exception. Now fixed.
    • +
    • When using tuples in variable dictionaries, the default name generation used to generate non-LP-compliant names, +because of ( and ). Now the name generator formats the tuples with a “_” separator without parentheses.
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • Split fzn stuff in a separate package docplex.cp.fzn
    • +
    • Optimize construction of arrays in FZN parser
    • +
    • Enhance FZN parser and save 30% time
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.6.94 (2018.04):

+
    +
  • +
    In docplex.cp:
    +
      +
    • Allow CpoModel.add() to accept list of constraints.
    • +
    • Fix a bug in the conversion of an array of boolean constants into CPO expression.
    • +
    • Extend CpoModel method set_parameters() to accept a dictionary and/or optional list of updates using named arguments.
    • +
    • Method CpoModel.set_parameters() now clone the CpoParameters object given in arguments.
    • +
    • Add a new method CpoModel.add_parameters() that updates parameters associated to the model.
    • +
    • Fix wrong source location (not in real model source) when CpoModel.add() is called from another docplex.cp method.
    • +
    • When constraint auto-naming is on (in particular for refine_conflict(), searchPhases are no more included in the process.
    • +
    • Parameters mean_UB and mean_LB are now optional in standard_deviation()
    • +
    • CpoModel.add() checks that the added expression is limited to constraint, boolean, objective or search phase.
    • +
    • Add documented functions slope_piecewise_linear() and coordinate__piecewise_linear() in modeler.py.
    • +
    • Remove default configuration settings for parameters TimeLimit and Workers.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.5.92 (2018.03):

+
    +
  • +
    docplex.cli gains new features:
    +
      +
    • option --details will display solve details as they are published on +DOcplexcloud.
    • +
    • options --url and -key allow specification of credentials without +using a config file.
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • Fix problem with min() and max() that did not support optional key.
    • +
    • Add a Flatzinc parser capable of reading Minizinc Challenge problems.
    • +
    • Move expression dependencies analysis from model to compiler side.
    • +
    • No more constraint to have a unique name for model expressions. Compiler reallocate private names when needed.
    • +
    • Multiple variables or expressions with the same public name is now allowed.
    • +
    • Replace method CpoModel.get_expression() by CpoModel.get_named_expressions_dict().
    • +
    • Make SolverProgressPanelListener work properly with Python 2
    • +
    • Solve is automatically set to start/next loop when SolverProgressPanelListener is used.
    • +
    • In CpoModel, add a method that allows to substitute a function by another in the whole model.
    • +
    • Overwrite method __bool__ to avoid accidental use of CPO expressions as Python booleans.
    • +
    • Add special cases to search for the local CP Optimizer Interactive executable.
    • +
    • Allow methods min(), max(), min_of() and max_of() to support variable number of arguments.
    • +
    • Allow method all_diff() to support variable number of arguments.
    • +
    • Context parameter ‘length_for_rename’ is deprecated. Only length_for_alias is used.
    • +
    • Add a method add_var() in CpoModelSolution as a shortcut to add_integer_var_solution() and add_interval_var_solution()
    • +
    • Overwrite method __contains__() in CpoModelSolution to easily verify that a solution to a given variable is in the solution.
    • +
    • When called on a model, export_model() and get_cpo_string() disable all model optimization options.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.4.61 (2017.11):

+
    +
  • +
    Both docplex.mp & docplex.cp:
    +
      +
    • Support for CPLEX engines 12.8. Some features of docplex2.4 are available only with engines >= 12.8.
    • +
    • Adding new ports (AIX, plinux).
    • +
    • Examples are now available as Zeppelin notebooks.
    • +
    +
    +
    +
  • +
  • +
    In docplex.mp:
    +
      +
    • Express a linear problem as a scikit-learn transformer by providing a numpy, a pandas or scipy matrix.
    • +
    • Logical constraints: constraint equivalence, if-then & rshift operator.
    • +
    • Meta-constraints: allow the use of discrete +linear constraints in expressions, using their truth value.
    • +
    • Solve hook to add a method to be called at each intermediate solution.
    • +
    • KPIS automatically published at each intermediate solution if running on docplexcloud python worker.
    • +
    • Support for scipy coo & csr matrixes.
    • +
    • Fixed a bug in Model.add_constraints() when passing a string instead of a list of strings.
    • +
    +
    +
    +
  • +
  • +
    In docplex.cp:
    +
      +
    • add new method run_seeds() to execute a model multiple times, available with local solver 12.8.
    • +
    • add support of new solver infos ‘SearchStatus’ and ‘SearchStopCause’.
    • +
    • In method docplex.cp.model.CpoModel.propagate(), add possibility to add an optional constraint to the model.
    • +
    • add domain iterator in integer variables and integer variables solutions, allowing to get domain +as a list of individual integers.
    • +
    • add possibility to identify some model variables as KPIs of the model.
    • +
    • add abort_search() method on solver (not supported everywhere)
    • +
    • Rework code generation to enhance performances and remove unused variables that was pointed by removed expressions.
    • +
    • add possibility to add one or more CpoSolverListener to put some callback functions +when solve is started, ended, or when a solution is found. +Implementation is provided in new python module docplex.cp.solver.solver_listener that also contains sample +listeners SolverProgressPanelListener and AutoStopListener.
    • +
    • Using parameter context.solver.solve_with_start_next, enable solve() method to execute a start/next loop instead +of standard solve. This enables, for optimization problems, usage of SolveListeners with a greater progress accuracy.
    • +
    • Completely remove deprecated ‘angel’ to identify local solver.
    • +
    • Deprecate usage of methods minimize() and maximize() on docplex.cp.CpoModel.
    • +
    • Add methods get_objective_bounds() and get_objective_gaps() in solution objects.
    • +
    +
    +
    +
  • +
+
+
+

Changed in 2.3.44 (2017.09):

+
    +
  • Module docplex.cp.model.solver_angel.py has been renamed solver_local.py. +A shadow copy with previous name still exist to preserve ascending compatibility. +Module docplex.cp.model.config.py is modified to refer this new module.
  • +
  • Class docplex.cp.model.solver_local.SolverAngel has been renamed SolverLocal. +A shadow copy with previous name still exist to preserve ascending compatibility.
  • +
  • Class docplex.cp.model.solver_local.AngelException has been renamed LocalSolverException. +A shadow copy with previous name still exist to preserve ascending compatibility.
  • +
  • Functions logical_and() and logical_or() are able to accept a list of model boolean expressions.
  • +
  • Fix defect on allowed_assignments() and forbiden_assignments() that was wrongly converting +list of tupes into tuple_set.
  • +
  • Update all examples to add comments and split them in sections data / prepare / model / solve
  • +
  • Add new sched_RCPSPMM_json.py example that reads data from JSON file instead of raw data file.
  • +
  • Rename all visu examples with more explicit names.
  • +
  • Remove the object class CpoTupleSet. Tuple sets can be constructed only by calling tuple_set() method, or more +simply by passing directly a Python iterable of iterables when a tupleset is required +(in expressions allowed_assignments() and forbidden_assignments)
  • +
  • Allow logical_and() and logical_or() to accept a list of boolean expressions.
  • +
  • Add overloading of builtin functions all() and any() as other form of logical_and() and logical_or().
  • +
  • In no_overlap() and state_function(), transition matrix can be passed directly as a Python iterable of iterables of integers,
  • +
  • Editable transition matrix, created with a size only, is deprecated. However it is still available for ascending compatibility.
  • +
  • Add conditional() modeling function
  • +
  • Parameter ‘AutomaticReplay’ is deprecated.
  • +
  • Add get_search_status() and get_stop_cause() on object CpoSolveResult, available for solver COS12.8
  • +
  • Improved performance of Var.reduced_cost() in docplex.mp.
  • +
+
+
+

Changed in 2.2.34 (2017.07):

+
    +
  • Methods docplex.cp.model.export_model() and docplex.cp.model.import_model() +have been added to respectively generate or parse a model in CPO format.
  • +
  • Methods docplex.cp.model.minimize() and docplex.cp.model.maximize() +have been added to directly indicate an objective at model level.
  • +
  • Notebook example scheduling_tuto.ipynb contains an extensive tutorial +to solve scheduling problems with CP.
  • +
  • Modeling method sum() now supports sum of cumul expressions.
  • +
  • Methods docplex.cp.model.start_search() allows to start a new +search sequence directly from the model object.
  • +
  • When setting context.solver.auto_publish is set, and using the CPLEX +engine, KPIs and current objective are automatically published when the +script is run on DOcplexcloud Python worker.
  • +
  • When setting context.solver.auto_publish is set, and using the CP +engine, current objective is automatically published when the +script is run on DOcplexcloud Python worker.
  • +
  • docplex.util.environment.Environment.set_stop_callback and +docplex.util.environment.Environment.get_stop_callback are added so that +you can add a callback when the DOcplexcloud job is aborted.
  • +
+
+
+

Changed in 2.1.28:

+
    +
  • New methods Model.logical_or() and Model.logical_and() handle +logical operations on binary variables.
  • +
  • DOcplex now supports CPLEX 12.7.1 and Benders decomposition. Set annotations +on constraints and variables using the benders_annotation property and use +the proper CPLEX parameters governing Benders decomposition.
  • +
  • CPLEX tutorials: in the documentation and as notebooks in the examples.
  • +
  • Fixed a bug in docplex.mp.solution.SolveSolution.display() and in +docplex.mp.solution.Model.report_kpi() when using unicode variable names.
  • +
  • There’s now a simple command line interface for DOcplexcloud. It can be run +in a terminal. python -m docplex.cli help for more info. That command +line reads your DOcplexcloud credentials in your cplex_config.py file. It +allows you to submit, list, delete jobs on DOcplexcloud. The cli is available +in notebooks too, using the %docplex_cli magics. %docplex_cli help for +some help. In a notebook, credentials can be passed using %docplex_url and +%docplex_key magics.
  • +
  • Removing constraints in 1 call
  • +
  • Bug fixes when editing an existing model.
  • +
  • Bug fix in the relaxation mechanism when using docplexcloud.
  • +
+
+
+

Changed in 2.0.15:

+
    +
  • Piecewise linear (PWL) functions are now supported. An API is now available +on docplex.mp.model to create PWL functions and to create constraints using these PWL functions. +PWL functions may be defined with breakpoints (default API) or by using slopes. Some simple arithmetic is +also available to build new PWL functions by adding, subtracting, or scaling existing PWL functions.
  • +
  • DOcplex has undergone a significant overhaul effort that has resulted in an average of 30-50% improvement +of modeling run-time performance. All parts of the API benefit from the performance improvements: creation of variables and constraints, removal of constraints, computation of sums of variables, and so on.
  • +
  • Constraints are now fully editable: +the expressions of a constraint can be modified. +Similarly, the objective expression can also be modified. This allows for complex workflows in which the model is modified after a solve and then solved again.
  • +
  • docplex is now available on Anaconda cloud and can be installed via the conda installation packager. +See the IBM Anaconda home +CPLEX Community Edition for Python is also provided on Anaconda Cloud to get free local solving capabilities with limitations.
  • +
  • Support of ~/.docplexrc configuration files for docplex.mp.context.Context is now dropped. +This feature has been deprecated since 1.0.0.
  • +
  • Known incompatibility: class docplex.mp.model.AbstractModel moved to docplex.mp.absmodel.AbstractModel. +Samples using this class have been updated.
  • +
+
+
+

Changed in 1.0.630:

+
    +
  • Added support for CPLEX 12.7 and Python 3.5.
  • +
  • Upgraded the DOcplexcloud client to version 1.0.202.
  • +
  • Module docplex.mp.advmodel is now officially supported. This module +provides support for efficient, specialized aggregator methods for large +models.
  • +
  • When solving on DOcplexcloud, proxies can now be specified with the +context.solver.docloud.proxies property.
  • +
  • When two constraints are defined with the same name, issue a warning instead of +a fatal exception. The last constraint defined will take over the first one in the name directory.
  • +
  • Fix ValueError when passing a pandas DataFrame as variable keys (using +DataFrame indexes).
  • +
  • Solution.get_values() returns a collection of variable values in one call.
  • +
  • docplex.mp.model no longer imports docloud.status. Any status +previously initialized as JobSolveStatus.UNKNOWN is now initialized as +None.
  • +
  • Minor improvements to notebooks and examples.
  • +
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/README.md.html b/docs/2.24.232/README.md.html new file mode 100644 index 0000000..9ee6c84 --- /dev/null +++ b/docs/2.24.232/README.md.html @@ -0,0 +1,191 @@ + + + + + + + + + README.md — IBM® Decision Optimization CPLEX® Modeling for Python (DOcplex) V2.24 documentation + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

README.md

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
# IBM&reg; Decision Optimization Modeling for Python (DOcplex)
+
+Welcome to the IBM&reg; Decision Optimization Modeling for Python.
+Licensed under the Apache License v2.0.
+
+With this library, you can quickly and easily add the power of optimization to
+your application. You need IBM ILOG CPLEX Optimization Studio to solve the models.
+
+This library is composed of 2 modules:
+
+* IBM&reg; Decision Optimization CPLEX Optimizer Modeling for Python - with namespace docplex.mp
+* IBM&reg; Decision Optimization CP Optimizer Modeling for Python - with namespace docplex.cp
+
+Solving with CPLEX requires that IBM&reg; ILOG CPLEX Optimization Studio V12.10 or later
+is installed on your machine.
+
+This library is numpy friendly.
+
+## Install the library
+
+```
+   pip install docplex
+```
+
+## Get the documentation and examples
+
+* [Latest documentation](http://ibmdecisionoptimization.github.io/docplex-doc/)
+* Documentation archives:
+   * [2.23.222](http://ibmdecisionoptimization.github.io/docplex-doc/2.23.222)
+   * [2.22.213](http://ibmdecisionoptimization.github.io/docplex-doc/2.22.213)
+   * [2.21.207](http://ibmdecisionoptimization.github.io/docplex-doc/2.21.207)
+   * [2.20.204](http://ibmdecisionoptimization.github.io/docplex-doc/2.20.204)
+   * [2.19.202](http://ibmdecisionoptimization.github.io/docplex-doc/2.19.202)
+   * [2.18.200](http://ibmdecisionoptimization.github.io/docplex-doc/2.18.200)
+   * [2.16.195](http://ibmdecisionoptimization.github.io/docplex-doc/2.16.195)
+* [Examples](https://github.com/IBMDecisionOptimization/docplex-examples)
+
+## Get your IBM&reg; ILOG CPLEX Optimization Studio edition
+
+- You can get a free [Community Edition](https://www.ibm.com/account/reg/us-en/signup?formid=urx-20028)
+ of CPLEX Optimization Studio, with limited solving capabilities in term of problem size.
+
+- Faculty members, research professionals at accredited institutions can get access to an unlimited version of CPLEX through the
+ [IBM&reg; Academic Initiative](http://ibm.biz/cplex-free-for-students).
+
+## Dependencies
+
+These third-party dependencies are automatically installed with ``pip``
+
+- [futures](https://pypi.python.org/pypi/futures)
+- [requests](https://pypi.python.org/pypi/requests)
+- [six](https://pypi.python.org/pypi/six)
+- [certifi](https://pypi.python.org/pypi/certifi)
+- [chardet](https://pypi.python.org/pypi/chardet)
+- [idna](https://pypi.python.org/pypi/idna)
+- [urllib3](https://pypi.python.org/pypi/urllib3)
+
+
+## License
+
+This library is delivered under the  Apache License Version 2.0, January 2004 (see LICENSE.txt).
+
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/_images/equ_mathProgr12_default.png b/docs/2.24.232/_images/equ_mathProgr12_default.png new file mode 100644 index 0000000000000000000000000000000000000000..c4ddeceb658e627e8bca67c67a1fa4388fc2623e GIT binary patch literal 9915 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 literal 0 HcmV?d00001 diff --git a/docs/2.24.232/_images/equ_mathProgr23_default.gif b/docs/2.24.232/_images/equ_mathProgr23_default.gif new file mode 100644 index 0000000000000000000000000000000000000000..6a41eeec5d451f6b045e3b62d09ee0181e64da24 GIT binary patch literal 5041 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 B*X95K literal 0 HcmV?d00001 diff --git a/docs/2.24.232/_images/ln_docloud-engine.png b/docs/2.24.232/_images/ln_docloud-engine.png new file mode 100644 index 0000000000000000000000000000000000000000..67e08e6e6d2a353c947728b092a2462d82e8d81b GIT binary patch literal 7291 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 KlWCMP5BooN>{8JH literal 0 HcmV?d00001 diff --git a/docs/2.24.232/_static/ajax-loader.gif b/docs/2.24.232/_static/ajax-loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..61faf8cab23993bd3e1560bff0668bd628642330 GIT binary patch literal 673 zcmZ?wbhEHb6krfw_{6~Q|Nno%(3)e{?)x>&1u}A`t?OF7Z|1gRivOgXi&7IyQd1Pl zGfOfQ60;I3a`F>X^fL3(@);C=vM_KlFfb_o=k{|A33hf2a5d61U}gjg=>Rd%XaNQW zW@Cw{|b%Y*pl8F?4B9 zlo4Fz*0kZGJabY|>}Okf0}CCg{u4`zEPY^pV?j2@h+|igy0+Kz6p;@SpM4s6)XEMg z#3Y4GX>Hjlml5ftdH$4x0JGdn8~MX(U~_^d!Hi)=HU{V%g+mi8#UGbE-*ao8f#h+S z2a0-5+vc7MU$e-NhmBjLIC1v|)9+Im8x1yacJ7{^tLX(ZhYi^rpmXm0`@ku9b53aN zEXH@Y3JaztblgpxbJt{AtE1ad1Ca>{v$rwwvK(>{m~Gf_=-Ro7Fk{#;i~+{{>QtvI yb2P8Zac~?~=sRA>$6{!(^3;ZP0TPFR(G_-UDU(8Jl0?(IXu$~#4A!880|o%~Al1tN literal 0 HcmV?d00001 diff --git a/docs/2.24.232/_static/background_b01.png b/docs/2.24.232/_static/background_b01.png new file mode 100644 index 0000000000000000000000000000000000000000..353f26dde0803aa172c23e21ef6ac068e1253bc8 GIT binary patch literal 78 zcmeAS@N?(olHy`uVBq!ia0vp^%plAGBp8aFUnK)6QBN1gkP61+AN9}d56}GnU-I97 adu9eB2afnIr`wM~3O!x@T-G@yGywpsd=;et literal 0 HcmV?d00001 diff --git a/docs/2.24.232/_static/basic.css b/docs/2.24.232/_static/basic.css new file mode 100644 index 0000000..0807176 --- /dev/null +++ b/docs/2.24.232/_static/basic.css @@ -0,0 +1,676 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 450px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist td { + vertical-align: top; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +div.code-block-caption { + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +div.code-block-caption + div > div.highlight > pre { + margin-top: 0; +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + padding: 1em 1em 0; +} + +div.literal-block-wrapper div.highlight { + margin: 0; +} + +code.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +code.descclassname { + background-color: transparent; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: relative; + left: 0px; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/2.24.232/_static/bizstyle.css b/docs/2.24.232/_static/bizstyle.css new file mode 100644 index 0000000..0464a74 --- /dev/null +++ b/docs/2.24.232/_static/bizstyle.css @@ -0,0 +1,490 @@ +/* + * bizstyle.css_t + * ~~~~~~~~~~~~~~ + * + * Sphinx stylesheet -- business style theme. + * + * :copyright: Copyright 2011-2014 by Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', + 'Verdana', sans-serif; + font-size: 14px; + letter-spacing: -0.01em; + line-height: 150%; + text-align: center; + background-color: white; + background-image: url(background_b01.png); + color: black; + padding: 0; + border-right: 1px solid #336699; + border-left: 1px solid #336699; + + margin: 0px 40px 0px 40px; +} + +div.document { + background-color: white; + text-align: left; + background-repeat: repeat-x; + + -moz-box-shadow: 2px 2px 5px #000; + -webkit-box-shadow: 2px 2px 5px #000; +} + +div.bodywrapper { + margin: 0 0 0 240px; + border-left: 1px solid #ccc; +} + +div.body { + margin: 0; + padding: 0.5em 20px 20px 20px; +} + +div.related { + font-size: 1em; + + -moz-box-shadow: 2px 2px 5px #000; + -webkit-box-shadow: 2px 2px 5px #000; +} + +div.related ul { + background-color: #336699; + height: 100%; + overflow: hidden; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; +} + +div.related ul li { + color: white; + margin: 0; + padding: 0; + height: 2em; + float: left; +} + +div.related ul li.right { + float: right; + margin-right: 5px; +} + +div.related ul li a { + margin: 0; + padding: 0 5px 0 5px; + line-height: 1.75em; + color: #fff; +} + +div.related ul li a:hover { + color: #fff; + text-decoration: underline; +} + +div.sphinxsidebarwrapper { + padding: 0; +} + +div.sphinxsidebar { + margin: 0; + padding: 0.5em 12px 12px 12px; + width: 210px; + font-size: 1em; + text-align: left; +} + +div.sphinxsidebar h3, div.sphinxsidebar h4 { + margin: 1em 0 0.5em 0; + font-size: 1em; + padding: 0.1em 0 0.1em 0.5em; + color: white; + border: 1px solid #336699; + background-color: #336699; +} + +div.sphinxsidebar h3 a { + color: white; +} + +div.sphinxsidebar ul { + padding-left: 1.5em; + margin-top: 7px; + padding: 0; + line-height: 130%; +} + +div.sphinxsidebar ul ul { + margin-left: 20px; +} + +div.sphinxsidebar input { + border: 1px solid #336699; +} + +div.footer { + background-color: white; + color: #336699; + padding: 3px 8px 3px 0; + clear: both; + font-size: 0.8em; + text-align: right; + border-bottom: 1px solid #336699; + + -moz-box-shadow: 2px 2px 5px #000; + -webkit-box-shadow: 2px 2px 5px #000; +} + +div.footer a { + color: #336699; + text-decoration: underline; +} + +/* -- body styles ----------------------------------------------------------- */ + +p { + margin: 0.8em 0 0.5em 0; +} + +a { + color: #336699; + text-decoration: none; +} + +a:hover { + color: #336699; + text-decoration: underline; +} + +div.body a { + text-decoration: underline; +} + +h1, h2, h3 { + color: #336699; +} + +h1 { + margin: 0; + padding: 0.7em 0 0.3em 0; + font-size: 1.5em; +} + +h2 { + margin: 1.3em 0 0.2em 0; + font-size: 1.35em; + padding-bottom: .5em; + border-bottom: 1px solid #336699; +} + +h3 { + margin: 1em 0 -0.3em 0; + font-size: 1.2em; + padding-bottom: .3em; + border-bottom: 1px solid #CCCCCC; +} + +div.body h1 a, div.body h2 a, div.body h3 a, +div.body h4 a, div.body h5 a, div.body h6 a { + color: black!important; +} + +h1 a.anchor, h2 a.anchor, h3 a.anchor, +h4 a.anchor, h5 a.anchor, h6 a.anchor { + display: none; + margin: 0 0 0 0.3em; + padding: 0 0.2em 0 0.2em; + color: #aaa!important; +} + +h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, +h5:hover a.anchor, h6:hover a.anchor { + display: inline; +} + +h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, +h5 a.anchor:hover, h6 a.anchor:hover { + color: #777; + background-color: #eee; +} + +a.headerlink { + color: #c60f0f!important; + font-size: 1em; + margin-left: 6px; + padding: 0 4px 0 4px; + text-decoration: none!important; +} + +a.headerlink:hover { + background-color: #ccc; + color: white!important; +} + +cite, code, tt { + font-family: 'Consolas', 'Deja Vu Sans Mono', + 'Bitstream Vera Sans Mono', monospace; + font-size: 0.95em; + letter-spacing: 0.01em; +} + +code { + background-color: #F2F2F2; + border-bottom: 1px solid #ddd; + color: #333; +} + +code.descname, code.descclassname, code.xref { + border: 0; +} + +hr { + border: 1px solid #abc; + margin: 2em; +} + +a code { + border: 0; + color: #CA7900; +} + +a code:hover { + color: #2491CF; +} + +pre { + background-color: transparent !important; + font-family: 'Consolas', 'Deja Vu Sans Mono', + 'Bitstream Vera Sans Mono', monospace; + font-size: 0.95em; + letter-spacing: 0.015em; + line-height: 120%; + padding: 0.5em; + border-right: 5px solid #ccc; + border-left: 5px solid #ccc; +} + +pre a { + color: inherit; + text-decoration: underline; +} + +td.linenos pre { + padding: 0.5em 0; +} + +div.quotebar { + background-color: #f8f8f8; + max-width: 250px; + float: right; + padding: 2px 7px; + border: 1px solid #ccc; +} + +div.topic { + background-color: #f8f8f8; +} + +table { + border-collapse: collapse; + margin: 0 -0.5em 0 -0.5em; +} + +table td, table th { + padding: 0.2em 0.5em 0.2em 0.5em; +} + +div.admonition { + font-size: 0.9em; + margin: 1em 0 1em 0; + border: 3px solid #cccccc; + background-color: #f7f7f7; + padding: 0; +} + +div.admonition p { + margin: 0.5em 1em 0.5em 1em; + padding: 0; +} + +div.admonition li p { + margin-left: 0; +} + +div.admonition pre, div.warning pre { + margin: 0; +} + +div.highlight { + margin: 0.4em 1em; +} + +div.admonition p.admonition-title { + margin: 0; + padding: 0.1em 0 0.1em 0.5em; + color: white; + border-bottom: 3px solid #cccccc; + font-weight: bold; + background-color: #165e83; +} + +div.danger { border: 3px solid #f0908d; background-color: #f0cfa0; } +div.error { border: 3px solid #f0908d; background-color: #ede4cd; } +div.warning { border: 3px solid #f8b862; background-color: #f0cfa0; } +div.caution { border: 3px solid #f8b862; background-color: #ede4cd; } +div.attention { border: 3px solid #f8b862; background-color: #f3f3f3; } +div.important { border: 3px solid #f0cfa0; background-color: #ede4cd; } +div.note { border: 3px solid #f0cfa0; background-color: #f3f3f3; } +div.hint { border: 3px solid #bed2c3; background-color: #f3f3f3; } +div.tip { border: 3px solid #bed2c3; background-color: #f3f3f3; } + +div.danger p.admonition-title, div.error p.admonition-title { + background-color: #b7282e; + border-bottom: 3px solid #f0908d; +} + +div.caution p.admonition-title, +div.warning p.admonition-title, +div.attention p.admonition-title { + background-color: #f19072; + border-bottom: 3px solid #f8b862; +} + +div.note p.admonition-title, div.important p.admonition-title { + background-color: #f8b862; + border-bottom: 3px solid #f0cfa0; +} + +div.hint p.admonition-title, div.tip p.admonition-title { + background-color: #7ebea5; + border-bottom: 3px solid #bed2c3; +} + +div.admonition ul, div.admonition ol, +div.warning ul, div.warning ol { + margin: 0.1em 0.5em 0.5em 3em; + padding: 0; +} + +div.versioninfo { + margin: 1em 0 0 0; + border: 1px solid #ccc; + background-color: #DDEAF0; + padding: 8px; + line-height: 1.3em; + font-size: 0.9em; +} + +.viewcode-back { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', + 'Verdana', sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} + +p.versionchanged span.versionmodified { + font-size: 0.9em; + margin-right: 0.2em; + padding: 0.1em; + background-color: #DCE6A0; +} + +/* -- table styles ---------------------------------------------------------- */ + +table.docutils { + margin: 1em 0; + padding: 0; + border: 1px solid white; + background-color: #f7f7f7; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 1px solid white; + border-bottom: 1px solid white; +} + +table.docutils td p { + margin-top: 0; + margin-bottom: 0.3em; +} + +table.field-list td, table.field-list th { + border: 0 !important; + word-break: break-word; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + color: white; + text-align: left; + padding-right: 5px; + background-color: #82A0BE; +} + +div.literal-block-wrapper div.code-block-caption { + background-color: #EEE; + border-style: solid; + border-color: #CCC; + border-width: 1px 5px; +} + +/* WIDE DESKTOP STYLE */ +@media only screen and (min-width: 1176px) { +body { + margin: 0 40px 0 40px; +} +} + +/* TABLET STYLE */ +@media only screen and (min-width: 768px) and (max-width: 991px) { +body { + margin: 0 40px 0 40px; +} +} + +/* MOBILE LAYOUT (PORTRAIT/320px) */ +@media only screen and (max-width: 767px) { +body { + margin: 0; +} +div.bodywrapper { + margin: 0; + width: 100%; + border: none; +} +div.sphinxsidebar { + display: none; +} +} + +/* MOBILE LAYOUT (LANDSCAPE/480px) */ +@media only screen and (min-width: 480px) and (max-width: 767px) { +body { + margin: 0 20px 0 20px; +} +} + +/* RETINA OVERRIDES */ +@media +only screen and (-webkit-min-device-pixel-ratio: 2), +only screen and (min-device-pixel-ratio: 2) { +} + +/* -- end ------------------------------------------------------------------- */ \ No newline at end of file diff --git a/docs/2.24.232/_static/bizstyle.js b/docs/2.24.232/_static/bizstyle.js new file mode 100644 index 0000000..6f01fc7 --- /dev/null +++ b/docs/2.24.232/_static/bizstyle.js @@ -0,0 +1,41 @@ +// +// bizstyle.js +// ~~~~~~~~~~~ +// +// Sphinx javascript -- for bizstyle theme. +// +// This theme was created by referring to 'sphinxdoc' +// +// :copyright: Copyright 2012-2014 by Sphinx team, see AUTHORS. +// :license: BSD, see LICENSE for details. +// +$(document).ready(function(){ + if (navigator.userAgent.indexOf('iPhone') > 0 || + navigator.userAgent.indexOf('Android') > 0) { + $("li.nav-item-0 a").text("Top"); + } + + $("div.related:first ul li:not(.right) a").slice(1).each(function(i, item){ + if (item.text.length > 20) { + var tmpstr = item.text + $(item).attr("title", tmpstr); + $(item).text(tmpstr.substr(0, 17) + "..."); + } + }); + $("div.related:last ul li:not(.right) a").slice(1).each(function(i, item){ + if (item.text.length > 20) { + var tmpstr = item.text + $(item).attr("title", tmpstr); + $(item).text(tmpstr.substr(0, 17) + "..."); + } + }); +}); + +$(window).resize(function(){ + if ($(window).width() <= 776) { + $("li.nav-item-0 a").text("Top"); + } + else { + $("li.nav-item-0 a").text("IBM® Decision Optimization CPLEX® Modeling for Python (DOcplex) V2.24 documentation"); + } +}); \ No newline at end of file diff --git a/docs/2.24.232/_static/comment-bright.png b/docs/2.24.232/_static/comment-bright.png new file mode 100644 index 0000000000000000000000000000000000000000..15e27edb12ac25701ac0ac21b97b52bb4e45415e GIT binary patch literal 756 zcmVgfIX78 z$8Pzv({A~p%??+>KickCb#0FM1rYN=mBmQ&Nwp<#JXUhU;{|)}%&s>suq6lXw*~s{ zvHx}3C%<;wE5CH!BR{p5@ml9ws}y)=QN-kL2?#`S5d*6j zk`h<}j1>tD$b?4D^N9w}-k)bxXxFg>+#kme^xx#qg6FI-%iv2U{0h(Y)cs%5a|m%Pn_K3X_bDJ>EH#(Fb73Z zfUt2Q3B>N+ot3qb*DqbTZpFIn4a!#_R-}{?-~Hs=xSS6p&$sZ-k1zDdtqU`Y@`#qL z&zv-~)Q#JCU(dI)Hf;$CEnK=6CK50}q7~wdbI->?E07bJ0R;!GSQTs5Am`#;*WHjvHRvY?&$Lm-vq1a_BzocI^ULXV!lbMd%|^B#fY;XX)n<&R^L z=84u1e_3ziq;Hz-*k5~zwY3*oDKt0;bM@M@@89;@m*4RFgvvM_4;5LB!@OB@^WbVT zjl{t;a8_>od-~P4 m{5|DvB&z#xT;*OnJqG}gk~_7HcNkCr0000W zanA~u9RIXo;n7c96&U)YLgs-FGlx~*_c{Jgvesu1E5(8YEf&5wF=YFPcRe@1=MJmi zag(L*xc2r0(slpcN!vC5CUju;vHJkHc*&70_n2OZsK%O~A=!+YIw z7zLLl7~Z+~RgWOQ=MI6$#0pvpu$Q43 zP@36QAmu6!_9NPM?o<1_!+stoVRRZbW9#SPe!n;#A_6m8f}|xN1;H{`0RoXQ2LM47 zt(g;iZ6|pCb@h2xk&(}S3=EVBUO0e90m2Lp5CB<(SPIaB;n4))3JB87Or#XPOPcum z?<^(g+m9}VNn4Y&B`g8h{t_$+RB1%HKRY6fjtd-<7&EsU;vs0GM(Lmbhi%Gwcfs0FTF}T zL{_M6Go&E0Eg8FuB*(Yn+Z*RVTBE@10eIOb3El^MhO`GabDll(V0&FlJi2k^;q8af zkENdk2}x2)_KVp`5OAwXZM;dG0?M-S)xE1IKDi6BY@5%Or?#aZ9$gcX)dPZ&wA1a< z$rFXHPn|TBf`e?>Are8sKtKrKcjF$i^lp!zkL?C|y^vlHr1HXeVJd;1I~g&Ob-q)& z(fn7s-KI}G{wnKzg_U5G(V%bX6uk zIa+<@>rdmZYd!9Y=C0cuchrbIjuRB_Wq{-RXlic?flu1*_ux}x%(HDH&nT`k^xCeC ziHi1!ChH*sQ6|UqJpTTzX$aw8e(UfcS^f;6yBWd+(1-70zU(rtxtqR%j z-lsH|CKQJXqD{+F7V0OTv8@{~(wp(`oIP^ZykMWgR>&|RsklFMCnOo&Bd{le} zV5F6424Qzl;o2G%oVvmHgRDP9!=rK8fy^!yV8y*4p=??uIRrrr0?>O!(z*g5AvL2!4z0{sq%vhG*Po}`a<6%kTK5TNhtC8}rXNu&h^QH4A&Sk~Autm*s~45(H7+0bi^MraaRVzr05hQ3iK?j` zR#U@^i0WhkIHTg29u~|ypU?sXCQEQgXfObPW;+0YAF;|5XyaMAEM0sQ@4-xCZe=0e z7r$ofiAxn@O5#RodD8rh5D@nKQ;?lcf@tg4o+Wp44aMl~c47azN_(im0N)7OqdPBC zGw;353_o$DqGRDhuhU$Eaj!@m000000NkvXXu0mjfjZ7Z_ literal 0 HcmV?d00001 diff --git a/docs/2.24.232/_static/css3-mediaqueries.js b/docs/2.24.232/_static/css3-mediaqueries.js new file mode 100644 index 0000000..59735f5 --- /dev/null +++ b/docs/2.24.232/_static/css3-mediaqueries.js @@ -0,0 +1 @@ +if(typeof Object.create!=="function"){Object.create=function(e){function t(){}t.prototype=e;return new t}}var ua={toString:function(){return navigator.userAgent},test:function(e){return this.toString().toLowerCase().indexOf(e.toLowerCase())>-1}};ua.version=(ua.toString().toLowerCase().match(/[\s\S]+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[])[1];ua.webkit=ua.test("webkit");ua.gecko=ua.test("gecko")&&!ua.webkit;ua.opera=ua.test("opera");ua.ie=ua.test("msie")&&!ua.opera;ua.ie6=ua.ie&&document.compatMode&&typeof document.documentElement.style.maxHeight==="undefined";ua.ie7=ua.ie&&document.documentElement&&typeof document.documentElement.style.maxHeight!=="undefined"&&typeof XDomainRequest==="undefined";ua.ie8=ua.ie&&typeof XDomainRequest!=="undefined";var domReady=function(){var e=[];var t=function(){if(!arguments.callee.done){arguments.callee.done=true;for(var t=0;t=200&&r.status<300||r.status===304||navigator.userAgent.indexOf("Safari")>-1&&typeof r.status==="undefined"){t(r.responseText)}else{n()}document.documentElement.style.cursor="";r=null}};r.send("")};var l=function(t){t=t.replace(e.REDUNDANT_COMPONENTS,"");t=t.replace(e.REDUNDANT_WHITESPACE,"$1");t=t.replace(e.WHITESPACE_IN_PARENTHESES,"($1)");t=t.replace(e.MORE_WHITESPACE," ");t=t.replace(e.FINAL_SEMICOLONS,"}");return t};var c={stylesheet:function(t){var n={};var r=[],i=[],s=[],o=[];var u=t.cssHelperText;var a=t.getAttribute("media");if(a){var f=a.toLowerCase().split(",")}else{var f=["all"]}for(var l=0;l-1&&a.href&&a.href.length!==0&&!a.disabled){r[r.length]=a}}if(r.length>0){var c=0;var d=function(){c++;if(c===r.length){i()}};var v=function(t){var n=t.href;f(n,function(r){r=l(r).replace(e.RELATIVE_URLS,"url("+n.substring(0,n.lastIndexOf("/"))+"/$1)");t.cssHelperText=r;d()},d)};for(u=0;u0){r.setAttribute("media",t.join(","))}document.getElementsByTagName("head")[0].appendChild(r);if(r.styleSheet){r.styleSheet.cssText=e}else{r.appendChild(document.createTextNode(e))}r.addedWithCssHelper=true;if(typeof n==="undefined"||n===true){cssHelper.parsed(function(t){var n=p(r,e);for(var i in n){if(n.hasOwnProperty(i)){g(i,n[i])}}a("newStyleParsed",r)})}else{r.parsingDisallowed=true}return r},removeStyle:function(e){return e.parentNode.removeChild(e)},parsed:function(e){if(n){s(e)}else{if(typeof t!=="undefined"){if(typeof e==="function"){e(t)}}else{s(e);d()}}},stylesheets:function(e){cssHelper.parsed(function(t){e(m.stylesheets||y("stylesheets"))})},mediaQueryLists:function(e){cssHelper.parsed(function(t){e(m.mediaQueryLists||y("mediaQueryLists"))})},rules:function(e){cssHelper.parsed(function(t){e(m.rules||y("rules"))})},selectors:function(e){cssHelper.parsed(function(t){e(m.selectors||y("selectors"))})},declarations:function(e){cssHelper.parsed(function(t){e(m.declarations||y("declarations"))})},properties:function(e){cssHelper.parsed(function(t){e(m.properties||y("properties"))})},broadcast:a,addListener:function(e,t){if(typeof t==="function"){if(!u[e]){u[e]={listeners:[]}}u[e].listeners[u[e].listeners.length]=t}},removeListener:function(e,t){if(typeof t==="function"&&u[e]){var n=u[e].listeners;for(var r=0;r=a||s&&l0}}else if("device-height"===e.substring(r-13,r)){c=screen.height;if(t!==null){if(u==="length"){return i&&c>=a||s&&c0}}else if("width"===e.substring(r-5,r)){l=document.documentElement.clientWidth||document.body.clientWidth;if(t!==null){if(u==="length"){return i&&l>=a||s&&l0}}else if("height"===e.substring(r-6,r)){c=document.documentElement.clientHeight||document.body.clientHeight;if(t!==null){if(u==="length"){return i&&c>=a||s&&c0}}else if("device-aspect-ratio"===e.substring(r-19,r)){return u==="aspect-ratio"&&screen.width*a[1]===screen.height*a[0]}else if("color-index"===e.substring(r-11,r)){var h=Math.pow(2,screen.colorDepth);if(t!==null){if(u==="absolute"){return i&&h>=a||s&&h0}}else if("color"===e.substring(r-5,r)){var p=screen.colorDepth;if(t!==null){if(u==="absolute"){return i&&p>=a||s&&p0}}else if("resolution"===e.substring(r-10,r)){var d;if(f==="dpcm"){d=o("1cm")}else{d=o("1in")}if(t!==null){if(u==="resolution"){return i&&d>=a||s&&d0}}else{return false}};var a=function(e){var t=e.getValid();var n=e.getExpressions();var r=n.length;if(r>0){for(var i=0;i0){u=false;for(var f=0;f0){l[c++]=","}l[c++]=h}}if(l.length>0){r[r.length]=cssHelper.addStyle("@media "+l.join("")+"{"+e.getCssText()+"}",t,false)}};var l=function(e,t){for(var n=0;n0}}var o=[],u=[];for(var f in i){if(i.hasOwnProperty(f)){o[o.length]=f;if(i[f]){u[u.length]=f}if(f==="all"){n=true}}}if(u.length>0){r[r.length]=cssHelper.addStyle(e.getCssText(),u,false)}var c=e.getMediaQueryLists();if(n){l(c)}else{l(c,o)}};var h=function(e){for(var t=0;td||Math.abs(s-t)>d){e=n;t=s;clearTimeout(r);r=setTimeout(function(){if(!i()){p()}else{cssHelper.broadcast("cssMediaQueriesTested")}},500)}};window.onresize=function(){var e=window.onresize||function(){};return function(){e();s()}}()};var m=document.documentElement;m.style.marginLeft="-32767px";setTimeout(function(){m.style.marginLeft=""},5e3);return function(){if(!i()){cssHelper.addListener("newStyleParsed",function(e){c(e.cssHelperParsed.stylesheet)});cssHelper.addListener("cssMediaQueriesTested",function(){if(ua.ie){m.style.width="1px"}setTimeout(function(){m.style.width="";m.style.marginLeft=""},0);cssHelper.removeListener("cssMediaQueriesTested",arguments.callee)});s();p()}else{m.style.marginLeft=""}v()}}());try{document.execCommand("BackgroundImageCache",false,true)}catch(e){} diff --git a/docs/2.24.232/_static/css3-mediaqueries_src.js b/docs/2.24.232/_static/css3-mediaqueries_src.js new file mode 100644 index 0000000..f21dd49 --- /dev/null +++ b/docs/2.24.232/_static/css3-mediaqueries_src.js @@ -0,0 +1,1104 @@ +/* +css3-mediaqueries.js - CSS Helper and CSS3 Media Queries Enabler + +author: Wouter van der Graaf +version: 1.0 (20110330) +license: MIT +website: http://code.google.com/p/css3-mediaqueries-js/ + +W3C spec: http://www.w3.org/TR/css3-mediaqueries/ + +Note: use of embedded + + + + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/docs/2.24.232/mp.html b/docs/2.24.232/mp.html new file mode 100644 index 0000000..aa36386 --- /dev/null +++ b/docs/2.24.232/mp.html @@ -0,0 +1,261 @@ + + + + + + + + + Overview of mathematical programming — IBM® Decision Optimization CPLEX® Modeling for Python (DOcplex) V2.24 documentation + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

Overview of mathematical programming

+

Linear programming was revolutionized when CPLEX® software was developed over 20 years ago. CPLEX was the first commercial linear optimizer on the market to be written in the C programming language. CPLEX gave operations researchers unprecedented flexibility, reliability, and performance, allowing them to create novel optimization algorithms, models, and applications.

+

The name CPLEX itself is a pun that is built on the concept of a Simplex algorithm that is written in C: C-Simplex resulted in CPLEX.

+

The Simplex algorithm, invented by George Dantzig in 1947, became the basis for the entire field of mathematical optimization and provided the first practical method to solve a linear programming problem.

+

CPLEX evolved over time to embrace, and become a leader in, the children categories of linear programming, such as integer programming, mixed integer programming, and quadratic programming.

+

Depending on how familiar you are with linear programming, you might be interested in various levels of information about linear programming and how they are handled by CPLEX.

+

The information that is presented in the following sections goes from the highest level fundamental explanation of what linear programming is (and how it runs in CPLEX) down to more advanced concepts.

+

Finally, some external references that help with learning operational research techniques are presented.

+
+

Linear programming: An essential optimization technique

+

The concept behind a linear programming problem is simple. It consists for four basic components:

+
+
    +
  • Decision variables represent quantities to be determined.
  • +
  • An objective function represents how the decision variables affect the cost or value to be optimized (minimized or maximized).
  • +
  • Constraints represent how the decision variables use resources, which are available in limited quantities.
  • +
  • Data quantifies the relationships that are represented in the objective function and the constraints.
  • +
+
+

In a linear program, the objective function and the constraints are linear relationships, meaning that the effect of changing a decision variable is proportional to its magnitude. While this requirement might seem overly restrictive, many real-world business problems can be formulated in this manner.

+_images/equ_mathProgr12_default.png +
+
+

Examples of problems solved with linear programming

+ ++++ + + + + + + + + + + + + + + + + + + + +
IndustryProblem
Manufacturing
    +
  • Blending
  • +
  • Economic planning
  • +
  • Factory planning
  • +
  • Farm planning
  • +
  • Food manufacturing
  • +
  • Refinery planning
  • +
+
Supply chainProduct deployment
Time tablingManpower planning
TransportationNetwork flows
+
+
+

Integer programming

+

Sometimes, linear relationships are not enough to capture the essence of a +business problem, particularly when decisions involve discrete choices, such as +whether or not to open a warehouse at a particular location. For these +situations, you need to use integer programming (or, if the problem includes +both discrete and continuous choices, it is a mixed integer program). Mixed +integer programs can have linear or convex quadratic objectives and linear, +convex quadratic or second-order cone constraints.

+_images/equ_mathProgr23_default.gif +
+
+

Examples of mixed integer programming problems

+

Examples of mixed integer programming problems include:

+
+
    +
  • Vehicle routing
  • +
  • Facility location
  • +
  • Personnel scheduling
  • +
  • Power plant commitment
  • +
  • Costs with fixed and variable components
  • +
  • Materials cutting
  • +
  • Network design
  • +
+
+

Integer programs are much harder to solve than linear programs, but they have important business applications. +CPLEX uses sophisticated mathematical techniques to solve hard integer programs. +These techniques involve systematically searching over possible combinations of the discrete decision variables, by using linear or quadratic programming relaxations +to compute bounds on the value of the optimal solution. They also use linear programming and other techniques to compute linear constraints that cut off possible +solutions that violate the discreteness constraints. +CPLEX’s innovative technologies made it possible to solve mixed integer programs that were previously considered intractable, thus enabling use of optimization +in important business applications.

+
+
+

CPLEX algorithms

+

CPLEX is a high-performance mathematical programming solver for linear programming, mixed integer programming, and quadratic programming

+
    +
  • Problem modeling: IBM® ILOG® CPLEX® Optimizer provides a framework to model business issues mathematically.
  • +
  • Improved profits: IBM ILOG CPLEX Optimizer’s mathematical programming provides technology to help improve efficiency, reduce costs, and increase profitability.
  • +
  • Fundamental algorithms: IBM ILOG CPLEX Optimizer provides flexible, high-performance mathematical programming solvers for linear programming, mixed integer programming, quadratic programming, and quadratically constrained programming problems. These include a distributed parallel algorithm for mixed integer programming to leverage multiple computers to solve difficult problems.
  • +
  • Robust algorithms for large problems: IBM ILOG CPLEX Optimizer has solved problems with millions of constraints and variables.
  • +
  • Industry-leading support: IBM has an impressive rate of product improvement and ample support resources to serve you.
  • +
  • High performance: IBM ILOG CPLEX Optimizer delivers the power needed to solve large, real-world optimization problems, as well as the speed required for today’s interactive decision optimization applications.
  • +
  • Robust and reliable: A large installed base helps us make IBM ILOG CPLEX Optimizer better with each release. Every new feature is tested on the biggest, most diverse model library in the world.
  • +
+
+ +
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/README.md.html b/docs/2.24.232/mp/README.md.html new file mode 100644 index 0000000..b163540 --- /dev/null +++ b/docs/2.24.232/mp/README.md.html @@ -0,0 +1,210 @@ + + + + + + + + + README.md — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

README.md

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
# IBM&reg; Decision Optimization Modeling for Python (DOcplex)
+
+Welcome to the IBM&reg; Decision Optimization Modeling for Python.
+Licensed under the Apache License v2.0.
+
+With this library, you can quickly and easily add the power of optimization to
+your application. You need IBM ILOG CPLEX Optimization Studio to solve the models.
+
+This library is composed of 2 modules:
+
+* IBM&reg; Decision Optimization CPLEX Optimizer Modeling for Python - with namespace docplex.mp
+* IBM&reg; Decision Optimization CP Optimizer Modeling for Python - with namespace docplex.cp
+
+Solving with CPLEX requires that IBM&reg; ILOG CPLEX Optimization Studio V12.10 or later
+is installed on your machine.
+
+This library is numpy friendly.
+
+## Install the library
+
+```
+   pip install docplex
+```
+
+## Get the documentation and examples
+
+* [Latest documentation](http://ibmdecisionoptimization.github.io/docplex-doc/)
+* Documentation archives:
+   * [2.23.222](http://ibmdecisionoptimization.github.io/docplex-doc/2.23.222)
+   * [2.22.213](http://ibmdecisionoptimization.github.io/docplex-doc/2.22.213)
+   * [2.21.207](http://ibmdecisionoptimization.github.io/docplex-doc/2.21.207)
+   * [2.20.204](http://ibmdecisionoptimization.github.io/docplex-doc/2.20.204)
+   * [2.19.202](http://ibmdecisionoptimization.github.io/docplex-doc/2.19.202)
+   * [2.18.200](http://ibmdecisionoptimization.github.io/docplex-doc/2.18.200)
+   * [2.16.195](http://ibmdecisionoptimization.github.io/docplex-doc/2.16.195)
+* [Examples](https://github.com/IBMDecisionOptimization/docplex-examples)
+
+## Get your IBM&reg; ILOG CPLEX Optimization Studio edition
+
+- You can get a free [Community Edition](https://www.ibm.com/account/reg/us-en/signup?formid=urx-20028)
+ of CPLEX Optimization Studio, with limited solving capabilities in term of problem size.
+
+- Faculty members, research professionals at accredited institutions can get access to an unlimited version of CPLEX through the
+ [IBM&reg; Academic Initiative](http://ibm.biz/cplex-free-for-students).
+
+## Dependencies
+
+These third-party dependencies are automatically installed with ``pip``
+
+- [futures](https://pypi.python.org/pypi/futures)
+- [requests](https://pypi.python.org/pypi/requests)
+- [six](https://pypi.python.org/pypi/six)
+- [certifi](https://pypi.python.org/pypi/certifi)
+- [chardet](https://pypi.python.org/pypi/chardet)
+- [idna](https://pypi.python.org/pypi/idna)
+- [urllib3](https://pypi.python.org/pypi/urllib3)
+
+
+## License
+
+This library is delivered under the  Apache License Version 2.0, January 2004 (see LICENSE.txt).
+
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_images/boxes_35_0.png b/docs/2.24.232/mp/_images/boxes_35_0.png new file mode 100644 index 0000000000000000000000000000000000000000..d79fb0575c5511797b2bcb7af6857140414481d8 GIT binary patch literal 21326 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 DUsz@g literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_images/boxes_38_0.png b/docs/2.24.232/mp/_images/boxes_38_0.png new file mode 100644 index 0000000000000000000000000000000000000000..684c6ed56296849628e9ab7c242ef209406db171 GIT binary patch literal 20904 zcmY&=1ymf-vMm}IJV0;>?w;Tl+=2vxySuv++}+*X-95Nl@WCy>K(L^{bJxHBz0X=O zOiy#P&aPd%stH$=mqbM(K!SpTLY0;h`vwICZ4dnZi1-fp*(Of(8F+zn5|LIx1irix zjl+QVAbTkdCnzXn%zxj|^P9fUz(sCnadl^9J5y&j14k1m8v|#1YddFa3&W4DCXP-P zcD5YMY|LzoAI+Ve?Ri;P{^xUMJ4Z7X3{zZuD5#H6(qdm!+_QdXd$=bpt-cA(Sg)!z zU?qHfqK+)19VM62S+m?cRc*FxzCG8nv1-0ufj=+Nsq*P;Up#MK>WM%eC3z1ACmvKM zs-BP@^xKO%adqZ0HXWqpPx*(x@xZ$#l7Tfn%ZY~4ZQ6V0zQ;}+F+hwwUaA0Hrwukv ziV}0IFAk*r?*evu9~EXerA++)|CTL?&tJ;8sDM8mP293(^~OvP#HM6uw|6Ej?_AZr4l zhBg9*5=2%NQGgflwNJDU-QO)3<4pWCOEX=m`ejxu>X3R=&@ZX`9C*j;&S?jJa2Z~U#(x6-*cNvKrV*1x2FsOoT8p94~p zlIm>MTY4mksc|-axb7>TBTGse62xnJDo!&n!;S6pXd~KNN1mL+%V{HYm6F)=plRV_ zpln1CTi@uG`V>FIL>pgdGz4XBzvYDcy?Mid3tHukyg@SciV87H^OpqbiY5$ zOxMvSDk%e2U%yz7Q-n;6u#O}Rx`-hHGfp2VKsPp5Bn&qQV<8x0p|MdR(Y_#Jb$HmA znlkI=SKDvvpT;J^bmZY;>}e;F$W(;(AhI=VacL+VCrx}6K`mcKXD|+NgsXY&Ue*#vp#pm|;>U*hz)Jtf`1;IiQ1Aec6IGcbyX#YE8XmHXWSjFv(*tz~Sbg-ZJvS*^VrdMzyUz(Eb>mm(gZCd_X`5TnG z-J37<;eJ_1Pr)eGL;J(|5BO)-ei*1_HP!3TsZ5jW6e$s56-r1wnV&% zbQ?RsQpA)=fva6OGh#Kony)r11I~7ae2KLfG}W)3acn#94NapHzE;W)S+vaFp5W{% z)G19f(HYocqnyV{83vTK4J{J7wCU(he-@BJhT#;U%g>4_?MBrxB#+?tk)<>sqsMIP zKSo~Iy9@EQ%N7dsGKLXCRq*0_Y`U=zC5uPVEFcd!g9UATc!aA#*EaMtff4$kpvCui z)Aav7MvwSnitoEMkOM=Lwn>^o5+O~kuz3op zm=R)_%;d|#js?jxTL%RNjp6yGeC5jl_agXV$(IixZEAb&*SB4MSCq^N@rb2}p4dPF z!#Nqc_@KnFuV5O@zr0$%pD*_^)7?hYf6gG;w72NuTxW@a--=I$aV@GUOmBK$qXl{B zCY7Wq0~5>Q6n8Owdd+@ws*T>GsTj(D4ve!*IUcJR)4Qk=STR3h6xz-*&H!HkfPPfjb{ZBU2ufJv1a_=-&hfk@E`Y$BSxyX?2%ES#AXyCf|l ziaZg~xQh6-JH@4tVk+AcKf+8qA+K6wb ze%rZ07=E+NU3)`R4} z;~yN^tfd|26jy5b%*lzyJ^R3#-RI&+J|lgjj%f0sL=4mQF{$r^INPr5)*FsU7xKrT zGF%b&APilwIJjU-P!;j&2Zmt>hM}jc>zzL=sxu`@W)Qgo*ncLVU}jV}H-q3pkuu?n zf}S=l5(KkO5AkXe6nm5lX{v?xT13C(E(x-iroD#qC2t5%7Q^JZ8J=dtdU#i-^>49_ zVqsh9BiP|FgsC9@)bAg|Wm)|(AF;R}y1?vyPxhLOlse7^ZG!+8xFHKbCgX&bVY$@I z_vJQ|n4=q_3MwtGl8?tk@?WtX(a~Zcrp1&F!uXt=2EGiPC(iMu{U!pPx-DW(&kVBr zh#3wS)&WP-lhQCRah#|i%nOWJiVUzSx_=bPP0cn@z@><3z}#Dacj6U|xkPe zc|Io78w-o$g@r-DWsqNCnSl7c2_8NQ`R_HTv&y_^k%!Y3dX zq+?OVf-FgjJc{-!m@GvR`(1O;F1r@%2i(0(JEnLk@?hZlHxUq(e51y{EkUFNn#KDB z`C?7+yB=d4SuUJ%7iiPA?*IY@?L={O7qF^-z*f6ugLoDPE0Tu`7qADwa*Mhnt(@RB z3EG#oW5%Ijhf`vP1LH6^|1bb281~s-Eic+yTG|~J*uK*D!65#dgPX@M{P80IV8}|x zns2?IY2y0Mzrn5$9o72>EB*&*_5uEisKgNT>*CFerj8U3 zNgKKA&~MGE&qWj7gZy0~Nw*U2H^O{&sr515sQ{idb^v1HrTzyvgljMgaJ;2SMwR&5 zINqQ7(t3*HCt;5NcUR~CH&zsqlDhR4(5_>PuP*=k#T|#IK(TmmBM720UKdA^Z-m!z zWPgl`)3%eMOz@pA1wba^z25a}E5@$Q+u&_P!T!^BF8ZgNyZZk{3>%li(%axfr9Rt> z8z|yj!)2&T|Fh|Iyy;uG;roTB|BDlsnn)Y){0DxWVu^@eWXxm2YMB%&>!_NfZ3~8E z2D?Fk13iJAd3`==`b$;&`d_l>D3+jTKMjXA3D*PtTNS%_^6l5&1LnO}M8O8Kx_@R9 z=*oE)6SZmCHQ2KA3(ul@n|50aF~#j^B|xl-(3>;Z!soHv&9%l)-x*MqEEs-CO$zcIjk6CW$N%U>K;Z81HNb( z`%o⪼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 literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_images/nurses_pandas_72_1.png b/docs/2.24.232/mp/_images/nurses_pandas_72_1.png new file mode 100644 index 0000000000000000000000000000000000000000..9a8f72a6f032432f5978a1fed70ed2fb252dbc70 GIT binary patch literal 5470 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 LPnVqX{Pw>A-vORY literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_images/nurses_pandas_90_1.png b/docs/2.24.232/mp/_images/nurses_pandas_90_1.png new file mode 100644 index 0000000000000000000000000000000000000000..a57b17944de7f59efa9187c5b5157e4759f06d50 GIT binary patch literal 4193 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

qXdqDNNnjYL>CGncOV9%f_i9uM zLH6mU-L>c-{=>!vjxIr=BBkS_j`&yiSwBLi8`n5>ZsVePsUGJch=N|dnwq+<;DmTK zhsY|p{lWCDmy{u^&@nq&EJDZd)_(oG(Fa+OcuG#wW<=e85tW+j7{7s3+i!oj@Z*ux z6nxH++eVx|Mk^)=v`L#@`&Sr+vW+Q*==9t2#OPJ0nS4+`fm*ujKqWGNQgeSE?Ej+6 z{zj+p+3nhnkwluBaKF$Aev)&=7BM_77hM6vlS*ed5HMi`peHJI5?hyDkQrteH6#P5 zF#9!%ODiiYBQ2m1w!0lI1TH67D4LpZ40f8?{*fGtlfQ&QdiUCi@;H5doS~h{WIs}e zl&d5V)qDZ}{7JqU>&eprb*N73Dx=M_XzR;Hq_pIN{-d4m>nTjvH7SN2_qQCT$2TD@moiK70<*@Dw+50 zdl7Lfz59EaBOesH%@@+9sYapzz%oOP9;->TD4>5qLJNw)5jU&G1C}cQF?&ad zeB2fqXGHzsaX1ziA=V`phTpu7THUP+a7~hP5sB8j437LGEv@+Jk>VN>=RIQ2o%}OM!7h@iKJ+apRlFv&6u5dsweDNdEj#yVnfmTS|Evb7EU@fLAkbS%TMQ# zuZ%Mav&(`EAsNB|#@j5WKo92Lg~-Hk%uZZauYPNZJ6E*qk&IR}RvJPwelKhP!xpe% ZUE?@~Nz6&SDk|$Sklj}Y8@}{S`zNjAdoch2 literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_images/nurses_scheduling_47_0.png b/docs/2.24.232/mp/_images/nurses_scheduling_47_0.png new file mode 100644 index 0000000000000000000000000000000000000000..fe19bafed2bd423ab5976b3a36a37a2d9e1a6428 GIT binary patch literal 53028 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 IWEu9q0EO>9vj6}9 literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_images/oil_blending_43_0.png b/docs/2.24.232/mp/_images/oil_blending_43_0.png new file mode 100644 index 0000000000000000000000000000000000000000..eb1893a2e49a6e90ea59a743982a8055e8c78d75 GIT binary patch literal 9817 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 literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_images/oil_blending_46_0.png b/docs/2.24.232/mp/_images/oil_blending_46_0.png new file mode 100644 index 0000000000000000000000000000000000000000..b826ce7d3cf4a719ff4f39bdb86a9e1b206e46fa GIT binary patch literal 11542 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 r%<{fRZ&*Z1psis*!NdN_psliJJ)ou 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�g`-7 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
)AkMf(IdGSlEt}EI|KEaMK?b^z=bb7j45g zfaa6#=pN+dQ>z~x_gnSlTU9yxSKl=ylCS-r%Dy`qj>FmaMwE)%_;#z3;vE-@E5HXULJCgf8V1%@h`_j8@G1>=l#feh7dam>uh<(?zSXEH@vvOdJknB!-var= z^Rglvu_$h@~+FS{yy#or$L=sCadzKN*8hE`&Dy z5Y+@MrkitHv-Jom`@^(82?{1SZLkq{>Z(SN7GBm7ub*#*831}kvEA(RMz#AC=)W@^ zj5>4b8?Zd8Z1BT;(iyV*KvI#KKdD`VX7!lQ^p+NbiJ*3D%-EJ|R#7>oW9(!Lwza0Q z7@C!w$@b)*)0>Qshep2D|=|K+~l-(*@1?P#M&x7{HrvVOMQ` zM>OuG0?sD)zPaa5NfY~Z)q42(xVRVqu)VFBAb2Z}_#hXs595m6_|m*|$w&7LDKuiC8uiDKq^ofcMZi?mMa7En%p z2c!>H8a=r_AGY^V#7ShPGl;!(P{4)Jq`VWd#ZZ_Vl*ITLP%{O7UxD%31v!-K#Bmju zVua-ck89sn{Pvj^uPqi-UiQX%oVYa1i90TZ`^j-<%Olu>k=8owPlgP?yYRZZXSO|Q zU(9eWj!3UTOv~pp>b%6?_A}zQ2~w#F2=cr8-BcE^79b|RI>5i|5#tVslsgh#fY+4z zOVDcTdYs{PNno`@jPl#jpLE&NLgFMqhCq0V3 z3@PdIoPfLa;o>c?2D5hAV$Q(fCtM0R6*?wr!=2gfT0;>d8^|5)G_GQpe{W!_W~y*X zMmZ452djf1rIRZ?v2U?0-0TJdV1oJA&2bhytKt4LK5Wq?`{1V8j|K+dhw7pAp#MwIsORmEqZYk6%8dkM*!toJobGFN8aia}ju6ww=%9l<~WJI!iip~uw6yS^H922Ky3HbWP z{#)`?h%|-V2;kWNvJZ}TbDiyXfC>*3P*PEN{+W&scXEkR`zQ*aJ6&g%K2uzWx!}by zxE-IBAuwUN-%DM}t=1KyZn>}VM%tLV=(7lUNOU}ncoycK$T3xl>a!^r!MQ@N#U0+V zSxD}MRoZyC$tx7p55gN!a#-MOe2YhJBnYQzmy$t|Hw=LuCs2@>+HVX9gwsV)vML)W zhPPpCG77SQh|5y>L$27|4@bnLH|J@Py_z0AWVn`}tE-bqlO5)wg|YEcHhr9OH~sH?H|XKA4TJ+dI?eJhS8pcF=`y=P z!sRg7vz-*&H1by=UQ5OT3|eG z{?>f>maOTp>Su?K4}ix}!cX2Xya<*oKBRml_lBWQnEuArOvql*fAKyTtI&_))9G&M z7R$)mtWo&_br^0hISa#h#Dl}LQxL8A>5h2yl?R3WvM2!A&bcBdp73c}lfoVBu(Zkq?U!M($X`Xi+TL1R0U(X+p0RS z8Y0=^NET4Y9u$Kn!U(Omvf65E=w>{MqP1laavcVGj!<5Zv3%_6DvmKo9 z@8uTJuJeQHf}N-*=0bEHbr`yIg31Ev+*SZImEKymS7a%0AXm&uK3w4|swnAewu}-s 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 literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_images/ucp_pandas_58_1.png b/docs/2.24.232/mp/_images/ucp_pandas_58_1.png new file mode 100644 index 0000000000000000000000000000000000000000..d90209aad5d61ec9869dca9678b961f046ae8ca9 GIT binary patch literal 6116 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
Dv7qr(o#uI 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 literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_images/ucp_pandas_62_1.png b/docs/2.24.232/mp/_images/ucp_pandas_62_1.png new file mode 100644 index 0000000000000000000000000000000000000000..8be4d8ac81b1831189ca83a04e5830840607a1c0 GIT binary patch literal 15419 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 literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_images/ucp_pandas_75_0.png b/docs/2.24.232/mp/_images/ucp_pandas_75_0.png new file mode 100644 index 0000000000000000000000000000000000000000..1d38c23371cc06ab05c170300f5bccf514339884 GIT binary patch literal 7940 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 b+TM_Z;0J@>R#^o$1_LSzny?Bv%aH#C$w9N{ literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_images/warehouse_40_0.png b/docs/2.24.232/mp/_images/warehouse_40_0.png new file mode 100644 index 0000000000000000000000000000000000000000..54e099b67d605271eecea6c685c89c6b890d5b6b GIT binary patch literal 12758 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 R#Qre@prx({t5UU%`9CtvMV|lw literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_images/warehouse_47_0.png b/docs/2.24.232/mp/_images/warehouse_47_0.png new file mode 100644 index 0000000000000000000000000000000000000000..cb85edfb5904478043dcde818526b01671ef1255 GIT binary patch literal 9942 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 + + + + + + docplex.mp.basic — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + +

+ + +
+
+
+
+ +

Source code for docplex.mp.basic

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+
+from abc import abstractmethod, ABCMeta
+
+from docplex.mp.operand import Operand
+from docplex.mp.sttck import StaticTypeChecker
+
+
+# noinspection PyUnusedLocal,PyPropertyAccess
+
+class _AbstractModelObject(metaclass=ABCMeta):
+    """
+    Abstract API for all classes which have a "model" property.
+    """
+
+    @property
+    @abstractmethod
+    def model(self):  # pragma: no cover
+        raise NotImplementedError
+
+    def is_in_model(self, mdl):
+        return self.model is mdl
+
+    def get_linear_factory(self):
+        return self.model._lfactory
+
+    @property
+    def lfactory(self):
+        return self.model._lfactory
+
+    @property
+    def qfactory(self):
+        return self.model._qfactory
+
+    def _check_model_has_solution(self):
+        self.model._check_has_solution()
+
+    @property
+    def error_handler(self):
+        return self.logger
+
+    @property
+    def logger(self):
+        return self.model.error_handler
+
+    def fatal(self, msg, *args):
+        self.logger.fatal(msg, args)
+
+    def error(self, msg, *args):
+        self.logger.error(msg, args)
+
+    def warning(self, msg, *args):
+        self.logger.warning(msg, args)
+
+
+class _AbstractValuable(_AbstractModelObject):
+    # abstract API for all objects which can be evaluated from a solution.
+
+    __slots__ = ()
+
+    def _round_if_discrete(self, raw_value):
+        return self.model._round_element_value_if_necessary(self, raw_value)
+
+    @abstractmethod
+    def _raw_solution_value(self, s=None):
+        # INTERNAL: compute raw solution value, no rounding, no checking
+        raise NotImplementedError  # pragma: no cover
+
+    @property
+    def solution_value(self):
+        self.model._check_has_solution()
+        raw = self._raw_solution_value()
+        return self._round_if_discrete(raw)
+
+    @property
+    def raw_solution_value(self):
+        self.model._check_has_solution()
+        return self._raw_solution_value()
+
+    @property
+    def sv(self):
+        return self.solution_value
+
+    @property
+    def rsv(self):
+        return self.raw_solution_value
+
+
+class _SubscriptionMixin(object):
+    __slots__ = ()
+
+    # INTERNAL:
+    # This class is absolutely not meant to be directly instantiated
+    # but used as a mixin
+
+    @classmethod
+    def _new_empty_subscribers(cls):
+        return []
+
+    def notify_used(self, user):
+        # INTERNAL
+        self._subscribers.append(user)
+
+    notify_subscribed = notify_used
+
+    def notify_unsubscribed(self, subscriber):
+        # 1 find index
+        for s, sc in enumerate(self._subscribers):
+            if sc is subscriber:
+                del self._subscribers[s]
+                break
+
+    def clear_subscribers(self):
+        self._subscribers = []
+
+    def is_in_use(self):
+        return bool(self._subscribers)
+
+    @property
+    def nb_subscribers(self):
+        return len(self._subscribers)
+
+    def is_shared(self):
+        return self.nb_subscribers >= 2
+
+    def is_used_by(self, obj):
+        # lists are not optimal here, but we favor insertion: append is faster than set.add
+        return any(obj is sc for sc in self.iter_subscribers())
+
+    def notify_modified(self, event):
+        for s in self._subscribers:
+            s.notify_expr_modified(self, event)
+
+    def iter_subscribers(self):
+        return iter(self._subscribers)
+
+    def notify_replaced(self, new_expr):
+        for s in self._subscribers:
+            s.notify_expr_replaced(self, new_expr)
+
+    def grab_subscribers(self, other):
+        # grab subscribers from another expression
+        # typically when an expression is replaced by another.
+        for s in other.iter_subscribers():
+            self._subscribers.append(s)
+        # delete all subscriptions on old
+        other.clear_subscribers()
+
+
+class _AbstractBendersAnnotated(_AbstractModelObject):
+    # a maxin class to group all benders-related code.
+    __slots__ = ()
+
+    def set_benders_annotation(self, group):
+        self.model.set_benders_annotation(self, group)
+
+    def get_benders_annotation(self):
+        return self.model.get_benders_annotation(self)
+
+
+class _AbstractNamable(metaclass=ABCMeta):
+
+    # abstract name API across all modeling objects.
+
+    @property
+    @abstractmethod
+    def name(self):  # pragma: no cover
+        raise NotImplemented
+
+    @abstractmethod
+    def _set_name(self, new_name):  # pragma: no cover
+        raise NotImplementedError
+
+    def check_name(self, new_name):
+        pass
+
+    def get_name(self):
+        # deprecate
+        return self.name
+
+    def set_name(self, new_name):
+        self.check_name(new_name)
+        self._set_name(new_name)
+
+    @property
+    def safe_name(self):
+        return self.name or ''
+
+    def check_lp_name(self, qualifier, new_name, accept_empty, accept_none):
+        return StaticTypeChecker.check_lp_name(logger=self, qualifier=qualifier, obj=self, new_name=new_name,
+                                               accept_empty=accept_empty, accept_none=accept_none)
+
+    def has_name(self):
+        return self.name is not None
+
+    def has_user_name(self):
+        return self.has_name()
+
+
+
[docs]class ModelObject(_AbstractModelObject): + # base for all model objects + __array_priority__ = 100 + + __slots__ = ('_model',) + + def __init__(self, model): + self._model = model + + @property + def model(self): + return self._model + + def repr_str(self): + # INTERNAL + try: + return self.to_string(use_space=False) + except (TypeError, AttributeError): + return str(self) + + def zero_expr(self): + # INTERNAL + return self._model._lfactory.new_zero_expr() + + def _unsupported_binary_operation(self, lhs, op, rhs): + self.fatal("Unsupported operation: {0!s} {1:s} {2!s}", lhs, op, rhs) + + def __str__(self): + return self.to_string(use_space=self._model.str_use_space)
+ + # def to_string(self): + # raise NotImplementedError + + +
[docs]class ModelingObjectBase(ModelObject, _AbstractNamable): + """ModelingObjectBase() + + Parent class for all modeling objects (variables and constraints). + + This class is not intended to be instantiated directly. + """ + + __array_priority__ = 100 + + __slots__ = ('_name',) + + # noinspection PyMissingConstructor + def __init__(self, model, name=None): + self._name = name + self._model = model + + @property + def name(self): + """ This property is used to get or set the name of the modeling object. + + """ + return self._name + + @name.setter + def name(self, new_name): + self.set_name(new_name) + + def _set_name(self, name): + self._name = name + +
[docs] def has_name(self): + """ Checks whether the object has a name. + + Returns: + True if the object has a name. + + """ + return super().has_name()
+ +
[docs] def has_user_name(self): + """ Checks whether the object has a valid name given by the user. + + Returns: + True if the object has a valid name given by the user. + + """ + return self.has_name()
+ + @property + def model(self): + """ + This property returns the :class:`docplex.mp.model.Model` to which the object belongs. + """ + return super().model
+ + +
[docs]class IndexableObject(ModelingObjectBase): + __slots__ = ("_index",) + + @staticmethod + def is_valid_index(idx): + # INTERNAL: This is where the valid index check is performed + return idx >= 0 + + _invalid_index = -2 + + # noinspection PyMissingConstructor + def __init__(self, model, name=None, index=_invalid_index): + # ModelingObjectBase.__init__(self, model, name) + self._model = model + self._name = name + self._index = index + +
[docs] def is_generated(self): + """ Checks whether this object has been generated by another modeling object. + + If so, the origin object is stored in the ``_origin`` attribute. + + Returns: + True if the objects has been generated. + """ + return self.origin is not None
+ + @property + def origin(self): + return self.model.get_obj_origin(self) + + @origin.setter + def origin(self, origin): + self.model.set_obj_origin(self, origin) + + def __hash__(self): + return id(self) + + @property + def model(self): + return self._model + + @property + def index(self): + return self._index + + @property + def index1(self): + raw = self._index + return raw if raw == self._invalid_index else raw + 1 + + def _set_index(self, idx): + self._index = idx + + def has_valid_index(self): + return self._index >= 0 + + def _set_invalid_index(self): + self._index = self._invalid_index + + @property + def safe_index(self): + if not self.has_valid_index(): + self.fatal("Modeling object {0!s} has invalid index: {1:d}", self, self._index) # pragma: no cover + return self._index + + @property + def container(self): + return self.model.get_var_container(self) + + @container.setter + def container(self, ctn): + self._model.set_var_container(self, ctn) + + @property + @abstractmethod + def cplex_scope(self) -> int: + return -1 # crash + + def get_scope(self): + try: + cpx_scope = self.cplex_scope + return self.model._get_obj_scope(cpx_scope, error='ignore') + except AttributeError: + return None + + @property + def scope(self): + return self.get_scope()
+ + +
[docs]class Expr(ModelObject, Operand, _AbstractValuable): + """Expr() + + Parent class for all expression classes. + """ + __slots__ = () + + @property + def name(self): + return None + + def clone(self): # pragma: no cover + raise NotImplementedError # pragma: no cover + + def iter_variables(self): + # internal + raise NotImplementedError # pragma: no cover + + def copy(self, target_model, var_mapping): + # internal + raise NotImplementedError # pragma: no cover + +
[docs] def number_of_variables(self): + """ + Returns: + integer: The number of variables in the expression. + """ + return sum(1 for _ in self.iter_variables()) # pragma: no cover
+ +
[docs] def contains_var(self, dvar): + """ Checks whether a variable is present in the expression. + + :param: dvar (:class:`docplex.mp.dvar.Var`): A decision variable. + + Returns: + Boolean: True if the variable is present in the expression, else False. + """ + return any(dvar is v for v in self.iter_variables())
+ + def to_string(self, nb_digits=None, use_space=False): + from io import StringIO + oss = StringIO() + if nb_digits is None: + nb_digits = self.model.float_precision + self.to_stringio(oss, nb_digits=nb_digits, use_space=use_space) + return oss.getvalue() + + def to_readable_string(self): + return self.to_string(use_space=True)[:self.model.readable_str_len] + + def to_stringio(self, oss, nb_digits, use_space, var_namer=lambda v: v.name): + raise NotImplementedError # pragma: no cover + + def _num_to_stringio(self, oss, num, ndigits=None, print_sign=False, force_plus=False, use_space=False): + k = num + if print_sign: + if k < 0: + sign = u'-' + k = -k + elif k > 0 and force_plus: + # force a plus + sign = u'+' + else: + sign = None + if use_space: + oss.write(u' ') + if sign: + oss.write(sign) + if use_space: + oss.write(u' ') + # INTERNAL + ndigits = ndigits or self.model.float_precision + try: + if k == int(k): + oss.write(u'%d' % k) + else: + # use second arg as nb digits: + oss.write(u"{0:.{1}f}".format(k, ndigits)) + except ValueError: + # possibly a nan + oss.write('?') + + # def __pos__(self): + # # + e is identical to e + # return self + + def is_discrete(self): + raise NotImplementedError # pragma: no cover + +
[docs] def is_quad_expr(self): + """ Returns True if the expression is quadratic + + """ + return False
+ + def get_linear_part(self): + return self # should be not implemented... + + def is_zero(self): + return False + + constant = property(Operand.get_constant) + + @property + def float_precision(self): + return 0 if self.is_discrete() else self.model.float_precision + + def __pow__(self, power): + # INTERNAL + if 0 == power: + return 1 + elif 1 == power: + return self + elif 2 == power: + return self.square() + else: + self.model.unsupported_power_error(self, power) + + def square(self): + # redefine for each class of expression + return None # pragma: no cover + + def __gt__(self, e): + """ The strict > operator is not supported + """ + self.model.unsupported_relational_operator_error(self, ">", e) + + def __lt__(self, e): + """ The strict < operator is not supported + """ + self.model.unsupported_relational_operator_error(self, "<", e)
+ +# --- +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/callbacks/cb_mixin.html b/docs/2.24.232/mp/_modules/docplex/mp/callbacks/cb_mixin.html new file mode 100644 index 0000000..5de21bd --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/callbacks/cb_mixin.html @@ -0,0 +1,345 @@ + + + + + + + + + docplex.mp.callbacks.cb_mixin — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.callbacks.cb_mixin

+#!/usr/bin/python
+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2017
+# ---------------------------------------------------------------------------
+
+from docplex.mp.cplex_engine import CplexEngine
+
+
+
[docs]class ModelCallbackMixin(object): + """ + This mixin class is intended as a bridge between DOcplex expression and constraints + and CPLEX callback API. + It is not intended to be instantiated directly, but to be inherited from in custom callbacks + , jointly with a CPLEX callback type. + + For example, to define a custom BranchCallback in Docplex, define a new class which + inherits both from ModelCallbackMixin and the legacy callback class BranchCallback. + + Note: + - `ModelCallbackMixin` should be first in inheritance order, + - the constructor of the custom callback class must take an `env` parameter to comply + with the CPLEX API + - the constructor of the custom callback must call two __init__() methods: + - one for the cplex callback class, taking an `env` parameter + - one for the mixin class. + + Example: + + class MyBranch(ModelCallbackMixin, cplex.callbacks.BranchCallback): + + def __init__(self, env): + cplex.callbacks.BranchCallback.__init__(self, env) + ModelCallbackMixin.__init__(self) + + A custom callback must be registered with a `Model` class using Model.register_callback; this method + assumes the custom callback has a model setter property to connect the model to the callback. + + + See Also: + :func:`docplex.mp.model.Model.register_callback` + """ + def __init__(self): + self._model = None + + @property + def model(self): + """ This property is used to get the model associated with the mixin. + + An exception is raised if no model has been associated with the mixin. + + :return: an instance of `docplex.mp.Model` + """ + if not self._model: + raise ValueError('No model has been attached to the callback.') # pragma: no cover + return self._model + +
[docs] def index_to_var(self, var_index): + """ This method converts a variable index to a Var object. + + A model must have been associated withthe mixin, otherwise an error is raised. + + :param var_index: A valid variable index, that is a positive integer. + + :return: A Docplex variable with this index, or None. + """ + assert var_index >= 0 + dv = self.model.get_var_by_index(var_index) + return dv
+ +
[docs] @staticmethod + def linear_ct_to_cplex(linear_ct): + """ Converst a DOcplex linear constraint to CPLEX Python data + + :param linear_ct: a DOcplex linear constraint. + :return: a 3-tuple containing elements representing the constraint in CPLEX-Python + - a list of two lists, indices and coefficients , representing the linear part + - a floating point number , the "right hand side" or rhs + - a one-letter string (possible values are: 'L', 'E', 'G') representing the sense of the constraint. + + Example: + Assuming variable X has index 1, the constraint (2X <= 7) will be converted to + + ct = 2 * X <= 7 + linear_ct_cplex(ct) + >>> [[1], [2.0]], 7.0, 'L' + + """ + cpx_lhs = CplexEngine.linear_ct_to_cplex(linear_ct=linear_ct) + cpx_rhs = linear_ct.cplex_num_rhs() + cpx_sense = linear_ct.sense.cplex_code + return cpx_lhs, cpx_sense, cpx_rhs
+ +
[docs] def make_solution_from_vars(self, dvars): + """ Creates an intermediate solution from a list of variables. + + :param dvars: a list of DOcplex variables. + :return: a :class:`docplex.mp.solution.SolveSolution` object. + """ + if dvars: + indices = [v._index for v in dvars] + # this calls the Cplex callback method get_values, which crashes if called with empty list + # noinspection PyUnresolvedReferences + var_values = super(ModelCallbackMixin, self).get_values(indices) + # noinspection PyArgumentList + var_value_dict = {v: val for v, val in zip(dvars, var_values)} + else: # pragma: no cover + var_value_dict = {} + return self.model.new_solution(var_value_dict)
+ +
[docs] def make_complete_solution(self): + """ Creates and returns an intermediate solution with all variables. + + Values are taken from the `get_values()` method of the callback + + :return: a :class:`docplex.mp.solution.SolveSolution` object. + """ + all_vars = list(self.model.iter_variables()) + return self.make_solution_from_vars(all_vars)
+ + # compatibility + make_solution = make_complete_solution
+ + +
[docs]class ConstraintCallbackMixin(ModelCallbackMixin): + + def __init__(self): + ModelCallbackMixin.__init__(self) + self._ct_vars = None + self.cts = [] + self._vars = [] + + def register_constraints(self, cts): + self.cts.extend(cts) + self._ct_vars = None + + def register_constraint(self, ct): + self.register_constraints([ct]) + +
[docs] def register_watched_var(self, dvar): + """ Register one variable. + + Registered variables will be part of the intermediate solutions. + + """ + self._vars.append(dvar)
+ +
[docs] def register_watched_vars(self, dvars): + """ Register an iterable of variables. + + Registered variables will be part of the intermediate solutions. + + """ + self._vars.extend(dvars)
+ + @staticmethod + def _collect_constraint_variables(cts): + # collect variables as a set + var_set = set(v for c in cts for v in c.iter_variables()) + # convert to list + var_list = list(var_set) + var_list.sort(key=lambda dv: dv._index) + return var_list + + def _get_or_collect_vars(self): + # INTERNAL + if self._ct_vars is None: + self._ct_vars = self._collect_constraint_variables(self.cts) + return self._ct_vars + + @property + def watched_vars(self): + return self._get_or_collect_vars() + self._vars + +
[docs] def make_solution_from_watched(self): + """ Creates and returns a DOcplex solution instance from watched items. + + This method should be called when CPLEX has a new incumbent solution. + It builds an intermediate solution from the watched variables and + variables mentioned in the registered constraints.. + + To build a soluton from all variables, use `make_complete_solution()` + + :return: + An instance of SolveSolution. + """ + return self.make_solution_from_vars(self.watched_vars)
+ +
[docs] def get_cpx_unsatisfied_cts(self, cts, sol, tolerance=1e-6): + """ returns the subset of unsatisfied constraints in a given solution. + This is used in custom lazy constraints or user cut callbacks. + + :param cts: a list of constraints among which to look for unsatisfied + :param sol: A solution object + :param tolerance: amn optional numerical value used to determine + whether a constraint is satisfied or not. Defaut is 1e-6. + + :return: a list of tuples (ct, lhs, sense, lhs) where: + ct is an unsatisfied constraint + lhs is the left-hand size, as expected by the cplex callback + sense is the constraint sense, as expected by the cplex callback + rhs is the rith-and side (a number), as expected by the cplex callback + + """ + unsatisfied = [] + for ct in cts: + if not ct.is_satisfied(sol, tolerance): + # use mixin API to convert to cplex lingo + cpx_lhs, cpx_sense, cpx_rhs = self.linear_ct_to_cplex(ct) + unsatisfied.append( (ct, cpx_lhs, cpx_sense, cpx_rhs) ) + return unsatisfied
+ + + + +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/conflict_refiner.html b/docs/2.24.232/mp/_modules/docplex/mp/conflict_refiner.html new file mode 100644 index 0000000..6adab23 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/conflict_refiner.html @@ -0,0 +1,525 @@ + + + + + + + + + docplex.mp.conflict_refiner — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.conflict_refiner

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+import warnings
+from collections import namedtuple, defaultdict
+
+from docplex.mp.utils import is_string
+from docplex.mp.constants import ComparisonType, VarBoundType
+from docplex.mp.utils import str_maxed
+from docplex.mp.publish import PublishResultAsDf
+
+
+TConflictConstraint = namedtuple("_TConflictConstraint", ["name", "element", "status"])
+
+
+def trim_field(element):
+    return str_maxed(element, maxlen=50)
+
+
+def to_output_table(conflicts, use_df=True):
+    # Returns the output tables, as df if pandas is available or as a list
+    # of named tuple ['Type', 'Status', 'Name', 'Expression']
+    columns = ['Type', 'Status', 'Name', 'Expression']
+    TOutputTables = namedtuple('TOutputTables', columns)
+
+    def convert_to_pandas_df(c):
+        return {'Type': 'Constraint',
+                'Status': c.status.name if c.status is not None else '',
+                'Name': c.name or '',
+                'Expression': trim_field(c.element)}
+
+    def convert_to_namedtuples(c):
+        return TOutputTables('Constraint',
+                             c.status,
+                             c.name or '',
+                             trim_field(c.element))
+
+    pandas = None
+    if use_df:
+        try:
+            import pandas
+        except ImportError:  # pragma: no cover
+            print("pandas module not found...")
+            pandas = None
+
+    data_converter = convert_to_pandas_df if pandas and use_df else convert_to_namedtuples
+    output_data = list(map(data_converter, conflicts))
+
+    if use_df:
+        return pandas.DataFrame(columns=columns, data=output_data)
+    else:
+        return output_data
+
+
+
[docs]class ConflictRefinerResult(object): + """ This class contains all conflicts as returned by the conflict refiner. + + A conflict refiner result contains a list of named tuples of type ``TConflictConstraint``, + the fields of which are: + + - an enumerated value of type ``docplex.mp.constants.ConflictStatus`` that indicates the + conflict status type (Excluded, Possible_member, Member...). + - the name of the constraint or None if the constraint corresponds to a variable lower or upper bound. + - a modeling object involved in the conflict: + can be either a constraint or a wrapper representing a variable upper or lower bound. + + + *New in version 2.11* + """ + + def __init__(self, conflicts, refined_by=None): + self._conflicts = conflicts + assert refined_by is None or is_string(refined_by) + self._refined_by = refined_by + + @property + def refined_by(self): + ''' + Returns a string indicating how the conflicts were produced. + + - If the conflicts are created by a program, this field returns None. + - If the conflicts originated from a local CPLEX run, this method returns 'cplex_local'. + - If the conflicts originated from a DOcplexcloud run, this method returns 'cplex_cloud'. + + Returns: + A string, or None. + + ''' + return self._refined_by + + def __iter__(self): + return self.iter_conflicts() + + def __len__(self): + """ Redefintion of maguic method __len__. + + Allows calling len() on an instance of ConflictRefinerResult + to get the number of conflicts + + :return: the number of conflicts. + """ + return len(self._conflicts) + +
[docs] def iter_conflicts(self): + """ Returns an iterator on conflicts (named tuples) + + :return: an iterator + """ + return iter(self._conflicts)
+ + @property + def number_of_conflicts(self): + """ This property returns the number of conflicts. """ + return len(self._conflicts) + +
[docs] def display(self): + """ Displays all conflicts. + + """ + print('conflict(s): {0}'.format(self.number_of_conflicts)) + for conflict in self.iter_conflicts(): + st = conflict.status + elt = conflict.element + if hasattr(conflict.element, 'as_constraint'): + ct = conflict.element.as_constraint() + label = elt.short_typename + else: + ct = elt + label = ct.__class__.__name__ + print(" - status: {1}, {0}: {2!s}".format(label, st.name, ct.to_readable_string()))
+ +
[docs] def display_stats(self): + """ Displays statistics on conflicts. + + Display show many conflict elements per type. + """ + def elt_typename(elt): + try: + return elt.short_typename.lower() + except AttributeError: # pragma: no cover + return elt.__class__.__name__.lower() + + ncf = self.number_of_conflicts + print('conflict{1}: {0}'.format(ncf, "s" if ncf > 1 else "")) + cf_stats = defaultdict(lambda: 0) + for conflict in self.iter_conflicts(): + elt_type = elt_typename(conflict.element) + cf_stats[elt_type] += 1 + for eltt, count in cf_stats.items(): + if count: + print(" - {0}{2}: {1}".format(eltt, count, ("s" if count > 1 else "")))
+ + def as_output_table(self, use_df=True): + return to_output_table(self, use_df) + +
[docs] def print_information(self): + """ Similar as `display_stats` + """ + self.display_stats()
+ + +class VarBoundWrapper(object): + # INTERNAL + + def __init__(self, dvar): + self._var = dvar + + @property + def var(self): + return self._var + + @property + def index(self): + return self._var.index + + @property + def short_typename(self): # pragma: no cover + return "Variable Bound" + + def as_constraint(self): # pragma: no cover + raise NotImplementedError + + def as_constraint_from_symbol(self, op_symbol): + self_var = self.var + var_lb = self.var.lb + op = ComparisonType.cplex_ctsense_to_python_op(op_symbol) + ct = op(self_var, var_lb) + return ct + + @classmethod + def make_wrapper(cls, var, bound_type): + if bound_type == VarBoundType.LB: + return VarLbConstraintWrapper(var) + elif bound_type == VarBoundType.UB: + return VarUbConstraintWrapper(var) + else: + return None + + +
[docs]class VarLbConstraintWrapper(VarBoundWrapper): + """ + This class is a wrapper for a model variable and its associated lower bound. + + Instances of this class are created by the ``refine_conflict`` method when the conflict involves + a variable lower bound. Each of these instances is then referenced by a ``TConflictConstraint`` namedtuple + in the conflict list returned by ``refine_conflict``. + + To check whether the lower bound of a variable causes a conflict, wrap the variable and + include the resulting constraint in a ConstraintsGroup. + """ + @property + def short_typename(self): + return "Lower Bound" + + def as_constraint(self): + return self.as_constraint_from_symbol('G')
+ + +
[docs]class VarUbConstraintWrapper(VarBoundWrapper): + """ + This class is a wrapper for a model variable and its associated upper bound. + + Instances of this class are created by the ``refine_conflict`` method when the conflict involves + a variable upper bound. Each of these instances is then referenced by a ``TConflictConstraint`` namedtuple + in the conflict list returned by ``refine_conflict``. + + To check whether the upper bound of a variable causes a conflict, wrap the variable and + include the resulting constraint in a ConstraintsGroup. + """ + + @property + def short_typename(self): + return "Upper Bound" + + def as_constraint(self): + return self.as_constraint_from_symbol('L')
+ + +
[docs]class ConstraintsGroup(object): + """ + This class is a container for the definition of a group of constraints. + A preference for conflict refinement is associated to the group. + + Groups may be assigned preference. A group with a higher preference is more likely to be included in the conflict. + A negative value specifies that the corresponding group should not be considered in the computation + of a conflict. In other words, such groups are not considered part of the model. Groups with a preference of 0 (zero) + are always considered to be part of the conflict. + + Args: + preference: A floating-point number that specifies the preference for the group. The higher the number, the + higher the preference. + """ + + __slots__ = ('_preference', '_cts') + + def __init__(self, preference=1.0, cts=None): + self._preference = preference + self._cts = [] + if cts is not None: + self.add_constraints(cts) + +
[docs] @classmethod + def from_var(cls, dvar, bound_type, pref): + """ A class method to build a group fromone variable. + + :param dvar: The variable whose bound is part of the conflict. + :param bound_type: An enumerated value of type `VarBoundType` + :param pref: a numerical preference. + + :return: an instance of ConstraintsGroup. + + See Also: + :class:`docplex.mp.constants.VarBoundType` + """ + cgg = cls(preference=pref, cts=VarBoundWrapper.make_wrapper(dvar, bound_type)) + return cgg
+ + @property + def preference(self): + return self._preference + + def add_one(self, x): + if x is not None: + self._cts.append(x) + + def add_constraint(self, ct): + self._cts.append(ct) + + def add_constraints(self, cts): + try: + for ct in cts: + self.add_one(ct) + except TypeError: # not iterable. + self.add_one(cts) + + def iter_constraints(self): + return iter(self._cts)
+ + +
[docs]class ConflictRefiner(PublishResultAsDf, object): + ''' This class is an abstract algorithm; it operates on interfaces. + + A conflict is a set of mutually contradictory constraints and bounds within a model. + Given an infeasible model, the conflict refiner can identify conflicting constraints and bounds + within it. CPLEX refines an infeasible model by examining elements that can be removed from the + conflict to arrive at a minimal conflict. + ''' + + # static variables for output + output_table_property_name = 'conflicts_output' + default_output_table_name = 'conflicts.csv' + output_table_using_df = True + + def __init__(self, output_processing=None): + self.output_table_customizer = output_processing + + @classmethod + def _make_atomic_ct_groups(cls, mdl_iter, pref): + # returns a list of singleton groups from a model iterator and a numerical preference. + lcgrps = [ConstraintsGroup(pref, ct) for ct in mdl_iter] + return lcgrps + +
[docs] @classmethod + def var_bounds(cls, mdl, pref=4.0, include_infinity_bounds=True): + """ Returns a list of singleton groups with variable bounds. + + This method a list of ConstraintGroup objects, each of which contains a variabel bound. + It replicate sthe behavior of the CPLEX interactive optimizer, that is, it returns + + - lower bounds for non-binary variables if different from 0 + - upper bound for non-binary-variables if non-default + + For binary variables, bounds are not considered, unless the variable is bound; more precisely: + - lower bound is included if >= 0.5 + - upper bound is included if <= 0.5 + + :param mdl: The model being analyzed for conflicts, + :param pref: the preference for variable bounds, the defaut is 4.0 + :param include_infinity_bounds: a flag indicating whether infi + + :return: a list of `ConstraintsGroup` objects. + """ + grps = [] + mdl_inf = mdl.infinity + for dv in mdl.iter_variables(): + lb, ub = dv.lb, dv.ub + if not dv.is_binary(): + if lb != 0: + if include_infinity_bounds or lb > - mdl_inf: + grps.append(ConstraintsGroup.from_var(dv, VarBoundType.LB, pref)) + if include_infinity_bounds or ub < mdl_inf: + grps.append(ConstraintsGroup.from_var(dv, VarBoundType.UB, pref)) + else: + if lb >= 0.5: + grps.append(ConstraintsGroup.from_var(dv, VarBoundType.LB, pref)) + if ub <= 0.5: + grps.append(ConstraintsGroup.from_var(dv, VarBoundType.UB, pref)) + return grps
+ + @classmethod + def linear_constraints(cls, mdl, pref=2.0): + return cls._make_atomic_ct_groups(mdl.iter_linear_constraints(), pref) + + @classmethod + def logical_constraints(cls, mdl, pref=1.0): + return cls._make_atomic_ct_groups(mdl.iter_logical_constraints(), pref) + + @classmethod + def quadratic_constraints(cls, mdl, pref=1.0): + return cls._make_atomic_ct_groups(mdl.iter_quadratic_constraints(), pref) + +
[docs] def refine_conflict(self, mdl, preferences=None, groups=None, display=False, **kwargs): + """ Starts the conflict refiner on the model. + + Args: + mdl: The model to be relaxed. + preferences: A dictionary defining constraint preferences. + groups: A list of ConstraintsGroups. + display: a boolean flag (default is True); if True, displays the result at the end. + kwargs: Accepts named arguments similar to solve. + + Returns: + An object of type `ConflictRefinerResut` which holds all information about + the minimal conflict. + + See Also: + :class:`ConflictRefinerResult` + + """ + + if mdl.has_multi_objective(): + mdl.fatal("Conflict refiner is not supported for multi-objective") + + # take into account local argument overrides + context = mdl.prepare_actual_context(**kwargs) + + # log stuff + saved_context_log_output = mdl.context.solver.log_output + saved_log_output_stream = mdl.log_output + + try: + mdl.set_log_output(context.solver.log_output) + if mdl.environment.has_cplex: + results = self._refine_conflict_local(mdl, context, preferences, groups) + else: + return mdl.fatal("CPLEX runtime not found: cannot run conflict refiner") + + # write conflicts table.write_output_table() handles everything related to + # whether the table should be published etc... + if self.is_publishing_output_table(mdl.context): + self.write_output_table(results.as_output_table(self.output_table_using_df), mdl.context) + if display: + results.display_stats() + return results + finally: + if saved_log_output_stream != mdl.log_output: + mdl.set_log_output_as_stream(saved_log_output_stream) + if saved_context_log_output != mdl.context.solver.log_output: + mdl.context.solver.log_output = saved_context_log_output
+ + # noinspection PyMethodMayBeStatic + def _refine_conflict_local(self, mdl, context, preferences=None, groups=None): + parameters = context.cplex_parameters + self_engine = mdl.get_engine() + return self_engine.refine_conflict(mdl, preferences, groups, parameters) + +
[docs] @staticmethod + def display_conflicts(conflicts): + """ + This method displays a formatted representation of the conflicts that are provided. + + Args: + conflicts: An instance of ``ConflictRefinerResult`` + """ + warnings.warn("deprecated: use ConflictRefinerresult.display", DeprecationWarning) + conflicts.display()
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/constants.html b/docs/2.24.232/mp/_modules/docplex/mp/constants.html new file mode 100644 index 0000000..d8d1b35 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/constants.html @@ -0,0 +1,745 @@ + + + + + + + + + docplex.mp.constants — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.constants

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+
+import operator
+from enum import Enum
+
+from docplex.mp.error_handler import docplex_fatal
+from docplex.mp.utils import is_string, is_int
+
+
+
[docs]class VarBoundType(Enum): + """This enumerated class describes the two types of variable bounds: + - LB is for lower bound + - UB is for uupper bound + + This enumerated type is used in conflict refiner. + """ + LB = 0 + UB = 1
+ + +
[docs]class ComparisonType(Enum): + """This enumerated class defines the various types of linear constraints: + + - LE for e1 <= e2 constraints + + - EQ for e1 == e2 constraints + + - GE for e1 >= e2 constraints + + where e1 and e2 denote linear expressions. + """ + LE = 1, '<=', 'L', operator.le + EQ = 2, '==', 'E', operator.eq + GE = 3, '>=', 'G', operator.ge + + def __new__(cls, code, operator_symbol, cplex_kw, python_op): + obj = object.__new__(cls) + # predefined + obj._value_ = code + obj._cplex_code = cplex_kw + obj._op_symbol = operator_symbol + obj._pyop = python_op + return obj + + # NOTE: Never add a static field in an enum class: it would be interpreted as an other enum + + @property + def short_name(self): + return self.name + + @property + def cplex_code(self): + return self._cplex_code + + @property + def operator_symbol(self): + """ Returns a string operator for the constraint. + + Example: + Returns string "<=" for a e1 <= e2 constraint. + + Returns: + string: A string describing the logical operator used in the constraint. + """ + return self._op_symbol + + @property + def python_operator(self): + return self._pyop + + @classmethod + def parse(cls, arg, do_raise=True): + # INTERNAL + # noinspection PyTypeChecker + for op in cls: + if arg in (op, op.value): + return op + elif is_string(arg): + if arg == op._cplex_code \ + or arg == str(op.value) \ + or arg.lower() == op.name.lower(): + return op + # not found + if do_raise: + docplex_fatal('cannot convert this to a comparison type: {0!r}'.format(arg)) + else: + return None + + @classmethod + def cplex_ctsense_to_python_op(cls, cpx_sense): + return cls.parse(cpx_sense).python_operator + + @classmethod + def almost_compare(cls, lval, op, rval, eps): + if op is cls.LE: + # lval <= rval with eps tolerance means lval-rval <= e + return lval - rval <= eps + elif op is cls.GE: + # lval >= rval with eps tolerance means lval-rval >= -eps + return lval - rval >= -eps + elif op is cls.EQ: + return abs(lval - rval) <= eps + else: + raise TypeError + + @classmethod + def almost_equal(cls, lval, rval, eps): + return cls.almost_compare(lval, cls.EQ, rval, eps)
+ + +
[docs]class RelaxationMode(Enum): + """ This enumerated type describes the different strategies for model relaxation: + - MinSum, OptSum, + - MinInf, OptInf, + - MinQuad, OptQuad. + + A relaxation algorithms works in two phases: In the first phase, it finds a + feasible solution while making minimal changes to the model (according to a metric). + In the second phase, it + searches for an optimal solution while keeping the relaxation at the minimal value found in phase 1. + + Enumerated values work in pairs: MinXXX, OptXXX + + - MinXXX values stop at phase 1, they look for a feasible soluion, they do not + optimize the objective. + - OptXXX run the two phases, looking for an optimal relaxed solution. They take longer. + + The metric used to evaluate the quality of the relaxation is determined by the XXX part of + the name. There are three metrics: + + - Inf (MinInf, OptInf) minimizes the number of relaxed constraints. This metric + will prefer to relax one constraint, even with a huge slack, instead of two. + - Sum (MinSum, OptSum): minimizes the sum of relaxations. + - Quand (MinQuad, OptQuad): minimizes the sum of squares of relaxations. + This metric is the most expensive in computation time, + but avoids huge discrepancies between relaxations: + two constraints with relaxations of 2,2 will have a better quality (2^2 + 2^2 = 8) + than relaxations of 3,1 (3^2 +1 = 10). + """ + + MinSum, OptSum, MinInf, OptInf, MinQuad, OptQuad = range(6) + + @staticmethod + def parse(arg): + # INTERNAL + # noinspection PyTypeChecker + for m in RelaxationMode: + if arg == m or arg == m.value: + return m + elif is_string(arg): + if arg == str(m.value) or arg.lower() == m.name.lower(): + return m + + docplex_fatal('cannot parse this as a relaxation mode: {0!r}'.format(arg)) + + @staticmethod + def get_no_optimization_mode(mode): + assert isinstance(mode, RelaxationMode) + # even values are MinXXX modes + relax_code = mode.value + if 0 == relax_code % 2: + return mode + else: + # OptXXX is 2k+1 when MinXXX is 2k + return RelaxationMode(relax_code - 1) + + def __repr__(self): + return 'docplex.mp.RelaxationMode.{0}'.format(self.name)
+ + +
[docs]class ConflictStatus(Enum): + """ + This enumerated class defines the conflict status types. + """ + Excluded, Possible_member, Possible_member_lower_bound, Possible_member_upper_bound, \ + Member, Member_lower_bound, Member_upper_bound = -1, 0, 1, 2, 3, 4, 5
+ + +
[docs]class SOSType(Enum): + """This enumerated class defines the SOS types: + + - SOS1 for SOS type 1 + + - SOS1 for SOS type 2. + """ + SOS1, SOS2 = 1, 2 + + def lower(self): + return self.name.lower() + + @staticmethod + def parse(arg, + sos1_tokens=frozenset(['1', 'sos1']), + sos2_tokens=frozenset(['2', 'sos2'])): + if isinstance(arg, SOSType): + return arg + elif 1 == arg: + return SOSType.SOS1 + elif 2 == arg: + return SOSType.SOS2 + elif is_string(arg): + arg_lower = arg.lower() + if arg_lower in sos1_tokens: + return SOSType.SOS1 + elif arg_lower in sos2_tokens: + return SOSType.SOS2 + + docplex_fatal("Cannot convert to SOS type: {0!s} - expecting 1|2|'sos1'|'sos2'", arg) + + def _cpx_sos_type(self): + # INTERNAL + return str(self.value) + + @property + def size(self): + return self.value + + def __repr__(self): + return 'docplex.mp.SOSType.{0}'.format(self.name)
+ + +
[docs]class SolveAttribute(Enum): + duals = 1, False, True + slacks = 2, False, True + reduced_costs = 3, True, True + + def __new__(cls, code, is_for_vars, requires_solve): + obj = object.__new__(cls) + # predefined + obj._value_ = code + obj.is_var_attribute = is_for_vars + obj.requires_solve = requires_solve + return obj + + @classmethod + def parse(cls, arg, do_raise=True): + # INTERNAL + # noinspection PyTypeChecker + for m in cls: + if arg == m or arg == m.value: + return m + elif is_string(arg): + if arg == str(m.value) or arg.lower() == m.name.lower(): + return m + + if do_raise: + docplex_fatal('cannot convert this to a solve attribute: {0!r}'.format(arg)) + else: + return None
+ + +
[docs]class UpdateEvent(Enum): + # INTERNAL + NoOp = 0 + # + # Linear constraint events + LinearConstraintCoef = 1 + LinearConstraintRhs = 2 + LinearConstraintGlobal = 3 # logical and of Coef + Rhs + ConstraintSense = 4 + + # Range constraint events + RangeConstraintBounds = 5 + RangeConstraintExpr = 6 + + # Expression events + ExprConstant = 8 + LinExprCoef = 16 + LinExprGlobal = 24 + LinExprPromotedToQuad = 25 # objective is ok, constraint wont support this. + + # Quad + QuadExprQuadCoef = 32 + QuadExprGlobal = 64 + + # Ind + IndicatorLinearConstraint = 128 + + # Quadct + QuadraticConstraintGlobal = 256 + + def __bool__(self): + return bool(self.value)
+ + +
[docs]class ObjectiveSense(Enum): + """ + This enumerated class defines the two types of objectives, `Minimize` and `Maximize`. + """ + Minimize, Maximize = 1, 2 + +
[docs] def is_minimize(self): + """ Returns True if objective is a minimizing objective. + """ + return self is ObjectiveSense.Minimize
+ +
[docs] def is_maximize(self): + """ Returns True if objective is a maximizing objective. + """ + return self is ObjectiveSense.Maximize
+ + @property + def verb(self): + """ Returns a string describing the objective (in lowercase) + + - 'minimize' for the Minimize objective + - 'maximize' for the Maximize objective + + """ + return self.name.lower() + + @property + def short_name(self): + """ Returns a short ('min' or 'max') string describing the objective. + """ + return self.verb[:3] + + @property + def cplex_coef(self): + return 1 if self.is_minimize() else -1 + + @classmethod + def from_cplex(cls, cpx_sense): + if cpx_sense == 1: + return cls.Minimize + elif cpx_sense == -1: + return cls.Maximize + else: + raise ValueError("expecting +1 or -1, {0} was passed".format(cpx_sense)) + + @staticmethod + def parse(arg, logger=None): + """ Converts an argument to an objective sense. + + Accepts either + - an objective sense (returns itself) + - a string (in long or short format), + - an integer (in CPLEX convention, 1 is for minimize, -1 for maximize). + + :param arg: the argument to convert to a sense + :param logger: + + :return: An instance of enumerated `ObjectiveSense` class. + """ + if isinstance(arg, ObjectiveSense): + return arg + + elif is_string(arg): + lower_text = arg.lower() + if lower_text in {"minimize", "min"}: + return ObjectiveSense.Minimize + elif lower_text in {"maximize", "max"}: + return ObjectiveSense.Maximize + elif logger: + logger.fatal("Not an objective sense: '{0}', expecting 'min|max'", arg) + else: + docplex_fatal("Not an objective sense: '{0}', expecting 'min|max'".format(arg)) + elif is_int(arg): + if arg == 1: + return ObjectiveSense.Minimize + elif -1 == arg: + return ObjectiveSense.Maximize + else: + logger.fatal("Not an objective sense: <{}>", (arg,)) + + if logger: + logger.fatal("Not an objective sense: <{}>", (arg,)) + else: + docplex_fatal("Not an objective sense: <{}>".format(arg))
+ + +# noinspection PyPep8 +
[docs]class CplexScope(Enum): + def __new__(cls, code, prefix, descr): + obj = object.__new__(cls) + # predefined + obj._value_ = code + obj.prefix = prefix + obj.descr = descr + return obj + + # INTERNAL + VAR_SCOPE = 0, 'x', 'variables' + LINEAR_CT_SCOPE = 1, 'c', 'linear constraints' + IND_CT_SCOPE = 2, 'ic', 'indicator constraints' + QUAD_CT_SCOPE = 3, 'qc', 'quadratic constraints' + PWL_CT_SCOPE = 4, 'pwl', 'piecewise constraints' + SOS_SCOPE = 5, 'sos', 'SOS' + + def is_constraint_scope(self): + return self._value_ in frozenset([1, 2, 3, 4])
+ + +
[docs]class QualityMetric(Enum): + def __new__(cls, code, has_int, cpx_codename): + obj = object.__new__(cls) + # predefined + obj._value_ = code + obj.has_int = has_int + obj.codename = cpx_codename + return obj + + @property + def cpx_codename(self): + return 'CPX_' + self.codename + + @property + def code(self): + return self._value_ + + @property + def key(self): + return self.codename.lower() + + @property + def int_key(self): + return '%s.int' % self.codename.lower() + + max_primal_infeasibility = 1, 1, 'MAX_PRIMAL_INFEAS' + max_scaled_primal_infeasibility = 2, 1, 'MAX_SCALED_PRIMAL_INFEAS' + sum_primal_infeasibilities = 3, 0, 'SUM_PRIMAL_INFEAS' + sum_scaled_primal_infeasibilities = 4, 0, 'SUM_SCALED_PRIMAL_INFEAS' + + max_dual_infeasibility = 5, 1, 'MAX_DUAL_INFEAS' + max_scaled_dual_infeasibility = 6, 1, 'MAX_SCALED_DUAL_INFEAS' + sum_dual_infeasibilities = 7, 0, 'SUM_DUAL_INFEAS' + sum_scaled_dual_infeasibilities = 8, 0, 'SUM_SCALED_DUAL_INFEAS' + + max_int_infeasibility = 9, 1, 'MAX_INT_INFEAS' + sum_integer_infeasibilities = 10, 0, 'SUM_INT_INFEAS' + + max_primal_residual = 11, 1, 'MAX_PRIMAL_RESIDUAL' + max_scaled_primal_residual = 12, 1, 'MAX_SCALED_PRIMAL_RESIDUAL' + sum_primal_residual = 13, 0, 'SUM_PRIMAL_RESIDUAL' + sum_scaled_primal_residual = 14, 0, 'SUM_SCALED_PRIMAL_RESIDUAL' + + max_dual_residual = 15, 1, 'MAX_DUAL_RESIDUAL' + max_scaled_dual_residual = 16, 1, 'MAX_SCALED_DUAL_RESIDUAL' + sum_dual_residual = 17, 0, 'SUM_DUAL_RESIDUAL' + sum_scaled_dual_residual = 18, 0, 'SUM_SCALED_DUAL_RESIDUAL' + + max_comp_slack = 19, 1, 'MAX_COMP_SLACK' # gap here + sum_comp_slack = 21, 0, 'SUM_COMP_SLACK' # gap here + + max_x = 23, 1, 'MAX_X' + max_scaled_x = 24, 1, 'MAX_SCALED_X' + max_pi = 25, 1, 'MAX_PI' + max_scaled_pi = 26, 1, 'MAX_SCALED_PI' + max_slack = 27, 1, 'MAX_SLACK' + max_scaled_slack = 28, 1, 'MAX_SCALED_SLACK' + + max_reduced_cost = 29, 1, 'MAX_RED_COST' + max_scaled_reduced_cost = 30, 1, 'MAX_SCALED_RED_COST' + + sum_x = 31, 0, 'SUM_X' + sum_scaled_x = 32, 0, 'SUM_SCALED_X' + sum_pi = 33, 0, 'SUM_PI' + sum_scaled_pi = 34, 0, 'SUM_SCALED_PI' + sum_slack = 35, 0, 'SUM_SLACK' + sum_scaled_slack = 36, 0, 'SUM_SCALED_SLACK' + + sum_reduced_cost = 37, 0, 'SUM_RED_COST' + sum_scaled_reduced_cost = 38, 0, 'SUM_SCALED_RED_COST' + + kappa = 39, 0, 'KAPPA' + objective_gap = 40, 0, 'OBJ_GAP' + dual_objective = 41, 0, 'DUAL_OBJ' + primal_objective = 42, 0, 'PRIMAL_OBJ' + + max_quadratic_primal_residual = 43, 1, 'MAX_QCPRIMAL_RESIDUAL' + sum_quadratic_primal_residual = 44, 0, 'SUM_QCPRIMAL_RESIDUAL' + max_quadratic_slack_infeasibility = 45, 1, 'MAX_QCSLACK_INFEAS' + sum_quadratic_slack_infeasibility = 46, 0, 'SUM_QCSLACK_INFEAS' + max_quadratic_slack = 47, 1, 'MAX_QCSLACK' + sum_quadratic_slack = 48, 0, 'SUM_QCSLACK' + max_indicator_slack_infeasibility = 49, 1, 'MAX_INDSLACK_INFEAS' + sum_indicator_slack_infeasibility = 50, 0, 'SUM_INDSLACK_INFEAS' + + exact_kappa = 51, 0, 'EXACT_KAPPA' + kappa_stable = 52, 0, 'KAPPA_STABLE' + kappa_suspicious = 53, 0, 'KAPPA_SUSPICIOUS' + kappa_unstable = 54, 0, 'KAPPA_UNSTABLE' + kappa_illposed = 55, 0, 'KAPPA_ILLPOSED' + kappa_max = 56, 0, 'KAPPA_MAX' + kappa_attention = 57, 0, 'KAPPA_ATTENTION' + + @classmethod + def parse(cls, txt, raise_on_error=True): + for qm in cls: + if txt == qm.name: + return qm + elif txt == qm.value: + return qm + elif txt == qm.cpx_codename: + return qm + + fmt = '* cannot interpret this as a QualityMetric enum: {0!r}' + if raise_on_error: + docplex_fatal(fmt, txt) + else: + print(fmt.format(txt)) + return None
+ + +
[docs]class BasisStatus(Enum): + """ This enumerated type describes the different values for basis status. + + Basis status can be queried for variables and linear constraints in LP problems. + + Possible values are: + + - NotABasisStatus: invalid or unknown status, + - Basic, means the variable belongs to the base, + - AtLower, means the variable is non-basic, at its lower bound, + - AtUpper, means the variable is non-basic, at its upper bound, + - FreeNonBasic, means the variable is nonbasic and is not at a bound. + + See Also: + The list of possible values for basis status can be found in the CPLEX documentation: + + https://www.ibm.com/support/knowledgecenter/SSSA5P_20.1.0/ilog.odms.cplex.help/refcallablelibrary/cpxapi/getbase.html + + """ + + def __new__(cls, code, cpx_codename): + obj = object.__new__(cls) + # predefined + obj._value_ = code + obj.codename = cpx_codename + return obj + + NotABasisStatus = -1, "NotBasisStatus" + AtLowerBound = 0, "CPX_AT_LOWER" + Basic = 1, "CPX_BASIC" + AtUpperBound = 2, "CPX_AT_UPPER" + FreeNonBasic = 3, "CPX_FREE_SUPER" + + @classmethod + def parse(cls, code): + for bs in cls: + if bs.value == code: + return bs + + return cls.NotABasisStatus
+ + +
[docs]class WriteLevel(Enum): + """ + This enumerated class controls what is written in MST mip start files. + The numeric value is identical to the CPLEX WriteLevel parameter values. + + The possible values are (in order of decreasing quantity of information written). + + - AllVars (1): all variables are written + - DiscreteVars (2): all discrete variables are written (binary, integer, semi-integer) + - NonZeroVars (3): all non-zero vars are written, regardless of their type. + - DiscreteNonZeroVars (4): all discrete non-zero vars are written. + - Auto (0): automatic value, same as DiscreteVars. + + *New in version 2.10* + """ + + def __new__(cls, code, short_name): + obj = object.__new__(cls) + # predefined + obj._value_ = code + obj.short_name = short_name + return obj + + Auto = 0, "auto" # same as DiscreteVars: filter discrete, keep zeros + AllVars = 1, "all" # write all variables and their value, zero or nonzero + DiscreteVars = 2, "discrete" # write all discrete variables and their value + NonZeroVars = 3, "nonzero" # write only nonzero variables + NonZeroDiscreteVars = 4, "nonzero_discrete" # write nonzero discrete variables + + def filter_zeros(self): + return self in {WriteLevel.NonZeroVars, WriteLevel.NonZeroDiscreteVars} + + def filter_nondiscrete(self): + return self in {WriteLevel.Auto, WriteLevel.DiscreteVars, WriteLevel.NonZeroDiscreteVars} + + @classmethod + def parse(cls, level): + if level is None: + return cls.Auto + elif isinstance(level, cls): + return level + else: + for wl in cls: + if wl.value == level: + return wl + elif is_string(level): + llevel = level.lower() + if llevel == wl.name.lower() or llevel == wl.short_name: + return wl + return cls.Auto
+ + +
[docs]class EffortLevel(Enum): + """ + This enumerated class controls the effort level used for a MIP start. + The numeric value is identical to the CPLEX EffortLevel parameter values. + + See Also: + The list of possible values for effort level status can be found in the CPLEX documentation: + +https://www.ibm.com/support/knowledgecenter/SSSA5P_20.1.0/ilog.odms.cplex.help/refcppcplex/html/enumerations/IloCplex_MIPStartEffort.html + + + """ + Auto = 0 + CheckFeas = 1 + SolveFixed = 2 + SolveMIP = 3 + Repair = 4 + NoCheck = 5 + + @classmethod + def parse(cls, arg): + fallback = cls.Auto + if arg is None: + return fallback + elif isinstance(arg, EffortLevel): + return arg + else: + for eff in EffortLevel: + if eff.value == arg: + return eff + elif is_string(arg) and arg.lower() == eff.name.lower(): + return eff + + return fallback
+ + +# problem type conversion +_problemtype_map = {0: "LP", + 1: "MILP", + 3: "fixed_MILP", + 4: "nodeLP", + 5: "QP", + 7: "MIQP", + 8: "fixed_MIQP", + 9: "node_QP", + 10: "QCP", + 11: "MIQCP", + 12: "node_QCP"} + + +def int_probtype_to_string(probtype, fallback_probtype="unknown"): + try: + iprobe_type = int(probtype) + return _problemtype_map.get(iprobe_type, fallback_probtype) + except ValueError: + return fallback_probtype +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/constr.html b/docs/2.24.232/mp/_modules/docplex/mp/constr.html new file mode 100644 index 0000000..4da982a --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/constr.html @@ -0,0 +1,1873 @@ + + + + + + + + + docplex.mp.constr — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.constr

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+import warnings
+
+from docplex.mp.basic import IndexableObject, ModelingObjectBase, _AbstractBendersAnnotated
+from docplex.mp.dvar import is_var
+from docplex.mp.priority import Priority
+from docplex.mp.constants import ComparisonType, UpdateEvent
+from docplex.mp.operand import LinearOperand
+from docplex.mp.sttck import StaticTypeChecker
+from docplex.mp.utils import DocplexLinearRelaxationError, str_maxed
+
+
+class _ExtraConstraintUsage(object):
+    # INTERNAL
+    def __init__(self, tag):
+        self.tag = tag
+
+    def __str__(self):
+        return "UsedAs<%s>" % self.tag
+
+    def notify_expr_modified(self, linct, new_expr, engine):
+        tagval = self.tag
+        engine.update_extra_constraint(linct, tagval, new_expr)
+
+
+class _ConstraintLogicalUsage(object):
+    def __init__(self, logct):
+        self._log_ct = logct
+
+    @property
+    def tag(self):
+        return "logical"
+
+    _cannot_modify_linearct_non_discrete_msg = 'Linear constraint: {0} is used in equivalence, cannot be modified with non-discrete expr: {1}'
+
+    def notify_expr_modified(self, linct, new_expr, engine):
+        log_ct = self._log_ct
+        if log_ct.is_equivalence() and not new_expr.is_discrete():
+            linct.fatal(self._cannot_modify_linearct_non_discrete_msg, linct, new_expr)
+        else:
+            engine.update_constraint(log_ct, event=UpdateEvent.IndicatorLinearConstraint)
+
+
+
[docs]class AbstractConstraint(IndexableObject, _AbstractBendersAnnotated): + __slots__ = () + + def __init__(self, model, name=None): + IndexableObject.__init__(self, model, name) + + + @property + def priority(self): + return self._model.get_constraint_priority(self) + + @priority.setter + def priority(self, newprio): + self.set_priority(newprio) + + def set_priority(self, newprio): + self._model.set_constraint_priority(self, Priority.parse(newprio, logger=self.error_handler, accept_none=True)) + +
[docs] def set_mandatory(self): + ''' Sets the constraint as mandatory. + + This prevents relaxation from relaxing this constraint. + To revert this, set the priority to any non-mandatory priority, or None. + ''' + self.priority = Priority.MANDATORY
+ + def is_mandatory(self): + return Priority.MANDATORY == self.priority + + # noinspection PyUnusedLocal + def _unsupported_relational_op(self, op_string, other): + self.fatal("Relational operator: {1} is unavailable with constraint: {0!s}", self, op_string) + + def __le__(self, e): + self._unsupported_relational_op("<=", e) + + def __ge__(self, e): + self._unsupported_relational_op(">=", e) + + def __lt__(self, e): + self._unsupported_relational_op("<", e) + + def __gt__(self, e): + self._unsupported_relational_op(">", e) + + # py3 needs an eq for hashing + # def __eq__(self, e): + # self._unsupported_relational_op("==", e) + + def _no_linear_ct_in_logical_test_error(self): + raise TypeError("cannot convert a constraint to boolean: {0!s}".format(self)) + + def __nonzero__(self): + self._no_linear_ct_in_logical_test_error() + + def __bool__(self): + # python 3 version of nonzero + self._no_linear_ct_in_logical_test_error() # pragma: no cover + + def iter_variables(self): + raise NotImplementedError # pragma: no cover + + def iter_exprs(self): + raise NotImplementedError # pragma: no cover + + def get_var_coef(self, dvar): # pragma: no cover + raise NotImplementedError + + @property + def size(self): + return sum(x.size for x in self.iter_exprs()) + + def copy(self, target_model, var_map): + raise NotImplementedError # pragma: no cover + + def relaxed_copy(self, relaxed_model, var_map): + return self.copy(relaxed_model, var_map) + + def compute_infeasibility(self, slack): # pragma: no cover + # INTERNAL: only used when json has no infeasibility info. + return slack + + # noinspection PyMethodMayBeStatic + def notify_deleted(self): + # INTERNAL + self._set_invalid_index() + + @property + def short_typename(self): + return "constraint" + + @property + def lp_name(self): + return self._name or "c%s" % (self.index + 1) + + @property + def lpt_name(self): + radix = self.cplex_scope.prefix + return "%s%d" % (radix, self.index + 1) + + def is_trivial(self): + return False + + def is_linear(self): + return False + + def is_quadratic(self): + return False + +
[docs] def is_added(self): + """ Returns True if the constraint has been added to its model. + + Example: + c = (x+y == 1) + m.add(c) + c.is_added() + >>> True + c2 = (x + 2*y) >= 3 + c2.is_added() + >>> False + + """ + return self.index >= 0
+ + @property + def cplex_scope(self): + return self._get_index_scope().cplex_scope + + def _get_index_scope(self): + raise NotImplementedError + + def is_logical(self): + return False + + def _get_dual_value(self): + # INTERNAL + # Note that dual values are only available for LP problems, + # so can be calle donly on linear or range constraints. + return self._model._dual_value1(self) + + def notify_expr_modified(self, expr, event): + # INTERNAL + pass # pragma: no cover + + def notify_expr_replaced(self, old_expr, new_expr): + # INTERNAL + pass # pragma: no cover + + def resolve(self): + raise NotImplementedError # pragma: no cover + + def is_satisfied(self, solution, tolerance): + raise NotImplementedError # pragma: no cover + + def lock_discrete(self): + # INTERNAL + # lock sub expressions + for expr in self.iter_exprs(): + expr.lock_discrete() + + def to_readable_string(self): + return str(self) + + def _compute_violation(self, solution, tolerance): + # fallback + return 0
+ + +# noinspection PyAbstractClass +
[docs]class BinaryConstraint(AbstractConstraint): + __slots__ = ("_ctsense", "_left_expr", "_right_expr") + + def _internal_set_sense(self, new_sense): + self._ctsense = new_sense + + def __init__(self, model, left_expr, ctsense, right_expr, name=None): + IndexableObject.__init__(self, model, name) + self._ctsense = ctsense + # noinspection PyPep8 + self._left_expr = left_expr + self._right_expr = right_expr + + def _iter_usages(self): + return iter([]) + + @property + def type(self): + """ This property returns the type of the constraint; type is an enumerated value + of type :class:`ComparisonType`, with three possible values: + + - LE for e1 <= e2 constraints + + - EQ for e1 == e2 constraints + + - GE for e1 >= e2 constraints + + where e1 and e2 denote linear expressions. + + """ + return self._ctsense + + @property + def cplex_code(self): + return self._ctsense._cplex_code + +
[docs] def get_left_expr(self): + """ This property returns the left expression in the constraint. + + Example: + (X+Y <= Z+1) has left expression (X+Y). + """ + return self._left_expr
+ +
[docs] def get_right_expr(self): + """ This property returns the right expression in the constraint. + + Example: + (X+Y <= Z+1) has right expression (Z+1). + """ + return self._right_expr
+ + def get_var_coef(self, dvar): + return self._left_expr.unchecked_get_coef(dvar) - self._right_expr.unchecked_get_coef(dvar) + +
[docs] def to_string(self, use_space=False): + """ Returns a string representation of the constraint. + + The operators in this representation are the usual operators <=, ==, and >=. + + Example: + The constraint (X+Y <= Z+1) is represented as "X+Y <= Z+1". + + Returns: + A string. + + """ + mdl_str_len_max = self.model.str_max_len + return self._to_string(use_space, mdl_str_len_max)
+ + def to_readable_string(self): + return self._to_string(use_space=True, str_len_max=self.model.readable_str_len) + + def _to_string(self, use_space, str_len_max): + left_string = self._left_expr.to_string(use_space=use_space) + right_string = self._right_expr.to_string(use_space=use_space) + self_name = self.name + s_lhs = str_maxed(left_string, str_len_max) + s_rhs = str_maxed(right_string, str_len_max) + s_name = "%s: " % self_name if self_name else "" + return u"%s%s %s %s" % (s_name, s_lhs, self._ctsense.operator_symbol, s_rhs) + + def cplex_num_rhs(self): + # INTERNAL + right_cst = self._right_expr.get_constant() + left_cst = self._left_expr.get_constant() + return float(right_cst - left_cst) + + @property + def cplex_net_rhs(self): + # returns the net constant (rhs.constant - lhs.constant) + # converted to float (no numpy floats) + return self.cplex_num_rhs() + + def __repr__(self): + classname = self.__class__.__name__ + user_name = self.safe_name + typename = self._ctsense.short_name + sleft = self._left_expr.repr_str() + sright = self._right_expr.repr_str() + return "docplex.mp.{0}[{1}]({2!s},{3},{4!s})". \ + format(classname, user_name, sleft, typename, sright) + + def _is_trivially_feasible(self): + # INTERNAL : assume self is trivial() + op_func = self._ctsense.python_operator + return op_func(self._left_expr.get_constant(), self._right_expr.get_constant()) if op_func else False + + def _is_trivially_infeasible(self): + # INTERNAL: assume self is trivial . + op_func = self._ctsense.python_operator + return not op_func(self._left_expr.get_constant(), self._right_expr.get_constant()) if op_func else False + + def is_trivial_feasible(self): + return self.is_trivial() and self._is_trivially_feasible() + + def is_trivial_infeasible(self): + return self.is_trivial() and self._is_trivially_infeasible() + + @staticmethod + def _generate_expr_opposite_linear_coefs(expr): + for v, k in expr.iter_sorted_terms(): + yield v, -k + + def _iter_net_linear_coefs_sorted(self, left_expr, right_expr): + # INTERNAL + if right_expr.is_constant(): + return left_expr.iter_sorted_terms() + elif left_expr.is_constant(): + return self._generate_expr_opposite_linear_coefs(right_expr) + else: + return self._generate_net_linear_coefs2_sorted(left_expr, right_expr) + +
[docs] def iter_variables(self): + """ Iterates over all variables mentioned in the constraint. + + *Note:* This includes variables that are mentioned with a zero coefficient. For example, + the iterator on the following constraint: + + X <= X+Y + 1 + + will return X and Y, although X is mentioned with a zero coefficient. + + Returns: + An iterator object. + """ + if self._right_expr.is_constant(): + return self._left_expr.iter_variables() + elif self._left_expr.is_constant(): + return self._right_expr.iter_variables() + else: + return self.generate_ordered_vars()
+ + def generate_ordered_vars(self): + left_expr = self._left_expr + for lv in left_expr.iter_variables(): + yield lv + for rv in self._right_expr.iter_variables(): + if not left_expr.contains_var(rv): + yield rv + + @staticmethod + def _generate_net_linear_coefs2_sorted(left_expr, right_expr): + # INTERNAL + for lv, lk in left_expr.iter_sorted_terms(): + net_k = lk - right_expr.unchecked_get_coef(lv) + if net_k: + yield lv, net_k + for rv, rk in right_expr.iter_sorted_terms(): + if not left_expr.contains_var(rv) and rk: + yield rv, -rk + + @staticmethod + def _generate_net_linear_coefs2_unsorted(left_expr, right_expr): + # INTERNAL + for lv, lk in left_expr.iter_terms(): + net_k = lk - right_expr.unchecked_get_coef(lv) + yield lv, net_k + for rv, rk in right_expr.iter_terms(): + if not left_expr.contains_var(rv): + yield rv, -rk + + def notify_deleted(self): + # INTERNAL + super(BinaryConstraint, self).notify_deleted() + self._left_expr.notify_unsubscribed(self) + self._right_expr.notify_unsubscribed(self) + + def iter_exprs(self): + return iter([self._left_expr, self._right_expr]) + + def get_expr_from_pos(self, pos): + if 0 == pos: + return self._left_expr + elif 1 == pos: + return self._right_expr + else: # pragma: no cover + self.fatal('Unexpected expression position: {0!r}, expecting 0 or 1', pos) + + def set_expr_from_pos(self, pos, new_expr): + if 0 == pos: + self._left_expr = new_expr + elif 1 == pos: + self._right_expr = new_expr + else: # pragma: no cover + self.fatal('Unexpected expression position: {0!r}, expecting 0 or 1', pos) + + def is_satisfied(self, solution, tolerance=1e-6): + violation = self._compute_violation(solution, tolerance) + return violation == 0 + + def _compute_violation(self, solution, tolerance, _eps_zero=1e-10): + left_value = self._left_expr._raw_solution_value(solution) + right_value = self._right_expr._raw_solution_value(solution) + net_value = left_value - right_value + ctsense = self._ctsense + violation = 0 + raw_violation = 0 + if ctsense == ComparisonType.EQ: + violation = max(0, abs(net_value) - tolerance) + raw_violation = abs(net_value) + elif ctsense == ComparisonType.LE: + violation = max(0, net_value - tolerance) + raw_violation = max(0, net_value) + elif ctsense == ComparisonType.GE: + violation = max(0, - (net_value + tolerance)) + raw_violation = max(0, -net_value) + + return raw_violation if violation >= _eps_zero else 0 + + def resolve(self): + self._left_expr.resolve() + self._right_expr.resolve() + + def is_discrete(self): + return self.get_left_expr().is_discrete() and self.get_right_expr().is_discrete() + + @property + def sense_string(self): + return self._ctsense.name
+ + +
[docs]class LinearConstraint(BinaryConstraint, LinearOperand): + """ The class that models all constraints of the form `<expr1> <OP> <expr2>`, + where <expr1> and <expr2> are linear expressions. + """ + __slots__ = ('_status_var', '_usages') + + def __init__(self, model, left_expr, ctsense, right_expr, name=None): + BinaryConstraint.__init__(self, model, left_expr, ctsense, right_expr, name) + left_expr.notify_used(self) + right_expr.notify_used(self) + + def check_name(self, new_name): + self.check_lp_name('constraint', new_name, accept_empty=True, accept_none=True) + + def set_name(self, new_name): + # INTERNAL + self.check_name(new_name) + if self.is_added(): + self._model.set_linear_constraint_name(self, new_name) + else: + self._set_name(new_name) + + def notify_used_in_logical_ct(self, lct): + self._add_usage(_ConstraintLogicalUsage(lct)) + + def _add_usage(self, usage): + if hasattr(self, '_usages'): + self._usages.append(usage) + else: + self._usages = [usage] + + def _get_usages(self): + return getattr(self, '_usages', []) + + def _iter_usages(self): + # INTERNAL + return iter(self._get_usages()) + + def is_linear(self): + return True + + @property + def short_typename(self): + return "linear constraint" + + # noinspection PyMethodMayBeStatic + def cplex_range_value(self): + return 0.0 + + def is_lazy_constraint(self): + return self.model._is_lazy_constraint(self) + + def is_user_cut_constraint(self): + return self.model._is_user_cut_constraint(self) + + def copy(self, target_model, var_map): + copied_left = self.left_expr.copy(target_model, var_map) + copied_right = self.right_expr.copy(target_model, var_map) + copy_name = None if target_model.ignore_names else self.name + return self.__class__(target_model, copied_left, self.sense, copied_right, copy_name) + + def relaxed_copy(self, relaxed_model, var_map): + copied_left = self.left_expr.relaxed_copy(relaxed_model, var_map) + copied_right = self.right_expr.relaxed_copy(relaxed_model, var_map) + copy_name = self.name + return self.__class__(relaxed_model, copied_left, self.sense, copied_right, copy_name) + + @property + def sense(self): + """ This property is used to get or set the sense of the constraint; sense is an enumerated value + of type :class:`ComparisonType`, with three possible values: + + - LE for e1 <= e2 constraints + + - EQ for e1 == e2 constraints + + - GE for e1 >= e2 constraints + + where e1 and e2 denote linear expressions. + + """ + return self._ctsense + + @sense.setter + def sense(self, new_sense): + self.set_sense(new_sense) + + def set_sense(self, new_sense): + self.get_linear_factory().set_linear_constraint_sense(self, new_sense) + + # compatibility + @property + def type(self): # pragma: no cover + warnings.warn( + "ct.type is deprecated, use ct.sense instead.", + DeprecationWarning, stacklevel=2) + return self._ctsense + + @property + def left_expr(self): + """ This property returns the left expression in the constraint. + + Example: + (X+Y <= Z+1) has left expression (X+Y). + """ + return self._left_expr + + @property + def right_expr(self): + """ This property returns the right expression in the constraint. + + Example: + (X+Y <= Z+1) has right expression (Z+1). + """ + return self._right_expr + + @right_expr.setter + def right_expr(self, new_rexpr): + self.set_right_expr(new_rexpr) + + def set_right_expr(self, new_rexpr): + self.get_linear_factory().set_linear_constraint_right_expr(ct=self, new_rexpr=new_rexpr) + + @left_expr.setter + def left_expr(self, new_lexpr): + self.set_left_expr(new_lexpr) + + def set_left_expr(self, new_lexpr): + self.get_linear_factory().set_linear_constraint_left_expr(ct=self, new_lexpr=new_lexpr) + + # aliases + lhs = left_expr + rhs = right_expr + + def _no_linear_ct_in_logical_test_error(self): + # if self.sense == ComparisonType.EQ: + # # for equality testing there -is- a workaround + # msg = "Cannot use == to test expression equality, try using Python is operator or method equals: {0!s}".format(self) + # else: + msg = "Cannot convert linear constraint to a boolean value: {0!s}".format(self) + if self.sense == ComparisonType.EQ: + # for equality testing there -is- a workaround + msg += "\n try using Python 'is' operator or method 'equals' for expression equality" + + raise TypeError(msg) + + def _cannot_promote_from_linear_to_quadratic(self, old_expr, new_expr): + msg = 'Cannot change linear constraint expr from linear to quadratic' + if new_expr is None: + self.fatal('{0}: {1}', msg, old_expr) + else: + self.fatal('{0}: was: {1!s}, new: {2!s}', msg, old_expr, new_expr) + + def _check_editable(self, new_expr, engine): + for usage in self._iter_usages(): + usage.notify_expr_modified(self, new_expr, engine) + + def notify_expr_modified(self, expr, event): + # INTERNAL + if event: + if event is UpdateEvent.LinExprPromotedToQuad: + self._cannot_promote_from_linear_to_quadratic(old_expr=expr, new_expr=None) + else: + self.get_linear_factory().update_linear_constraint_exprs(ct=self, expr_event=event) + + def notify_expr_replaced(self, old_expr, new_expr): + # INTERNAL + if new_expr.is_quad_expr() and not old_expr.is_quad_expr(): + self._cannot_promote_from_linear_to_quadratic(old_expr, new_expr) + if old_expr is self._left_expr: + self.get_linear_factory().set_linear_constraint_expr_from_pos(lct=self, pos=0, new_expr=new_expr, + update_subscribers=False) + elif old_expr is self._right_expr: + self.get_linear_factory().set_linear_constraint_expr_from_pos(lct=self, pos=1, new_expr=new_expr, + update_subscribers=False) + else: + # should not happen + pass + # new expr takes al subscribers from old expr + if not is_var(new_expr): + new_expr.grab_subscribers(old_expr) + +
[docs] def to_string(self, use_space=False): + """ Returns a string representation of the constraint. + + The operators in this representation are the usual operators <=, ==, and >=. + + Example: + The constraint (X+Y <= Z+1) is represented as "X+Y <= Z+1". + + Returns: + A string. + + """ + return BinaryConstraint.to_string(self, use_space=use_space)
+ + def compute_infeasibility(self, slack): # pragma: no cover + ctsense = self._ctsense + if ctsense == ComparisonType.EQ: + infeas = slack + elif ComparisonType.LE == ctsense: + infeas = slack if slack <= 0 else 0 + elif ComparisonType.GE == ctsense: + infeas = slack if slack >= 0 else 0 + else: + infeas = 0 + return infeas + + def _get_index_scope(self): + return self._model._linct_scope + + def is_trivial(self): + # Checks whether the constraint is equivalent to a comparison between numbers. + # For example, x <= x+1 is trivial, but 1.5 X <= X + 1 is not. + def has_nonzero_coef(term_iter): + return any(tk for _, tk in term_iter) + + self_left_expr = self._left_expr + self_right_expr = self._right_expr + if self_right_expr.is_constant(): + return not has_nonzero_coef(self.left_expr.iter_terms()) + elif self_left_expr.is_constant(): + return not has_nonzero_coef(self.right_expr.iter_terms()) + else: + return not has_nonzero_coef( + BinaryConstraint._generate_net_linear_coefs2_unsorted(self_left_expr, self_right_expr)) + + def _post_meta_constraint(self, rhs, ctsense): + status_var = self.get_resolved_status_var() + return self._model._new_xconstraint(lhs=status_var, rhs=rhs, comparaison_type=ctsense) + + def le(self, rhs): + return self._post_meta_constraint(rhs, ComparisonType.LE) + + def eq(self, rhs): + return self._post_meta_constraint(rhs, ComparisonType.EQ) + + def ge(self, rhs): + return self._post_meta_constraint(rhs, ComparisonType.GE) + + def __le__(self, rhs): + return self.le(rhs) + + def __ge__(self, rhs): + return self.ge(rhs) + + def __hash__(self): + return id(self) + + def unchecked_get_coef(self, dvar): + return 1 if dvar is self._get_status_var() else 0 + + def contains_var(self, dvar): + # only user vars + return dvar is self._get_status_var() + + def iter_terms(self): + yield self.get_resolved_status_var(), 1 + + iter_sorted_terms = iter_terms + + @property + def dual_value(self): + """ This property returns the dual value of the constraint. + + Note: + This method will raise an exception if the model has not been solved successfully. + This method is OK with small numbers of constraints. For large numbers of constraints (>100), + consider using Model.dual_values() with a sequence of constraints. + + See Also: + `func:docplex.mp.model.Model.dual_values()` + """ + return self._get_dual_value() + + @property + def slack_value(self): + """ This property returns the slack value of the constraint. + + Note: + This method will raise an exception if the model has not been solved successfully. + + + This method is OK with small numbers of constraints. For large numbers of constraints (>100), + consider using Model.slack_values() with a sequence of constraints. + + See Also: + `func:docplex.mp.model.Model.slack_values()` + """ + return self._model._slack_value1(self) + + @property + def basis_status(self): + """ This property returns the basis status of the slack variable of the constraint, if any. + + Returns: + An enumerated value from the enumerated type `docplex.constants.BasisStatus`. + + Note: + for the model to hold basis information, the model must have been solved as a LP problem. + In some cases, a model which failed to solve may still have a basis available. Use + `Model.has_basis()` to check whether the model has basis information or not. + + See Also: + :func:`docplex.mp.model.Model.has_basis` + :class:`docplex.mp.constants.BasisStatus` + + *New in version 2.10* + + """ + return self._model._linearct_basis_status([self])[0] + + def iter_net_linear_coefs(self): + # INTERNAL + left_expr = self._left_expr + right_expr = self._right_expr + if right_expr.is_constant(): + return left_expr.iter_sorted_terms() + elif left_expr.is_constant(): + return self._generate_expr_opposite_linear_coefs(right_expr) + else: + return self._generate_net_linear_coefs2_sorted(left_expr, right_expr) + + def _get_status_var(self): + # this call does -not- create the variable. Returns None if not present. + return getattr(self, '_status_var', None) + + @property + def status_var(self): + return self.get_resolved_status_var() + + as_var = status_var + + def as_logical_operand(self): + if not self.is_discrete(): + return None + else: + return self.get_resolved_status_var(caller_msg= + 'Conversion to logical operand is available only for discrete constraints') + + def _check_is_discrete(self, ct, msg=None): + err_msg = msg or "Conversion from constraint to expression is available only for discrete constraints" + StaticTypeChecker.typecheck_discrete_constraint(self, ct, msg=err_msg) + + def get_resolved_status_var(self, caller_msg=None): + status_var = self._get_status_var() # always use the getter! + if status_var is not None: + return status_var + + self._model._check_logical_constraint_support() + + # TODO: issue a meaningful message on why the ct is not discrete + self._check_is_discrete(self, msg=caller_msg) + # lock it discrete + self.lock_discrete() + + # lazy allocation of a new status variable...: + lfactory = self.get_linear_factory() + status_var = lfactory.new_constraint_status_var(self) + if self.is_added(): + # status variable is bound + status_var.lb = 1 + self._status_var = status_var + # store ct in model + eqct = lfactory.new_equivalence_constraint(status_var, linear_ct=self) + eqct.origin = self + engine = self._model.get_engine() + eqx = engine.create_logical_constraint(eqct, is_equivalence=True) + self._model._register_implicit_equivalence_ct(eqct, eqx) + return status_var + + def to_linear_expr(self): + return self.status_var + + def notify_deleted(self): + super(LinearConstraint, self).notify_deleted() + svar = self._get_status_var() # possibly not resolved + if svar: + svar.lb = 0 + + @property + def benders_annotation(self): + """ + This property is used to get or set the Benders annotation of a constraint. + The value of the annotation must be a positive integer + + """ + return self.get_benders_annotation() + + @benders_annotation.setter + def benders_annotation(self, new_anno): + self.set_benders_annotation(new_anno) + + # -- arithmetic operators + def times(self, e): + # TODO: dow e limit numbers here, otherwise non-convex errors may creep in + return self.to_linear_expr().__mul__(e) + + def __mul__(self, e): + return self.times(e) + + def __rmul__(self, e): + return self.times(e) + + def __div__(self, e): + return self.quotient(e) + + def __truediv__(self, e): + # for py3 + # INTERNAL + return self.quotient(e) # pragma: no cover + + def quotient(self, e): + svar = self.get_resolved_status_var() + self._model._typecheck_as_denominator(e, svar) + inverse = 1.0 / float(e) + return self.times(inverse) + + def __add__(self, e): + return self.to_linear_expr().__add__(e) + + def __radd__(self, e): + return self.to_linear_expr().__add__(e) + + def __sub__(self, e): + return self.to_linear_expr().__sub__(e) + + def __rsub__(self, e): + return self.to_linear_expr().__rsub__(e) + + def __or__(self, other): + return self.logical_or(other) + + def __and__(self, other): + return self.logical_and(other) + + def _check_logical_operator(self, other, caller=None): + self._check_is_discrete(self, msg="Logical operators require discrete constraints") + StaticTypeChecker.typecheck_logical_op(self, other, caller=caller) + + def logical_or(self, other): + self._check_logical_operator(other, "LinearConstraint.or") + return self.get_linear_factory().new_binary_constraint_or(self, other) + + def logical_and(self, other): + self._check_logical_operator(other, "LinearConstraint.and") + return self.get_linear_factory().new_binary_constraint_and(self, other) + + def __rshift__(self, other): + """ Redefines the right-shift operator to define if-then constraints. + + This operator allows to create if-then constraint with the `>>` operator. + It expects a linear constraint as second argument. + + :param other: the linear constraint used to build a new if-then constraint. + + :return: + an instance of IfThenConstraint, that is not added to the model. + Use `Model.add()` to add it to the model. + + Note: + The constraint must be discrete, otherwise an exception is raised. + + Example: + + m.add((x >= 3) >> (y == 5)) + + creates an if-then constraint which links the satisfaction of constraint (x >= 3) to the satisfaction + of (y ==5). + + """ + return self._model.if_then(if_ct=self, then_ct=other) + + def _tag_as_extra_ct(self, qualifier): + # INTERNAL + self._add_usage(_ExtraConstraintUsage(qualifier)) + + def _untag_as_extra_ct(self, qualifier): + usages = self._get_usages() + upos = -1 + for u, used in enumerate(usages): + if used.tag == qualifier: + upos = u + break + if upos >= 0: + del usages[upos] + + _user_cut_tag = "user-cut constraint" + _lazy_constraint_tag = "lazy constraint" + + def notify_used_as_user_cut(self): + # INTERNAL + self._tag_as_extra_ct(self._user_cut_tag) + + def notify_used_as_lazy_constraint(self): + # INTERNAL + self._tag_as_extra_ct(self._lazy_constraint_tag) + + def notify_unused_as_user_cut(self): + # INTERNAL + self._untag_as_extra_ct(self._user_cut_tag) + + def notify_unused_as_lazy_constraint(self): + # INTERNAL + self._untag_as_extra_ct(self._lazy_constraint_tag)
+ + +
[docs]class RangeConstraint(AbstractConstraint): + """ This class models range constraints. + + A range constraint states that an expression must stay between two + values, `lb` and `ub`. + + This class is not meant to be instantiated by the user. + To create a range constraint, use the factory method :func:`docplex.mp.model.Model.add_range` + defined on :class:`docplex.mp.model.Model`. + + """ + + def __init__(self, model, expr, lb, ub, name=None): + AbstractConstraint.__init__(self, model, name) + self._ub = ub + self._lb = lb + self._expr = expr + + def is_linear(self): + return True + + # noinspection PyMethodMayBeStatic + @property + def cplex_code(self): + return 'R' + + def _get_index_scope(self): + return self._model._linct_scope + + @property + def short_typename(self): + return "range constraint" + + def is_trivial(self): + return self._expr.is_constant() + + def _is_trivially_feasible(self): + # INTERNAL : assume self is trivial() + expr_num = self._expr.constant + return self._lb <= expr_num <= self._ub + + def _is_trivially_infeasible(self): + # INTERNAL : assume self is trivial() + expr_num = self._expr.constant + return expr_num < self._lb or expr_num > self._ub + + def compute_infeasibility(self, slack): # pragma: no cover + # compatible with cplex... + return -slack + + def get_var_coef(self, dvar): + return self._expr.unchecked_get_coef(dvar) + + def is_satisfied(self, solution, tolerance=1e-6): + expr_value = self._expr._raw_solution_value(solution) + return self._lb - tolerance <= expr_value <= self._ub + tolerance + + @property + def expr(self): + """ This property returns the linear expression of the range constraint. + """ + return self._expr + + @expr.setter + def expr(self, new_expr): + self.get_linear_factory().set_range_constraint_expr(self, new_expr) + + @property + def lb(self): + """ This property is used to get or set the lower bound of the range constraint. + + """ + return self._lb + + @lb.setter + def lb(self, new_lb): + self._model._typecheck_num(new_lb) + self.get_linear_factory().set_range_constraint_lb(self, new_lb) + + @property + def ub(self): + """ This property is used to get or set the upper bound of the range constraint. + + """ + return self._ub + + @ub.setter + def ub(self, new_ub): + self._model._typecheck_num(new_ub) + self.get_linear_factory().set_range_constraint_ub(self, new_ub) + + @property + def bounds(self): + """ This property is used to get or set the (lower, upper) bounds of a range constraint. + + """ + return self._lb, self._ub + + @bounds.setter + def bounds(self, new_bounds): + try: + new_lb, new_ub = new_bounds + self._model._typecheck_num(new_lb) + self._model._typecheck_num(new_ub) + self.get_linear_factory().set_range_constraint_bounds(self, new_lb, new_ub) + except ValueError: + self.fatal('RangeConstraint.bounds expects a 2-tuple of numbers, {0!r} was passed', new_bounds) + + def _internal_set_lb(self, new_lb): + self._lb = new_lb + + def _internal_set_ub(self, new_ub): + self._ub = new_ub + + def is_feasible(self): + return self._ub >= self._lb + + @property + def dual_value(self): + """ This property returns the dual value of the constraint. + + Note: + This method will raise an exception if the model has not been solved successfully. + """ + return self._get_dual_value() + + @property + def slack_value(self): + """ This property returns the slack value of the constraint. + + Note: + This method will raise an exception if the model has not been solved successfully. + """ + return self._model._slack_value1(self) + + @property + def basis_status(self): + """ This property returns the basis status of the slack variable of the constraint, if any. + + Returns: + An enumerated value from the enumerated type `docplex.constants.BasisStatus`. + + Note: + for the model to hold basis information, the model must have been solved as a LP problem. + In some cases, a model which failed to solve may still have a basis available. Use + `Model.has_basis()` to check whether the model has basis information or not. + + See Also: + :func:`docplex.mp.model.Model.has_basis` + :class:`docplex.mp.constants.BasisStatus` + + *New in version 2.10* + """ + return self._model._linearct_basis_status([self])[0] + +
[docs] def iter_variables(self): + """Iterates over all the variables of the range constraint. + + Returns: + An iterator object. + """ + return self._expr.iter_variables()
+ + def iter_exprs(self): + yield self._expr + + def cplex_range_value(self, do_raise=True): + return self.static_cplex_range_value(self, self._lb, self._ub, + lambda: "Range has infeasible domain: {0!s}".format(self), + do_raise=do_raise) + + @classmethod + def static_cplex_range_value(cls, logger, lbval, ubval, msg_fun, do_raise=True): + rangeval = float(lbval - ubval) + # this should be negative, otherwise fails.... + # no way to model infeasible ranges with cplex rngval. + if rangeval >= 1e-6 and do_raise: + logger.fatal(msg_fun()) + return rangeval + + def cplex_num_rhs(self): + # force conversion to float for numpy, etc... + return float(self._ub - self._expr.get_constant()) + + def get_left_expr(self): + return self._expr + + # noinspection PyMethodMayBeStatic + def get_right_expr(self): + return None + + def copy(self, target_model, var_map): + copied_expr = self.expr.copy(target_model, var_map) + copy_name = None if target_model.ignore_names else self.name + copied_range = RangeConstraint(target_model, copied_expr, self.lb, self.ub, copy_name) + return copied_range + + def to_string(self, use_space=False): + np = self.model._num_printer + s_expr = self._expr.to_string(use_space=use_space) + return "{0} <= {1!s} <= {2}".format(np.to_string(self._lb), s_expr, np.to_string(self._ub)) + + def to_readable_string(self): + np = self.model._num_printer + s_expr = self._expr.to_string(use_space=True)[:self.model.readable_str_len] + return "{0} <= {1!s} <= {2}".format(np.to_string(self._lb), s_expr, np.to_string(self._ub)) + + def __str__(self): + """ Returns a string representation of the range constraint. + + Example: + 1 <= x+y+z <= 3 represents the range constraint where the expression (x+y+z) is + constrained to stay between 1 and 3. + + Returns: + A string. + """ + return self.to_string(use_space=self.model.str_use_space) + + def __repr__(self): + printable_name = self.safe_name + return "docplex.mp.RangeConstraint[{0}]({1},{2!s},{3})". \ + format(printable_name, self.lb, self._expr, self.ub) + + def resolve(self): + self._expr.resolve() + + @property + def benders_annotation(self): + """ + This property is used to get or set the Benders annotation of a constraint. + The value of the annotation must be a positive integer + + """ + return self.get_benders_annotation() + + @benders_annotation.setter + def benders_annotation(self, new_anno): + self.set_benders_annotation(new_anno)
+ + +
[docs]class NotEqualConstraint(LinearConstraint): + + def __init__(self, model, negated_eqct, name=None): + # assume negated_eqct is the equality constraint we want to negate + self._negated_ct = negated_eqct + aux_eq_ct_status = self._negated_ct.get_resolved_status_var() + zero = model._lfactory.new_zero_expr() + LinearConstraint.__init__(self, model, aux_eq_ct_status, ComparisonType.EQ, zero, name) + self.lock_discrete() + +
[docs] def to_string(self, use_space=False): + eqct = self._negated_ct + s_lhs = eqct._left_expr.to_string(use_space=use_space) + s_rhs = eqct._right_expr.to_string(use_space=use_space) + return "{0} != {1}".format(s_lhs, s_rhs)
+ + def set_sense(self, new_sense): + self.fatal("cannot modify sense of a not_equal constraint: {0!s}", self) + + @property + def negated_constraint(self): + return self._negated_ct + + def __str__(self): + """ Returns a string representation of the not equals constraint. + + Returns: + A string. + """ + return self.to_string(use_space=self.model.str_use_space) + + def __repr__(self): + return "docplex.mp.NotEquals({0}, {1})". \ + format(self._left_expr, self._right_expr)
+ + +
[docs]class LogicalConstraint(AbstractConstraint): + """ This class models logical constraints. + + An equivalence constraint links (both ways) the value of a binary variable + to the satisfaction of a linear constraint. + + If the binary variable equals the truth value (default is 1), + then the constraint is satisfied, conversely if the constraint + is satisfied, the value of the variable is set to the truth value. + + This class is not meant to be instantiated by the user. + + """ + __slots__ = ('_binary_var', '_linear_ct', '_active_value') + + def __init__(self, model, binary_var, linear_ct, active_value=1, name=None): + AbstractConstraint.__init__(self, model, name) + self._binary_var = binary_var + self._linear_ct = linear_ct + self._active_value = active_value + # connect exprs + for expr in linear_ct.iter_exprs(): + expr.notify_used(self) + + linear_ct.notify_used_in_logical_ct(self) + + def resolve(self): + self._linear_ct.resolve() + + def _get_index_scope(self): + return self._model._logical_scope + + def iter_exprs(self): + return self._linear_ct.iter_exprs() + + def is_equivalence(self): + raise NotImplementedError # pragma: no cover + + def cplex_num_rhs(self): + return self._linear_ct.cplex_num_rhs() + + @property + def active_value(self): + return self._active_value + + @property + def binary_var(self): + return self._binary_var + + @property + def linear_constraint(self): + return self._linear_ct + + @property + def lct(self): + return self.linear_constraint + + @property + def benders_annotation(self): + """ + This property is used to get or set the Benders annotation of a constraint. + The value of the annotation must be a positive integer + + """ + return self.get_benders_annotation() + + @benders_annotation.setter + def benders_annotation(self, new_anno): + self.set_benders_annotation(new_anno) + + def get_linear_constraint(self): + return self._linear_ct + + @property + def cpx_complemented(self): + return 1 - self._active_value + + def copy(self, target_model, var_map): + copied_binary = var_map[self.binary_var] + copied_linear_ct = self.linear_constraint.copy(target_model, var_map) + copy_name = None if target_model.ignore_names else self.name + copied_equiv = self.__class__(target_model, + copied_binary, + copied_linear_ct, + self._active_value, + copy_name) + return copied_equiv + + def relaxed_copy(self, relaxed_model, var_map): + raise DocplexLinearRelaxationError(self, cause='logical') + + def is_logical(self): + return True + + def iter_variables(self): + yield self._binary_var + for v in self._linear_ct.iter_variables(): + yield v + + def get_var_coef(self, dvar): + if dvar is self._binary_var: + return 1 + else: + return self._linear_ct.get_var_coef(dvar) + + def __str__(self): + return self.to_string(use_space=self.model.str_use_space) + + def __repr__(self): + printable_name = self.safe_name + clazzname = self.__class__.__name__ + s_lc = self._linear_ct.to_string(use_space=False).replace(' ', '') + + return "docplex.mp.constr.{0:s}[{1}]({2!s},{3!s},true={4})" \ + .format(clazzname, printable_name, self._binary_var, s_lc, self._active_value) + + def notify_expr_modified(self, expr, event): + # INTERNAL + self.get_linear_factory().update_indicator_constraint_expr(self, event, expr) + + @property + def slack_value(self): + return self._model._slack_value1(self) + + def _symbol(self): + return '<->' if self.is_equivalence() else '->' + +
[docs] def to_string(self, use_space=False): + """ + Displays the equivalence constraint in a (shortened) LP style: + z <-> x+y+z == 2 + + Returns: + A string. + """ + + return self._to_string(use_space, self.model.str_max_len)
+ + def _to_string(self, use_space, max_str_len): + eqname = self.name + name_part = '{0}: '.format(eqname) if eqname else '' + eq_var = self._binary_var + if eq_var.is_generated(): + varname = 'x%d' % eq_var._index + else: + varname = str(eq_var) + s_active_value = '' if self._active_value else '=0' + s_symbol = self._symbol() + s_lc = self.linear_constraint._to_string(use_space, max_str_len) + return "{0}{1}{2} {4} [{3}]".format(name_part, varname, s_active_value, s_lc, s_symbol) + + def to_readable_string(self): + return self._to_string(use_space=True, max_str_len=self.model.readable_str_len)
+ + +
[docs]class IndicatorConstraint(LogicalConstraint): + """ This class models indicator constraints. + + An indicator constraint links (one-way) the value of a binary variable to the satisfaction of a linear constraint. + If the binary variable equals the active value, then the constraint is satisfied, but otherwise the constraint + may or may not be satisfied. + + This class is not meant to be instantiated by the user. + + To create an indicator constraint, use the factory method :func:`docplex.mp.model.Model.add_indicator` + defined on :class:`docplex.mp.model.Model`. + + """ + __slots__ = () + + def __init__(self, model, binary_var, linear_ct, active_value=1, name=None): + LogicalConstraint.__init__(self, model, binary_var, linear_ct, active_value, name) + + @property + def short_typename(self): + return "indicator constraint" + + def is_equivalence(self): + return False + + def is_logical(self): + return True + +
[docs] def invalidate(self): + """ + Sets the binary variable to the opposite of its active value. + Typically used by indicator constraints with a trivial infeasible linear part. + For example, z=1 -> 4 <= 3 sets z to 0 and + z=0 -> 4 <= 3 sets z to 1. + This is equivalent to if z=a => False, then z *cannot* be equal to a. + """ + if 0 == self.active_value: + # set to 1 : lb = 1 + self.binary_var.lb = 1 + elif 1 == self.active_value: + # set to 0 ub = 0 + self.binary_var.ub = 0 + else: + self.fatal("Unexpected active value for indicator constraint: {0!s}, value is: {1!s}, expecting 0 or 1", + # pragma: no cover + self, self.active_value) # pragma: no cover
+ + def is_satisfied(self, solution, tolerance=1e-6): + binary_value = solution.get_value(self._binary_var) + if abs(binary_value - self._active_value) <= tolerance: + is_ct_satisfied = self._linear_ct.is_satisfied(solution, tolerance) + # only active if binary is active value: + return abs(1 - is_ct_satisfied) <= tolerance + else: + # when binary var is not equal to active_value, indicator has no effect + return True
+ + +
[docs]class EquivalenceConstraint(LogicalConstraint): + """ This class models equivalence constraints. + + An equivalence constraint links (both ways) the value of a binary variable + to the satisfaction of a linear constraint. + + If the binary variable equals the truth value (default is 1), + then the constraint is satisfied, conversely if the constraint + is satisfied, the value of the variable is set to the truth value. + + This class is not meant to be instantiated by the user. + + """ + __slots__ = () + + def __init__(self, model, binary_var, linear_ct, truth_value=1, name=None): + LogicalConstraint.__init__(self, model, binary_var, linear_ct, truth_value, name) + # connect exprs + linear_ct.lock_discrete() + + @property + def short_typename(self): + return "equivalence constraint" + + def is_equivalence(self): + return True + + def is_logical(self): + return True + + def is_satisfied(self, solution, tolerance=1e-6): + bvar = self._binary_var + if bvar.is_generated() and bvar not in solution: + # bvar is not mentioned, ok + return True + + is_ct_satisfied = self._linear_ct.is_satisfied(solution, tolerance) + binary_value = solution.get_value(bvar) + expected_value = self._active_value if is_ct_satisfied else 1 - self._active_value + ok = ComparisonType.almost_equal(binary_value, expected_value, tolerance) + return ok
+ + +
[docs]class IfThenConstraint(IndicatorConstraint): + + def __init__(self, model, if_ct, then_ct, negate=False): + if_ct_status_var = if_ct.status_var + self._if_ct = if_ct + # if negated, then the then constraint is satisfied if not(if_ct) is satisfied + # this is actually an if-else ... + true_value = 0 if negate else 1 + IndicatorConstraint.__init__(self, model, binary_var=if_ct_status_var, + linear_ct=then_ct, active_value=true_value, name=None) + +
[docs] def to_string(self, use_space=False): + return "{0!s} -> {1}".format(self._if_ct, self.linear_constraint.to_string(use_space=use_space))
+ + +
[docs]class QuadraticConstraint(BinaryConstraint): + """ The class models quadratic constraints. + + Quadratic constraints are of the form `<qexpr1> <OP> <qexpr2>`, + where at least one of <qexpr1> or <qexpr2> is a quadratic expression. + + """ + + def copy(self, target_model, var_map): + # noinspection PyPep8 + copied_left_expr = self.left_expr.copy(target_model, var_map) + copied_right_expr = self.right_expr.copy(target_model, var_map) + copy_name = None if target_model.ignore_names else self.name + return QuadraticConstraint(target_model, copied_left_expr, self.type, copied_right_expr, copy_name) + + def relaxed_copy(self, relaxed_model, var_map): + raise DocplexLinearRelaxationError(self, cause='quadratic') + + def is_quadratic(self): + return True + + @property + def short_typename(self): + return "quadratic constraint" + + def _get_index_scope(self): + return self._model._quadct_scope + + __slots__ = () + + def is_trivial(self): + for _, nqk in self.iter_net_quads(): + if nqk: + return False + # now check linear parts + + for _, lk in self.iter_net_linear_coefs(): + if lk: + return False + return True + + def iter_net_linear_coefs(self): + linear_left = self._left_expr.get_linear_part() + linear_right = self._right_expr.get_linear_part() + return self._iter_net_linear_coefs_sorted(linear_left, linear_right) + + def iter_net_quads(self): + # INTERNAL + left_expr = self._left_expr + right_expr = self._right_expr + if not right_expr.is_quad_expr(): + return left_expr.iter_sorted_quads() + elif not left_expr.is_quad_expr(): + return right_expr.iter_opposite_ordered_quads() + else: + return self.generate_ordered_net_quads(left_expr, right_expr) + + @classmethod + def generate_ordered_net_quads(cls, qleft, qright): + # left first, then right + for lqv, lqk in qleft.iter_sorted_quads(): + net_k = lqk - qright._get_quadratic_coefficient_from_var_pair(lqv) + if 0 != net_k: + yield lqv, net_k + for rqv, rqk in qright.iter_sorted_quads(): + if not qleft.contains_quad(rqv) and rqk: + yield rqv, -rqk + + def _set_left_expr(self, new_left_expr): + self.qfactory.set_quadratic_constraint_expr_from_pos(self, pos=0, new_expr=new_left_expr) + + @property + def left_expr(self): + return BinaryConstraint.get_left_expr(self) + + @left_expr.setter + def left_expr(self, new_left_expr): + self._set_left_expr(new_left_expr) + + def _set_right_expr(self, new_right_expr): + self.qfactory.set_quadratic_constraint_expr_from_pos(self, pos=1, new_expr=new_right_expr) + + @property + def right_expr(self): + return BinaryConstraint.get_right_expr(self) + + @right_expr.setter + def right_expr(self, new_right_expr): + self._set_right_expr(new_right_expr) + + # aliases + rhs = right_expr + lhs = left_expr + + @property + def benders_annotation(self): + """ + This property is used to get or set the Benders annotation of a constraint. + The value of the annotation must be a positive integer + + """ + return self.get_benders_annotation() + + @benders_annotation.setter + def benders_annotation(self, new_anno): + self.set_benders_annotation(new_anno) + + @property + def slack_value(self): + """ This property returns the slack value of the constraint. + + Note: + This method will raise an exception if the model has not been solved successfully. + """ + return self._model._slack_value1(self) + + @property + def sense(self): + """ This property is used to get or set the sense of the constraint; sense is an enumerated value + of type :class:`ComparisonType`, with three possible values: + + - LE for e1 <= e2 constraints + + - EQ for e1 == e2 constraints + + - GE for e1 >= e2 constraints + + where e1 and e2 denote quadratic expressions. + + """ + return self._ctsense + + @sense.setter + def sense(self, new_sense): + self.set_sense(new_sense) + + def set_sense(self, new_sense): + self.qfactory.set_quadratic_constraint_sense(self, new_sense) + + # compat + @property + def type(self): + return self._ctsense + + def has_net_quadratic_term(self): + # INTERNAL + return any(nk for _, nk in self.iter_net_quads()) + + def notify_expr_modified(self, expr, event): + # INTERNAL + self.qfactory.update_quadratic_constraint(self, expr, event) + + def notify_expr_replaced(self, old_expr, new_expr): + qfact = self.qfactory + if old_expr is self._left_expr: + qfact.set_quadratic_constraint_expr_from_pos(qct=self, pos=0, new_expr=new_expr, + supdate_subscribers=False) + elif old_expr is self._right_expr: + qfact.set_quadratic_constraint_expr_from_pos(qct=self, pos=1, new_expr=new_expr, + update_subscribers=False) + else: + # should not happen + pass + # new expr takes al subscribers from old expr + if not is_var(new_expr): + new_expr.grab_subscribers(old_expr)
+ + +
[docs]class PwlConstraint(AbstractConstraint): + """ This class models piecewise linear constraints. + + This class is not meant to be instantiated by the user. + To create a piecewise constraint, use the factory method :func:`docplex.mp.model.Model.piecewise` + defined on :class:`docplex.mp.model.Model`. + + """ + + __slots__ = ('_pwl_expr', '_input_var', '_y') + + def __init__(self, model, pwl_expr, name=None): + AbstractConstraint.__init__(self, model, name) + self._pwl_expr = pwl_expr + self._input_var = pwl_expr._x_var + self._y = None + + def resolve(self): + self._pwl_expr.resolve() + + def is_satisfied(self, solution, tolerance): + expr_value = self._input_var._raw_solution_value(solution) + y_value = solution._get_var_value(self._y) + computed_f_expr_value = self.pwl_func.evaluate(expr_value) + return ComparisonType.almost_equal(y_value, computed_f_expr_value, tolerance) + + @property + def expr(self): + """ This property returns the linear expression of the piecewise linear constraint. + """ + return self._input_var + + @property + def pwl_func(self): + """ This property returns the piecewise linear function of the piecewise linear constraint. + """ + return self._pwl_expr.pwl_func + + @property + def y(self): + """ This property returns the output variable associated with the piecewise linear constraint. + """ + if self._y is None: + self._y = self._pwl_expr._get_allocated_f_var() + return self._y + + def _get_index_scope(self): + return self._model._pwl_scope + + def iter_exprs(self): + yield self._pwl_expr + +
[docs] def iter_variables(self): + """Iterates over all the variables of the piecewise linear constraint. + + Returns: + An iterator object. + """ + yield self.y + yield self._input_var
+ + def iter_extended_variables(self): + # iterates on all extended variables involved in the computation + # yvar, xavr, plus all argument rexpr vars + # if argument var is identical to the input var, it is returned twice...? + yield self.y + yield self._input_var + for v in self._pwl_expr._argument_expr.iter_variables(): + yield v + + def get_var_coef(self, dvar): + if dvar is self.y: + return 1 + else: + return self.expr.unchecked_get_coef(dvar) + + def copy(self, target_model, var_map): + # Internal: copy must not be invoked on PwlConstraint. + raise NotImplementedError # pragma: no cover + + def relaxed_copy(self, relaxed_model, var_map): + raise DocplexLinearRelaxationError(self, cause='pwl') + + def to_string(self, use_space=False): + pwlf = self.pwl_func + pwlf_s = pwlf.name or 'pwl?' + s_expr = self.expr.to_string(use_space=use_space) + return "{0} == {1!s}({2!s})".format(self.y, pwlf_s, s_expr) + + def __str__(self): + """ Returns a string representation of the piecewise linear constraint. + + Example: + `y == pwl_name(x + z)` represents the piecewise linear constraint where the variable `y` is + constrained to be equal to the value of the piecewise linear function whose name is 'pwl_name' + applied to the expression (x + z). + + Returns: + A string. + """ + return self.to_string(use_space=self.model.str_use_space) + + def __repr__(self): + return "docplex.mp.PwlConstraint({0},{1!s},{2})". \ + format(self.y, self.pwl_func, self.expr)
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/context.html b/docs/2.24.232/mp/_modules/docplex/mp/context.html new file mode 100644 index 0000000..ffd8ec3 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/context.html @@ -0,0 +1,861 @@ + + + + + + + + + docplex.mp.context — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.context

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2019
+# --------------------------------------------------------------------------
+'''Configuration of Mathematical Programming engine.
+
+The :class:`~Context` is the base class to control the behaviour of solve
+engine.
+
+It is not advised to instanciate a Context in your code.
+
+Instead, you should obtain a Context by the following ways:
+
+    * create one using Context.make_default_context()
+    * use the one in your :class:`docplex.mp.model.Model`
+
+``docplex.mp`` configuration files are stored in files named:
+
+    * cplex_config.py
+    * cplex_config_<hostname>.py
+
+When obtaining a Context with make_default_context(), the PYTHONPATH is
+searched for the configuration files and read.
+
+Configuration files are evaluated with a `context` object in their
+scope, and you set values from this context::
+
+    context.cplex_parameters.emphasis.memory = 1
+    context.cplex_parameters.emphasis.mip = 2
+'''
+import os
+
+from copy import deepcopy
+
+import shlex
+import socket
+import sys
+import warnings
+from os.path import isfile, isabs
+
+from docplex.util.environment import get_environment
+
+from docplex.mp.utils import is_string, open_universal_newline
+from docplex.mp.params.cplex_params import get_params_from_cplex_version
+from docplex.mp.params.parameters import RootParameterGroup
+from docplex.mp.utils import DOcplexException
+from docplex.mp.error_handler import docplex_fatal
+
+try:
+    from docplex.worker.solvehook import get_solve_hook
+except ImportError:
+    get_solve_hook = None
+
+
+def init_cplex_parameters(x):
+    return x.init_cplex_parameters()
+
+
+class StreamWithCustomClose(object):
+    # wrapper for streams, so that we can keep track of those who need
+    # to be closed by us.
+    def __init__(self, target):
+        self._target = target
+
+    def __getattr__(self, name):
+        return getattr(self._target, name)
+
+    def custom_close(self):
+        return self._target.close()
+
+    def __str__(self):
+        return "<{0}>".format(self._target.name or "input")
+
+
+# some utility methods
+# def _get_value_as_int(d, option):
+#     try:
+#         value = int(d[option])
+#     except Exception:
+#         value = None
+#     return value
+#
+#
+# def _convert_to_int(value):
+#     if str(value).lower() == 'none':
+#         return None
+#     try:
+#         value = int(value)
+#     except Exception:
+#         value = None
+#     return value
+#
+#
+# def _get_value_as_string(d, option):
+#     return d.get(option, None)
+#
+#
+# def _get_value_as_boolean(d, option):
+#     try:
+#         value = _convert_to_bool(d[option])
+#     except Exception:
+#         value = None
+#     return value
+
+
+_boolean_map = {'1': True, 'yes': True, 'true': True, 'on': True,
+                '0': False, 'no': False, 'false': False, 'off': False}
+
+
+def _convert_to_bool(value):
+    if value is None:
+        return None
+    elif is_string(value):
+        svalue = str(value).lower()
+        if svalue == "none":
+            return None
+        else:
+            bvalue = _boolean_map.get(svalue)
+            if bvalue is not None:
+                return bvalue
+            else:
+                raise ValueError('Not a boolean: {0}'.format(value))
+    else:
+        raise ValueError('Not a boolean: {0}'.format(value))
+
+
+
[docs]class InvalidSettingsFileError(Exception): + '''The error raised when an error occured when reading a settings file. + + *New in version 2.8* + ''' + + def __init__(self, mesg, filename=None, source=None, *args, **kwargs): + super(InvalidSettingsFileError, self).__init__(mesg) + self.filename = filename + self.source = source
+ + +def is_auto_publishing_solve_details(context): + try: + auto_publish_details = context.solver.auto_publish.solve_details + except AttributeError: + try: + auto_publish_details = context.solver.auto_publish + except AttributeError: + auto_publish_details = False + return auto_publish_details + + +def check_credentials(context): + # Checks if the context has syntactically valid credentials. The context + # has valid credentials when it has an `url` and a `key` fields and that + # both fields are string. + # + # If the credentials are not defined, `message` contains a message describing + # the cause. + # + # Returns: + # (has_credentials, message): has_credentials` - True if the context contains syntactical credentials. + # and `message` - contains a message if applicable. + credentials_ok = True + message = None + if not context.url or not context.key: + credentials_ok = False + elif not is_string(context.url): + message = "DOcplexcloud: URL is not a string: {0!s}".format(context.url) + credentials_ok = False + elif not is_string(context.key): + message = "API key is not a string: {0!s}".format(context.key) + credentials_ok = False + if context.key and credentials_ok: + credentials_ok = isinstance(context.key, str) + return credentials_ok, message + + +def has_credentials(context): + # Checks if the context has valid credentials. + # + # Returns: + # True if the context has valid credentials. + # ignore message + has_credentials_, _ = check_credentials(context) + return has_credentials_ + + +def print_context(context): + # prints the context. + def print_r(node, prefix): + for n in sorted(node): + if not n.startswith('_'): + path = ".".join([prefix, n] if prefix else [n]) + if isinstance(node.get(n), (dict, SolverContext)): + print("%s # type: %s" % (path, type(node.get(n)).__name__)) + print_r(node.get(n), path) + else: + print("%s = %s # type: %s" % (path, node.get(n), type(node.get(n)).__name__)) + + print_r(context, "context") + + +
[docs]class BaseContext(dict): + # Class for handling the list of parameters. + + def __init__(self, **kwargs): + """ Create a new context. + + Args: + List of ``key=value`` to initialize context with. + """ + super(BaseContext, self).__init__() + for k, v in kwargs.items(): + self.set_attribute(k, v) + + def __setattr__(self, name, value): + self.set_attribute(name, value) + + def __getattr__(self, name): + return self.get_attribute(name) + + def set_attribute(self, name, value): + self[name] = value + + def get_attribute(self, name, default=None): + if name.startswith('__'): + raise AttributeError + res = self.get(name, default) + return res + + def display(self): + # prints the context. + def print_r(node, prefix): + for n in sorted(node): + if not n.startswith('_'): + path = ".".join([prefix, n] if prefix else [n]) + if isinstance(node.get(n), (dict, SolverContext)): + print("%s # type: %s" % (path, type(node.get(n)).__name__)) + print_r(node.get(n), path) + else: + print("%s = %s # type: %s" % (path, node.get(n), type(node.get(n)).__name__)) + + print_r(self, "context")
+ + +
[docs]class SolverContext(BaseContext): + # for internal use + + def __init__(self, **kwargs): + super(SolverContext, self).__init__(**kwargs) + self.log_output = False + self.max_threads = get_environment().get_available_core_count() + self.auto_publish = create_default_auto_publish_context() + self.kpi_reporting = BaseContext() + from docplex.mp.progress import ProgressClock + self.kpi_reporting.filter_level = ProgressClock.Gap + + def __deepcopy__(self, memo): + # We override deepcopy here just to make sure that we don't deepcopy + # file descriptors... + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.items(): + # do not duplicate those (io like objects) + if k == "log_output" and hasattr(v, "write"): + value = v + else: + value = deepcopy(v, memo) + setattr(result, k, value) + return result + + @property + def log_output_as_stream(self): + try: + log_output = self.log_output + except AttributeError: # pragma: no cover + return None + + output_stream = None + # if log_output is an object with a lower attribute, let's use it + # as string.lower and check for some known string values + if hasattr(log_output, "lower"): + k = log_output.lower() + if k in _boolean_map: + if _convert_to_bool(k): + output_stream = sys.stdout + else: + output_stream = None + elif k in ["stdout", "sys.stdout"]: + output_stream = sys.stdout + elif k in ["stderr", "sys.stderr"]: + output_stream = sys.stderr + else: + output_stream = open(log_output, 'w') + output_stream = StreamWithCustomClose(output_stream) + # if log_output is == to True, just use stdout + if log_output is True: + output_stream = sys.stdout + # if it has a write() attribute, just return it + elif hasattr(log_output, "write"): + output_stream = log_output + + return output_stream
+ + +
[docs]class Context(BaseContext): + """ The context used to control the behavior of solve engine. + + Attributes: + cplex_parameters: A + :class:`docplex.mp.params.parameters.RootParameterGroup` to store + CPLEX parameters. + solver.auto_publish: If ``True``, a model being solved will automatically + publish all publishable items (``solve_details``, + ``result_output``, ``kpis_output``). + solver.auto_publish.solve_details: if ``True``, solve details are + published automatically. + solver.auto_publish.result_output: if not None, the filename where + solution is saved. This can be a list of filenames if multiple + solutions are to be published. If True, ``solution.json`` is used. + solver.auto_publish.kpis_output: if not None, the filename where KPIs + are saved as a table with KPI name and values. Currently only + csv files are supported. This can be a list of filenames if + multiple KPIs files are to be published. + context.solver.auto_publish.kpis_output_field_name: Name of field for KPI names in KPI output + table. Defaults to 'Name' + context.solver.auto_publish.kpis_output_field_value: Name of field for KPI values for KPI output + table. Defaults to 'Value' + solver.log_output: This attribute can have the following values: + + * True: When True, logs are printed to sys.out. + * False: When False, logs are not printed. + * A file-type object: Logs are printed to that file-type object. + + solver.kpi_reporting.filter_level: Specify the filtering level for kpi reporting. + If None, no filtering is done. Can take values of + docplex.mp.progress.KpiFilterLevel or a string representation of + one of the values of this enum (Unfiltered, FilterObjectiveAndBound, + FilterObjective) + """ + + def __init__(self, **kwargs): + # store env used for initialization + self['_env_at_init'] = kwargs.get('_env') + # map lazy members to f(model) (actually f(self) ) returning the + # initial value + self['_lazy_members'] = \ + {'cplex_parameters': init_cplex_parameters} + # initialize fields + if 'solver_context' in kwargs: + solver_context = kwargs.pop('solver_context') + else: + solver_context = SolverContext() #create_default_auto_publish_context(defaults=False) #Context.make_default_context() + + super(Context, self).__init__(solver=solver_context, + cos=BaseContext(), + docplex_tests=BaseContext()) + # update will also ensure compatibility with older kwargs like + # 'url' and 'api_key' + self.update(kwargs, create_missing_nodes=True) + + self.model_build_hook = None + + def init_cplex_parameters(self): + # we need a local import here so that docplex.mp.environment + # does not depend on context + from docplex.mp.environment import Environment + local_env = self.get('_env_at_init') or Environment.get_default_env() + cplex_version = local_env.cplex_version + local_env.check_cplex_version() + cplex_parameters = get_params_from_cplex_version(cplex_version) + return cplex_parameters + + def __getattr__(self, name): + if name not in self: + lazy_members = self.get('_lazy_members') + if lazy_members and name in lazy_members: + evaluated = lazy_members[name](self) + self[name] = evaluated + return self.get_attribute(name) + + def _get_raw_cplex_parameters(self): + # NON LAZY: may return None + return self.get('cplex_parameters') + +
[docs] @staticmethod + def make_default_context(file_list=None, logger=None, **kwargs): + """Creates a default context. + + If `file_list` is a string, then it is considered to be the name + of a config file to be read. + + If `file_list` is a list, it is considered to be a list of names + of a config files to be read. + + if `file_list` is None or not specified, the following files are + read if they exist: + + * the PYTHONPATH is searched for the following files: + + * cplex_config.py + * cplex_config_<hostname>.py + + Args: + file_list: The list of config files to read. + kwargs: context parameters to override. See :func:`docplex.mp.context.Context.update` + """ + context = Context(**kwargs) + context.read_settings(file_list=file_list, logger=logger) + if 'DOCPLEX_CONTEXT' in os.environ: + values_pairs = [] + for v in shlex.split(os.environ['DOCPLEX_CONTEXT']): + s = v.split('=', 1) # max 1 split + # convert values to bool if relevant + value = _boolean_map.get(s[1].strip().lower(), s[1]) + values_pairs.append((s[0], value)) + if logger: + logger.info('Setting context value %s to %s (from DOCPLEX_CONTEXT env)' % (s[0], value)) + context.update_from_list(values_pairs, logger=logger) + return context
+ +
[docs] def copy(self): + # Makes a deep copy of the context. + # + # Returns: + # A deep copy of the context. + return deepcopy(self)
+ + def clone(self): + # Makes a deep copy of the context. + # + # Returns: + # A deep copy of the context. + return deepcopy(self) + + def override(self): + return ContextOverride(self) + + def update_from_list(self, values, logger=None): + # For each pair of `(name, value)` in values, try to set the + # attribute. + for name, value in values: + try: + self._set_value(self, name, value) + except AttributeError: + if logger is not None: + logger.warning("Ignoring undefined attribute : {0}".format(name)) + + def _set_value(self, root, property_spec, property_value): + property_list = property_spec.split('.') + property_chain = property_list[:-1] + to_be_set = property_list[-1] + o = root + for c in property_chain: + o = getattr(o, c) + try: + target_attribute = getattr(o, to_be_set) + except AttributeError: + target_attribute = None + if target_attribute is None: + # Simply set the attribute + try: + setattr(o, to_be_set, property_value) + except DOcplexException: + pass # ignore this + else: + # try a set_converted_value if it's a Parameter + try: + target_attribute.set(property_value) + except (AttributeError, TypeError): + # no set(), just setattr + setattr(o, to_be_set, property_value) + +
[docs] def update(self, kwargs, create_missing_nodes=False): + """ Updates this context from child parameters specified in ``kwargs``. + + The following keys are recognized: + + - cplex_parameters: A set of CPLEX parameters to use instead of the parameters defined as ``context.cplex_parameters``. + - agent: Changes the ``context.solver.agent`` parameter. + Supported agents include: + - ``local``: forces the solve operation to use native CPLEX + + - log_output: if ``True``, solver logs are output to stdout. + If this is a stream, solver logs are output to that stream object. + Overwrites the ``context.solver.log_output`` parameter. + + Args: + kwargs: A ``dict`` containing keyword args to use to update this context. + create_missing_nodes: When a keyword arg specify a parameter that is not already member of this context, + creates the parameter if ``create_missing_nodes`` is True. + + """ + for k in kwargs: + value = kwargs.get(k) + if value is not None: + self.update_key_value(k, value, + create_missing_nodes=create_missing_nodes)
+ + cplex_parameters_key = "cplex_parameters" + + def update_cplex_parameters(self, arg_params): + # INTERNAL + if isinstance(arg_params, RootParameterGroup): + self.cplex_parameters = arg_params + else: + new_params = self.cplex_parameters.copy() + # try a dictionary of parameter qualified names, parameter values + # e.g. cplex_parameters={'mip.tolerances.mipgap': 0.01, 'timelimit': 180} + try: + for pk, pv in arg_params.items(): + p = new_params.find_parameter(key=pk) + if not p: + docplex_fatal('Cannot find matching parameter from: {0!r}'.format(pk)) + else: + p.set(pv) + self.cplex_parameters = new_params + + except (TypeError, AttributeError): + docplex_fatal('Expecting CPLEX parameters or dict, got: {0!r}'.format(arg_params)) + + def update_key_value(self, k, value, create_missing_nodes=False, warn=True): + if k == 'cplex_parameters': + if isinstance(value, RootParameterGroup): + self.cplex_parameters = value + else: + self.update_cplex_parameters(value) + elif k == 'log_output': + self.solver.log_output = value + elif k == 'override': + self.update_from_list(value.items()) + elif k == '_env': + # do nothing this is just here to avoid creating too many envs + pass + elif k == 'agent': + self.solver.agent = value + else: + if create_missing_nodes: + self[k] = value + elif warn: + warnings.warn("Unknown quick-setting in Context: {0:s}, value: {1!s}".format(k, value), + stacklevel=2) + +
[docs] def read_settings(self, file_list=None, logger=None): + """Reads settings for a list of files. + + If `file_list` is a string, then it is considered to be the name + of a config file to be read. + + If `file_list` is a list, it is considered to be a list of names + of config files to be read. + + if `file_list` is None or not specified, the following files are + read if they exist: + + * the PYTHONPATH is searched for the following files: + + * cplex_config.py + * cplex_config_<hostname>.py + + Args: + file_list: The list of config files to read. + + Raises: + InvalidSettingsFileError: If an error occurs while reading a config + file. *(Since version 2.8)* + """ + if file_list is None: + file_list = [] + targets = ['cplex_config.py', + 'cplex_config_{0}.py'.format(socket.gethostname()) + ] + for target in targets: + if isabs(target) and isfile(target) and target not in file_list: + file_list.append(target) + else: + for d in sys.path: + f = os.path.join(d, target) + if os.path.isfile(f): + abs_name = os.path.abspath(f) + if abs_name not in file_list: + file_list.append(f) + + if len(file_list) == 0: + file_list = None # let read_settings use its default behavior + + if isinstance(file_list, str): + file_list = [file_list] + + if file_list is not None: + for f in file_list: + if os.path.isfile(f): + if logger: + logger.info("Reading settings from %s" % f) + if f.endswith(".py"): + self.read_from_python_file(f)
+ + def read_from_python_file(self, filename): + # Evaluates the content of a Python file containing code to set up a + # context. + # + # Args: + # filename (str): The name of the file to evaluate. + try: + if os.path.isfile(filename): + with open_universal_newline(filename, 'r') as f: + l = {'context': self, + '__file__': os.path.abspath(filename)} + exec(f.read(), globals(), l) + except Exception as exc: + raise InvalidSettingsFileError('Error occured while reading file: %s' % filename, + filename=filename, source=exc) + return self
+ + +def create_default_auto_publish_context(defaults=True): + auto_publish = BaseContext() + # in a future version, we might want to be able to set individual + # default values with a dict => that's why we compare to True and False + if defaults is True and get_solve_hook: + # Set default values when in a worker + auto_publish.solve_details = True + auto_publish.result_output = 'solution.json' + auto_publish.kpis_output = 'kpis.csv' + auto_publish.kpis_output_field_name = 'Name' + auto_publish.kpis_output_field_value = 'Value' + auto_publish.relaxations_output = 'relaxations.csv' + auto_publish.conflicts_output = 'conflicts.csv' + else: + auto_publish.solve_details = False + auto_publish.result_output = None + auto_publish.kpis_output = None + auto_publish.kpis_output_field_name = None + auto_publish.kpis_output_field_value = None + auto_publish.relaxations_output = None + auto_publish.conflicts_output = None + return auto_publish + + +
[docs]class ContextOverride(Context): + + def __init__(self, initial_context): + soc2 = deepcopy(initial_context.solver) + super(ContextOverride, self).__init__(solver_context=soc2) + self._initial_context = initial_context # unchanged + self.cplex_parameters = initial_context.cplex_parameters + + # @property + # def solver(self): + # if self._solver is None: + # self._solver = deepcopy(self._initial_context.solver) + # return self._solver + + def update_key_value(self, k, value, create_missing_nodes=False, warn=True): + if k == 'cplex_parameters': + if isinstance(value, RootParameterGroup): + self.cplex_parameters = value + else: + self.update_cplex_parameters(value) + elif k == 'time_limit': + time_limit_failed = True + try: + time_limit = int(value) + if time_limit >= 0: + # this method makes a local copy for the duration of solve() + self.update_cplex_parameters({"timelimit": time_limit}) + time_limit_failed = False + + except ValueError: + pass + if time_limit_failed: + print("Invalid time limit: {0!r} - ignored".format(value)) + elif k == 'log_output': + self.solver.log_output = value + elif k == 'override': + self.update_from_list(value.items()) + elif k == '_env': + # do nothing this is just here to avoid creating too many envs + pass + elif k == 'agent': + self.solver.agent = value + else: + if create_missing_nodes: + self.k = value + elif warn: + warnings.warn("Unknown quick-setting in Context: {0:s}, value: {1!s}".format(k, value), + stacklevel=2) + + def update_cplex_parameters(self, arg_params): + # INTERNAL + new_params = self.cplex_parameters.copy() + # try a dictionary of parameter qualified names, parameter values + # e.g. cplex_parameters={'mip.tolerances.mipgap': 0.01, 'timelimit': 180} + try: + for pk, pv in arg_params.items(): + p = new_params.find_parameter(key=pk) + if not p: + docplex_fatal('Cannot find matching parameter from: {0!r}'.format(pk)) + else: + p.set(pv) + self.cplex_parameters = new_params + + except (TypeError, AttributeError): + docplex_fatal('Expecting CPLEX parameters or dict, got: {0!r}'.format(arg_params)) + + def update_from_list(self, key_value_pairs, logger=None): + # For each pair of `(name, value)` in values, try to set the + # attribute. + for name, value in key_value_pairs: + try: + self._set_value(self, name, value) + except AttributeError: + if logger is not None: + logger.warning("Ignoring undefined attribute : {0}".format(name)) + + def _set_value(self, root, property_spec, property_value): + property_list = property_spec.split('.') + property_chain = property_list[:-1] + to_be_set = property_list[-1] + o = root + for c in property_chain: + o = getattr(o, c) + try: + target_attribute = getattr(o, to_be_set) + except AttributeError: + target_attribute = None + if target_attribute is None: + # Simply set the attribute + try: + setattr(o, to_be_set, property_value) + except DOcplexException: + print('attribute not found: {0}'.format(to_be_set)) + else: + # try a set_converted_value if it's a Parameter + try: + target_attribute.set(property_value) + except (AttributeError, TypeError): + # no set(), just setattr + setattr(o, to_be_set, property_value)
+ + +class OverridenOutputContext(object): + + def __init__(self, mdl, stream): + self._model = mdl + self._stream = stream + self._saved_context_log_output = mdl.context.solver.log_output + self._saved_log_output_stream = mdl.log_output + + def __enter__(self): + mdl = self._model + # change stream + mdl.set_log_output(self._stream) + return mdl + + def __exit__(self, exc_type, exc_val, exc_tb): + mdl = self._model + log_stream = mdl.log_output + if log_stream: + try: + log_stream.flush() + except AttributeError: + pass + try: + log_stream.custom_close() + except AttributeError: + pass + + saved_log_output_stream = self._saved_log_output_stream + saved_context_log_output = self._saved_context_log_output + if self._saved_log_output_stream != mdl.log_output: + mdl.set_log_output_as_stream(saved_log_output_stream) + if saved_context_log_output != mdl.context.solver.log_output: + mdl.context.solver.log_output = saved_context_log_output +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/dvar.html b/docs/2.24.232/mp/_modules/docplex/mp/dvar.html new file mode 100644 index 0000000..edf2e6c --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/dvar.html @@ -0,0 +1,762 @@ + + + + + + + + + docplex.mp.dvar — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.dvar

+
+from docplex.mp.constants import CplexScope
+
+from docplex.mp.basic import IndexableObject, _AbstractBendersAnnotated, _AbstractValuable
+from docplex.mp.operand import LinearOperand
+from docplex.mp.utils import is_number, is_quad_expr
+
+from docplex.mp.sttck import StaticTypeChecker
+
+
+
[docs]class Var(IndexableObject, LinearOperand, _AbstractBendersAnnotated, _AbstractValuable): + """Var() + + This class models decision variables. + Decision variables are instantiated by :class:`docplex.mp.model.Model` methods such as :func:`docplex.mp.model.Model.var`. + + """ + + __slots__ = ('_vartype', '_lb', '_ub') + + def __init__(self, model, vartype, name, + lb=None, ub=None, + _safe_lb=False, _safe_ub=False): + IndexableObject.__init__(self, model, name) + self._vartype = vartype + + if _safe_lb: + #assert lb is not None + self._lb = lb + else: + self._lb = vartype._compute_lb(lb, model) + if _safe_ub: + #assert ub is not None + self._ub = ub + else: + self._ub = vartype._compute_ub(ub, model) + + @property + def cplex_scope(self): + return CplexScope.VAR_SCOPE + + # noinspection PyUnusedLocal + def copy(self, new_model, var_mapping): + return var_mapping[self] + + relaxed_copy = copy + + # linear operand api + + def as_variable(self): + return self + + def iter_terms(self): + yield self, 1 + + def clone(self): + return self + + def negate(self): + return self.lfactory._new_monomial_expr(self, -1, safe=True) + + iter_sorted_terms = iter_terms + + def number_of_variables(self): + return 1 + def number_of_terms(self): + return 1 + + def unchecked_get_coef(self, dvar): + return 1 if dvar is self else 0 + + def contains_var(self, dvar): + return self is dvar + + def accepts_value(self, candidate_value, tolerance=1e-6): + # INTERNAL + return self.vartype.accept_domain_value(candidate_value, lb=self._lb, ub=self._ub, tolerance=tolerance) + + def check_name(self, new_name): + self.check_lp_name(qualifier='variable', new_name=new_name, accept_empty=False, accept_none=False) + + def __hash__(self): + return self._index + + def set_name(self, new_name): + # INTERNAL + self.check_name(new_name) + self.model.set_var_name(self, new_name) + + @property + def name(self): + return self._name + + @name.setter + def name(self, new_name): + self.set_name(new_name) + + @property + def lb(self): + """ This property is used to get or set the lower bound of the variable. + + Possible values for the lower bound depend on the variable type. Binary variables + accept only 0 or 1 as bounds. An integer variable will convert the lower bound value to the + ceiling integer value of the argument. + """ + return self._lb + + @lb.setter + def lb(self, new_lb): + self.set_lb(new_lb) + + def set_lb(self, lb): + if lb != self._lb: + self._model.set_var_lb(self, lb) + return self._lb + + def _internal_set_lb(self, lb): + # Internal, used only by the model + self._lb = lb + + def _internal_set_ub(self, ub): + # INTERNAL + self._ub = ub + + @property + def ub(self): + """ This property is used to get or set the upper bound of the variable. + + Possible values for the upper bound depend on the variable type. Binary variables + accept only 0 or 1 as bounds. An integer variable will convert the upper bound value to the + floor integer value of the argument. + + To reset the upper bound to its default infinity value, use :func:`docplex.mp.model.Model.infinity`. + """ + return self._ub + + @ub.setter + def ub(self, new_ub): + self.set_ub(new_ub) + + def set_ub(self, ub): + if ub != self._ub: + self._model.set_var_ub(self, ub) + return self._ub + + def has_free_lb(self): + return self.lb <= - self._model.infinity + + def has_free_ub(self): + return self.ub >= self._model.infinity + + def is_free(self): + return self.has_free_lb() and self.has_free_ub() + + def _reset_bounds(self): + vartype = self._vartype + vtype_lb, vtype_ub = vartype.default_lb, vartype.default_ub + self.set_lb(vtype_lb) + self.set_ub(vtype_ub) + + @property + def vartype(self): + """ This property returns the variable type, an instance of :class:`VarType`. + + """ + return self._vartype + + def set_vartype(self, new_vartype): + # INTERNAL + self._model._set_var_type(self, new_vartype) + + def _set_vartype_internal(self, new_vartype): + # INTERNAL + self._vartype = new_vartype + + def _has_type(self, vartype_code): + # internal + return self.cplex_typecode == vartype_code + +
[docs] def is_binary(self): + """ Checks if the variable is binary. + + Returns: + Boolean: True if the variable is of type Binary. + """ + return self._has_type('B')
+ +
[docs] def is_integer(self): + """ Checks if the variable is integer. + + Returns: + Boolean: True if the variable is of type Integer. + """ + return self._has_type('I')
+ +
[docs] def is_continuous(self): + """ Checks if the variable is continuous. + + Returns: + Boolean: True if the variable is of type Continuous. + """ + return self._has_type('C')
+ +
[docs] def is_discrete(self): + """ Checks if the variable is discrete. + + Returns: + Boolean: True if the variable is of type Binary or Integer. + """ + return self._vartype.is_discrete()
+ + @property + def float_precision(self): + return 0 if self.is_discrete() else self._model.float_precision + + def get_value(self): + # for compatibility only: use solution_value instead + print("* get_value() is deprecated, use property solution_value instead") # pragma: no cover + return self.solution_value # pragma: no cover + + @property + def solution_value(self): + """ This property returns the solution value of the variable. + + Raises: + DOCplexException + if the model has not been solved succesfully. + + """ + self.model._check_has_solution() + return self._raw_solution_value() + + @property + def sv(self): + """ Same as `solution_value` but shorter""" + return super().sv + + def _raw_solution_value(self, s=None): + sol = s or self.model._get_solution() + return sol._get_var_value(self) + +
[docs] def get_key(self): + """ Returns the key used to create the variable, or None. + + When the variable is part of a list or dictionary of variables created from a sequence of keys, + returns the key associated with the variable. + + Example: + xs = m.continuous_var_dict(keys=['a', 'b', 'c']) + xa = xs['a'] + assert xa.get_key() == 'a' + + :return: + a Python object, possibly None. + """ + container = self.container + return container.get_var_key(self) if container else None
+ + def __mul__(self, e): + return self.times(e) + + @classmethod + def is_zero_op(cls, other): + try: + return other.is_zero() + except AttributeError: + return False + + def times(self, e): + if is_number(e): + return self.lfactory._new_monomial_expr(dvar=self, coeff=e, safe=False) + + elif self.is_zero_op(e): + return self.lfactory.new_zero_expr() + elif isinstance(e, LinearOperand): + return self._model._qfactory.new_var_product(self, e) + else: + return self.to_linear_expr().multiply(e) + + def __rmul__(self, e): + return self.times(e) + + def __add__(self, e): + return self.plus(e) + + @staticmethod + def _extract_calling_ct_xhs(): + _searched_patterns = [("lhs", 0), ("left_expr", 0), ("rhs", 1), ("right_expr", 1)] + import inspect + # need to get 2 steps higher to find caller to add/sub + frame = inspect.stack()[2] + code_context = frame.code_context # if PY3 else frame[4] + + def find_in_line(line_): + for xhs_s, xhs_p in _searched_patterns: + if xhs_s in line_: + return line_.find(xhs_s), xhs_p + + return -1, -1 + + if code_context: + line = code_context[0] + if line: + spos, lr = find_in_line(line) + + if spos > 1: + assert lr >= 0 + # strip whitespace before code... + ct_varname = line[:spos - 1].lstrip() + # evaluate ct in caller locals dict + subframe = frame.frame # if PY3 else frame[0] + ct_object = subframe.f_locals.get(ct_varname) + # returns a constraint (or None if that fails), plus 0-1 (0 for lhs, 1 for rhs) + return ct_object, lr + return None, -1 + + # def _perform_arithmetic_to_self(self, self_arithmetic_method, e): + # # INTERNAL + # res = self_arithmetic_method(e) + # ct, xhs_pos = self._extract_calling_ct_xhs() + # if ct is not None: + # self.get_linear_factory().set_linear_constraint_expr_from_pos(ct, xhs_pos, res) + # return res + + def add(self, e): + res = self.plus(e) + ct, xhs_pos = self._extract_calling_ct_xhs() + if ct is not None: + self.lfactory.set_linear_constraint_expr_from_pos(ct, xhs_pos, res) + return res + + def subtract(self, e): + res = self.minus(e) + ct, xhs_pos = self._extract_calling_ct_xhs() + if ct is not None: + self.lfactory.set_linear_constraint_expr_from_pos(ct, xhs_pos, res) + return res + + def plus(self, e): + if isinstance(e, Var): + expr = self._make_linear_expr() + expr._add_term(e) + return expr + + elif is_number(e): + return self._make_linear_expr(constant=e, safe=False) + elif is_quad_expr(e): + return e.plus(self) + else: + return self.to_linear_expr().add(e) + + def to_linear_expr(self): + # INTERNAL + return self._make_linear_expr() + + def _make_linear_expr(self, constant=0, safe=True): + return self.lfactory.linear_expr(self, constant, safe=safe, transient=True) + + def __radd__(self, e): + return self.plus(e) + + def __sub__(self, e): + return self.minus(e) + + def minus(self, e): + if isinstance(e, LinearOperand): + return self.to_linear_expr().subtract(e) + + elif is_number(e): + # v -k -> expression(v,-1) -k + return self._make_linear_expr(constant=-e, safe=False) + + elif is_quad_expr(e): + return e.rminus(self) + else: + return self.to_linear_expr().subtract(e) + + def __rsub__(self, e): + + expr = self.lfactory._to_linear_operand(e, force_clone=True) # makes a clone. + return expr.subtract(self) + + def divide(self, e): + return self.to_linear_expr().divide(e) + + def __div__(self, e): + return self.divide(e) + + def __truediv__(self, e): + # for py3 + # INTERNAL + return self.divide(e) # pragma: no cover + + def __rtruediv__(self, e): + # for py3 + self.fatal("Variable {0!s} cannot be used as denominator of {1!s}", self, e) # pragma: no cover + + def __rdiv__(self, e): + self.fatal("Variable {0!s} cannot be used as denominator of {1!s}", self, e) + + def __pos__(self): + # the "+e" unary plus is syntactic sugar + return self + + def __neg__(self): + # the "-e" unary minus returns a linear expression + return self.negate() + + def __pow__(self, power): + # INTERNAL + if 0 == power: + return 1 + elif 1 == power: + return self + elif 2 == power: + return self.square() + else: + self.model.unsupported_power_error(self, power) + + def __rshift__(self, other): + """ Redefines the right-shift operator to create indicators. + + This operator allows to create indicators with the `>>` operator. + It expects a linear constraint as second argument. + + :param other: a linear constraint used to create the indicator + + :return: + an instance of IndicatorConstraint, that is not added to the model. + Use `Model.add()` to add it to the model. + + Note: + The variable must be binary, otherwise an exception is raised. + + Example: + + >>> m.add(b >> (x >=3) + + creates an indicator which links the satisfaction of the constraint (x >= 3) + to the value of binary variable b. + """ + return self._model.indicator_constraint(self, other) + + def square(self): + return self._model._qfactory.new_var_square(self) + + def __int__(self): + """ Converts a decision variable to a integer number. + + This is only possible for discrete variables, + and when the model has been solved successfully. + If the model has been solved, returns the variable's solution value. + + Returns: + int: The variable's solution value. + + Raises: + DOCplexException + if the model has not been solved successfully. + DOCplexException + if the variable is not discrete. + """ + + if self.is_continuous(): + self.fatal("Cannot convert continuous variable value to int: {0!s}", self) + return int(self.solution_value) + + def __float__(self): + """ Converts a decision variable to a floating-point number. + + This is only possible when the model has been solved successfully, + otherwise an exception is raised. + If the model has been solved, it returns the variable's solution value. + + Returns: + float: The variable's solution value. + Raises: + DOCplexException + if the model has not been solved successfully. + """ + return float(self.solution_value) + +
[docs] def to_bool(self, precision=1e-6): + """ Converts a variable value to True or False. + + Assuming the variable is discrete (integer or binary), returns True if the variable value is + set a non-zero integer, taking into account precision. + For binary variables, returns TRue if the variable value equals 1, taking into account precision. + + Raises: + DOCplexException + if the model has not been solved successfully. + DOCplexException + if the variable is not discrete + + Returns: + Boolean: True if the variable value is nonzero, else False. + """ + if not self.is_discrete(): + self.fatal("boolean conversion only for discrete variables, type is {0!s}", self.vartype) + value = self.solution_value # this property checks for a solution. + return abs(value) >= precision
+ + def __str__(self): + """ + Returns: + string: A string representation of the variable. + + """ + return self.to_string() + + def to_string(self, use_space=False): + return self.lp_name + + @property + def lp_name(self): + return self._name or "x%s" % self.index1 + + @property + def lpt_name(self): + # 'c1', 'b2', 'i3' ... first letter of cplex code + index + cpx_typecode = self.cplex_typecode.lower() + radix = 'x' if cpx_typecode == 'c' else cpx_typecode[0] + return "%s%d" % (radix, self.index1) + + @property + def cplex_typecode(self): + return self._vartype.cplex_typecode + + def _must_print_lb(self): + return self.cplex_typecode not in 'SN' and self.lb == self._vartype.default_lb + + def __repr__(self): + self_vartype, self_lb, self_ub = self._vartype, self.lb, self.ub + # print lb for semi-xx + if self._must_print_lb(): + repr_lb = '' + else: + repr_lb = ',lb={0:g}'.format(self_lb) + if self_vartype.default_ub == self_ub: + repr_ub = '' + else: + repr_ub = ',ub={0:g}'.format(self_ub) + if self.has_name(): + repr_name = ",name='{0}'".format(self.name) + else: + repr_name = '' + cpxc = self.cplex_typecode + return "docplex.mp.Var(type={0}{1}{2}{3})". \ + format(cpxc, repr_name, repr_lb, repr_ub) + + @property + def reduced_cost(self): + """ Returns the reduced cost of the variable. + + This method will raise an exception if the model has not been solved as a LP. + + Note: + For a large number of variables (> 100), using the `Model.reduced_costs()` method can be much faster. + + Returns: + The reduced cost of the variable (a float value). + + See Also: + + :func:`docplex.mp.model.Model.reduced_costs` + """ + return self._model._reduced_cost1(self) + + @property + def basis_status(self): + """ This property returns the basis status of the variable, if any. + The variable must be continuous, otherwise an exception is raised. + + Returns: + An enumerated value from the enumerated type `docplex.constants.BasisStatus`. + + Note: + for the model to hold basis information, the model must have been solved as a LP problem. + In some cases, a model which failed to solve may still have a basis available. Use + `Model.has_basis()` to check whether the model has basis information or not. + + See Also: + :func:`docplex.mp.model.Model.has_basis` + :class:`docplex.mp.constants.BasisStatus` + + """ + if not self.is_continuous(): + self.fatal("Basis status is for continuous variables, {0!s} has type {1!s}", self, self.vartype.short_name) + return self._model._var_basis_status1(self) + + @property + def benders_annotation(self): + """ + This property is used to get or set the Benders annotation of a variable. + The value of the annotation must be a positive integer + + """ + return self.get_benders_annotation() + + @benders_annotation.setter + def benders_annotation(self, new_anno): + self.set_benders_annotation(new_anno) + +
[docs] def iter_constraints(self): + """ Returns an iterator traversing all constraints in which the variable is used. + + :return: + An iterator. + """ + for ct in self._model.iter_constraints(): + for ctv in ct.iter_variables(): + if ctv is self: + yield ct + break
+ +
[docs] def equals(self, other): + """ + This method is used to test equality to an expression. + Because of the overloading of operator `==` through the redefinition of + the `__eq__` method, you cannot use `==` to test for equality. + In order to test that two decision variables ar ethe same, use th` Python `is` operator; + use the `equals` method to test whether a given expression is equivalent to a variable: + for example, calling `equals` with a linear expression which consists of this variable only, + with a coefficient of 1, returns True. + + Args: + other: an expression or a variable. + + :return: + A boolean value, True if the passed variable is this very variable, or + if the passed expression is equivalent to the variable, else False. + + """ + # noinspection PyPep8 + return self is other or \ + (isinstance(other, LinearOperand) and + other.get_constant() == 0 and + other.number_of_terms() == 1 and + other.unchecked_get_coef(self) == 1)
+ + def as_logical_operand(self): + # INTERNAL + return self if self.is_binary() else None + + def _check_binary_variable_for_logical_op(self, op_name): + if not self.is_binary(): + self.fatal("Logical {0} is available only for binary variables, {1} has type {2}", + op_name, self, self.vartype.short_name) + + def logical_and(self, other): + self._check_binary_variable_for_logical_op(op_name="and") + StaticTypeChecker.typecheck_logical_op(self, other, caller="Var.logical_and") + return self.lfactory.new_logical_and_expr([self, other]) + + def logical_or(self, other): + self._check_binary_variable_for_logical_op(op_name="or") + StaticTypeChecker.typecheck_logical_op(self, other, caller="Var.logical_or") + return self.lfactory.new_logical_or_expr([self, other]) + + def logical_not(self): + self._check_binary_variable_for_logical_op(op_name="not") + return self.lfactory.new_logical_not_expr(self) + + def __and__(self, other): + return self.logical_and(other) + + def __or__(self, other): + return self.logical_or(other)
+ + # no unary not in magic methods... + + +def is_var(x): + return isinstance(x, Var) +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/environment.html b/docs/2.24.232/mp/_modules/docplex/mp/environment.html new file mode 100644 index 0000000..630fe75 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/environment.html @@ -0,0 +1,556 @@ + + + + + + + + + docplex.mp.environment — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.environment

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+'''Provides utility functions about the runtime environment.
+
+You can display information about your runtime environment using::
+
+    $ python
+    >>> from docplex.mp.environment import Environment
+    >>> Environment().print_information()
+
+or by invoking the `docplex.mp.environment` package on your shell command line::
+
+    $ python -m docplex.mp.environment
+    * system is: Linux 64bit
+    * Python version 3.6.1, located at: /usr/bin/python
+    * docplex is present, version is (2, 9, 0)
+    * CPLEX library is present, version is 12.9.0.0, located at: /usr/local/CPLEX_Studio129/cplex/python/3.6/x86-64_linux
+'''
+try:
+    import importlib.util as importlib_util
+except ImportError:  # pragma: no cover
+    importlib_util = None  # Python 2
+import platform
+import os
+import sys
+import warnings
+
+from docplex.mp.error_handler import docplex_fatal
+
+min_cplex_major = 12
+min_cplex_minor = 8
+
+
+def env_is_64_bit():
+    return sys.maxsize > 2 ** 32
+
+
+
[docs]class UnsupportedPlatformError(Exception): + pass
+ + +# maps paths to modules for already loaded cplexes +# for path == None, key is "__NONE__" +_loaded_cplexes = {} + + +# noinspection PyPep8 +
[docs]class Environment(object): + """ This class detects and contains information regarding other modules of interest, such as + whether CPLEX, `numpy`, and `matplotlib` are installed. + """ + _default_env = None # The default env singleton + + """ This class detects and contains information regarding other modules of interest, such as + whether CPLEX, `numpy`, and `matplotlib` are installed. + """ + def __init__(self, start_auto_configure=True, logger=None): + """ + __init__(self) + """ + self._found_cplex = False + self._cplex_version = '' + self._cplex_location = None + + self._found_numpy = None + self._numpy_version = None + self._numpy_hook = None + + self._found_pandas = None + self._pandas_version = '' + self._found_matplotlib = None + self._matplotlib_version = None + + self._python_version = platform.python_version() + self._system = platform.system() + self._machine = platform.machine() + self._bitness = platform.architecture()[0] + self._is64bit = sys.maxsize > 2 ** 32 + + if start_auto_configure: + self.auto_configure(logger=logger) + + # class variable + # Must be true if python versin is > 3.6 + env_is_python36 = (sys.version_info.major >= 3) and (sys.version_info.minor >= 6) + + def _get_numpy_hook(self): + return self._numpy_hook + + def _set_numpy_hook(self, hook): + self._numpy_hook = hook + if hook is not None: + if self.has_numpy: # now that we have set a hook, do check for numpy + hook() # if numpy is present, do call the hook + + numpy_hook = property(_get_numpy_hook, _set_numpy_hook) + + def equals(self, other): + if type(other) != Environment: + return False + if self.has_cplex != other.has_cplex: + return False + if self.cplex_version != other.cplex_version: + return False + + if self.has_numpy != other.has_numpy: + return False + if self.has_matplotlib != other.has_matplotlib: + return False + if self.has_pandas != other.has_pandas: + return False + + return True + + @property + def has_cplex(self): + """True if the CPLEX libraries are available. + + The cplex libraries search order is: + + - import the module `import cplex` if the import is sucessful + - import the module in location $CPLEX_STUDIO_DIR1210/cplex/python/<python.version>/<platform> + - import the module in location $CPLEX_STUDIO_DIR129/cplex/python/<python.version>/<platform> + - import the module in location $CPLEX_STUDIO_DIR128/cplex/python/<python.version>/<platform> + """ + return self._found_cplex + + def hash_cplex_with_version_min(self, min_version): + return self.has_cplex and self._cplex_version >= min_version + + def check_cplex_version(self): + cpx_version = self.cplex_version_as_tuple + + if self.has_cplex and cpx_version < (min_cplex_major, min_cplex_minor): + s_min_version = "{0}.{1}".format(min_cplex_major, min_cplex_minor) + docplex_fatal("DOcplex supports Cplex from {0} up, unsupported version {1} was found" + .format(s_min_version, self.cplex_version)) + + @property + def cplex_lp_obj_kw(self): + if self.has_cplex and self.cplex_version_as_tuple >= (12, 10): + obj_kw = "obj1" + else: + obj_kw = "obj" + return obj_kw + + @staticmethod + def cplex_platform(): + sys_platform = platform.system() + machine = platform.machine() + if sys_platform == 'Windows': + return 'x64_win64' + elif sys_platform == 'Darwin': + return 'x86-64_osx' + elif sys_platform == 'Linux': + if machine == 'x86_64': + return 'x86-64_linux' + else: + # different flavors of linux (ppc64le_linux, s390x_linux) + return machine + "_linux" + return None + + @staticmethod + def cplex_distribname(): + distribname = Environment.cplex_platform() + if distribname == 'x64_win64': + distribname = 'x64_windows' + return distribname + + @property + def cplex_version(self): + return self._cplex_version + + @property + def cplex_version_as_tuple(self): + cpxv = self._cplex_version + return self.parse_cplex_version(cpxv, fallback=None) + + @property + def has_matplotlib(self): + """True if the `matplotlib` libraries are available. + """ + if self._found_matplotlib is None: + self.check_matplotlib() + return self._found_matplotlib + + @property + def has_pandas(self): + """True if the `pandas` libraries are available. + """ + self.check_pandas() + return self._found_pandas + + @property + def pandas_version(self): + self.check_pandas() + return self._pandas_version + + @property + def cplex_location(self): + """The system path where CPLEX is located, if present. Otherwise, returns None. + """ + return self._cplex_location + + @property + def has_numpy(self): + """True if the `numpy` libraries are available. + """ + self.check_numpy() + return self._found_numpy + +
[docs] def is_64bit(self): + """True if running on a 64-bit platform. + """ + return self._is64bit
+ + @property + def python_version(self): + """ Returns the Python version as a string""" + return platform.python_version() + + def auto_configure(self, logger=None): + self.check_cplex(logger=logger) + # check for pandas (watson studio) + self.check_pandas() + + def check_all(self): + self.check_cplex() + self.check_pandas() + self.check_numpy() + self.check_matplotlib() + +
[docs] def get_cplex_module(self, default_location=None, logger=None): + '''Returns the cplex module. + + If `default_location` is None, this method will try to import the cplex module in the following order: + + - by importing the module `import cplex` if the import is sucessful + - by importing the module in location $CPLEX_STUDIO_DIR20101/cplex/python + - by importing the module in location $CPLEX_STUDIO_DIR201/cplex/python + - by importing the module in location $CPLEX_STUDIO_DIR1210/cplex/python + - by importing the module in location $CPLEX_STUDIO_DIR129/cplex/python + - by importing the module in location $CPLEX_STUDIO_DIR128/cplex/python + + If `default_location` is a valid path and contains a valid python package, + `cplex` is imported from the specified location. + + If `default_location` is a valid path and + `<default_location>/cplex/python/<python_version>/<platform>` exists, `cplex` + is imported from that location + + If `cplex` could not be found, this method returns `None` + ''' + cplex = None + + def load_cplex(location, version=""): + # we cache loaded modules as we want to make sure that modules are loaded + # only once (same behaviour than 'import') + # example location: C:\Program Files\IBM\ILOG\CPLEX_Studio1210\cplex\python\3.7\x64_win64\ + cplex = _loaded_cplexes.get(location, None) + if cplex is None: + absolute_name = "cplex%s" % version + module_location = os.path.join(location, "cplex", "__init__.py") + if not os.path.isfile(module_location): + return None + if importlib_util: + spec = importlib_util.spec_from_file_location(absolute_name, + module_location) + cplex = importlib_util.module_from_spec(spec) + # TODO: Error out if one was already loaded + previous = sys.modules.get(absolute_name, None) + if previous is not None: + lo = location if location else "default sys.path" + raise RuntimeError("Cannot load cplex from %s, a previous version has already been loaded from %s" % (lo, previous.__file__)) + sys.modules[absolute_name] = cplex + spec.loader.exec_module(cplex) + else: + import imp + cplex = imp.load_source(absolute_name.split('.')[-1], module_location) + _loaded_cplexes[location] = cplex + return cplex + + def load_cplex_from_cos_root(cos_root, version=""): + platform = Environment.cplex_platform() + if platform is None: + raise UnsupportedPlatformError("Platform not supported, please install cplex python module") + python_version = '%s.%s' % (sys.version_info[0], + sys.version_info[1]) + full_path = os.path.join(cos_root, 'cplex', 'python', python_version, platform) + return load_cplex(full_path, version=version) + + if default_location is None: + try: + import cplex # @UnresolvedImport + if logger is not None: + logger.info("Found cplex with 'import cplex'") + except (ImportError, ModuleNotFoundError): + # in py3.7, ModuleNotFoundError is raised + user_cos_location = os.environ.get('DOCPLEX_COS_LOCATION', None) + if user_cos_location is not None: + cplex = load_cplex_from_cos_root(user_cos_location) + if cplex is None: + # user provided a cos location that was not right, raise warning + warnings.warn("Could not load CPLEX from Location provided by DOCPLEX_COS_LOCATION=%s. Using default locations." % user_cos_location) + if cplex is None: + try_environs = ['CPLEX_STUDIO_DIR221', + 'CPLEX_STUDIO_DIR20101', + 'CPLEX_STUDIO_DIR201', + 'CPLEX_STUDIO_DIR1210', + 'CPLEX_STUDIO_DIR129', + 'CPLEX_STUDIO_DIR128'] + for t in try_environs: + loc = os.environ.get(t, None) + # version = t[len('CPLEX_STUDIO_DIR'):] if loc else "" + # currently, there are some import in CPLEX, like: + # File "C:\Program Files\IBM\ILOG\CPLEX_Studio1210\cplex\python\3.7\x64_win64\cplex\_internal\_pycplex_platform.py", line 24, in <module> + # from cplex._internal.py37_cplex12100 import * + # that prevent us from loading multiples instances of cplex + # so for now, let's just ignore this version + cplex = load_cplex_from_cos_root(loc) if loc else None + if logger is not None: + logger.info("Looking into location %s, found = %s" % (loc, (cplex is not None))) + if cplex is not None: + return cplex + else: + if os.path.isfile(os.path.join(default_location, "__init__.py")): + cplex = load_cplex(default_location) + else: + cplex = load_cplex_from_cos_root(default_location) + return cplex
+ + def check_cplex(self, logger=None): + # detecting CPLEX using default search location + cplex = self.get_cplex_module(logger=logger) + self._found_cplex = (cplex is not None) + if self.has_cplex: + cplex_module_file = cplex.__file__ + if cplex_module_file: + self._cplex_location = os.path.dirname(os.path.dirname(cplex_module_file)) + try: + self._cplex_version = cplex.__version__ + except AttributeError: + # older version: use an instance + cpx = cplex.Cplex() + # format: MM.mm.rr.ff e.g.e 12.6.2.0 + self._cplex_version = cpx.get_version() + # terminate the dummy instance... + del cpx + + def check_numpy(self): + if self._found_numpy is None: + try: + import numpy.version as npv + self._found_numpy = True + self._numpy_version = npv.version + + self_numpy_hook = self._numpy_hook + if self_numpy_hook is not None: + # lazy call the hook once at first check time. + self_numpy_hook() + + except ImportError: # pragma: no cover + self._found_numpy = False + self._numpy_version = None + + return self._found_numpy + + def check_matplotlib(self): + try: + from matplotlib import __version__ as matplotlib_version + self._found_matplotlib = True + self._matplotlib_version = matplotlib_version + except ImportError: # pragma: no cover + self._found_matplotlib = False + + def check_pandas(self): + if self._found_pandas is None: + try: + import pandas + self._found_pandas = True + self._pandas_version = pandas.__version__ + except ImportError: # pragma: no cover + self._found_pandas = False + + @staticmethod + def _display_feature(is_present, feature_name, feature_version, location=None): + safe_feature_version = feature_version or "?" + if is_present is None: + pass # we dont know yet + elif is_present: + if location: + print("* {0} is present, version is {1}, located at: {2}".format(feature_name, safe_feature_version, + location)) + else: + print("* {0} is present, version is {1}".format(feature_name, safe_feature_version)) + else: + print("* {0} is not available".format(feature_name)) + + @property + def max_nb_digits(self): + # source: https://en.wikipedia.org/wiki/IEEE_floating_point + return 17 if self.is_64bit() else 9 + + @property + def bitness(self): + return 64 if self.is_64bit() else 32 + + def print_information(self): + print("* system is: {0} {1}".format(self._system, self._bitness)) + from sys import version_info + from docplex.mp import __version_info__ + + python_version = '%s.%s.%s' % (version_info[0], version_info[1], version_info[2]) + print("* Python version %s, located at: %s" % (python_version, sys.executable)) + self._display_feature(True, "docplex", "%d.%d.%d" % __version_info__) + self._display_feature(self.has_cplex, "CPLEX library", self._cplex_version, self._cplex_location) + self._display_feature(self._found_pandas, "pandas", self._pandas_version) + self._display_feature(self._found_numpy, "numpy", self._numpy_version) + self._display_feature(self._found_matplotlib, "matplotlib", self._matplotlib_version) + + @staticmethod + def closed_env(): + closed = Environment(start_auto_configure=False) + # force matplotlib absent + closed._found_matplotlib = False + closed._found_numpy = False + closed._found_pandas = False + return closed + + @staticmethod + def make_new_configured_env(): + # returns a fresh new environment + return Environment(start_auto_configure=True) + + @staticmethod + def get_default_env(): + if not Environment._default_env: + Environment._default_env = Environment.make_new_configured_env() + return Environment._default_env + + @staticmethod + def parse_cplex_version(version_text, fallback=None): + # parse such strings as: 20.1.0.0 | 2020-11-10 | 9bedb6d68 + if not version_text: + return fallback + barpos = version_text.find('|') + if barpos > 0: + assert barpos >= 2 + before_bar = barpos-1 + baseline_version = version_text[:before_bar] + else: + baseline_version = version_text + return tuple(float(x) for x in baseline_version.split('.')) + + # for pickling: recreate a fresh environment at the other end of pickle. + def __reduce__(self): + return Environment.make_new_configured_env, ()
+ + +def get_closed_environment(): + # This instance assumes nothing is found, CPLEX, numpy, etc, to be used for tests + return Environment.closed_env() + + +if __name__ == '__main__': + Environment().print_information() +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/error_handler.html b/docs/2.24.232/mp/_modules/docplex/mp/error_handler.html new file mode 100644 index 0000000..52462c9 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/error_handler.html @@ -0,0 +1,391 @@ + + + + + + + + + docplex.mp.error_handler — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.error_handler

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+
+from docplex.mp.utils import DOcplexException, DOcplexLimitsExceeded, resolve_pattern, is_int, is_string
+
+from enum import Enum
+import os
+
+##########################
+# Error handling
+
+
+class IErrorHandler(object):
+    def __init__(self):
+        pass  # pragma: no cover
+
+    def info(self, msg, args=None):
+        pass  # pragma: no cover
+
+    def warning(self, msg, args=None):
+        pass  # pragma: no cover
+
+    def error(self, msg, args=None):
+        pass  # pragma: no cover
+
+    def fatal(self, msg, args=None):
+        pass  # pragma: no cover
+
+    def ok(self):
+        return False  # pragma: no cover
+
+    def get_output_level(self):
+        return 0  # pragma: no cover
+
+    def set_output_level(self, new_level):
+        pass  # pragma: no cover
+
+    def ensure(self, condition, msg, *args):
+        if not condition:
+            self.fatal(msg, args)
+
+
+
[docs]class InfoLevel(Enum): + """ Enumerated type for the possible output levels. + + Info levels are sorted in increasing order of severity: `INFO`, `WARNING`, `ERROR`, `FATAL`. + Setting a level enables the printing of all messages from that severity and above. + + Example: + Setting the level to `WARNING` enables the printing of `WARNING`, `ERROR`, and `FATAL` messages, but not + `INFO` level messages. + Setting the level to `FATAL` suppresses all messages, except for fatal errors. + """ + INFO, WARNING, ERROR, FATAL = 1, 10, 100, 9999999 + + @classmethod + def parse(cls, arg, default_level=INFO): + # INTERNAL + if not arg: + return cls.INFO + elif isinstance(arg, cls): + return arg + elif is_string(arg): + return cls._name2level_map().get(arg.lower(), default_level) + elif is_int(arg): + if arg < 10: + # anything below 10 is INFO + return cls.INFO + elif arg < 100: + return cls.WARNING + elif arg < 1000: + return cls.ERROR + else: + # level fatal prints nothing except fatal errors + return cls.FATAL + else: + raise DOcplexException("Cannot convert this to InfoLevel: {0!r}".format(arg)) + + def __str__(self): + return self.name + + @staticmethod + def _headers(): + return {InfoLevel.FATAL: "FATAL", + InfoLevel.INFO: "*", + InfoLevel.WARNING: "Warning:", + InfoLevel.ERROR: "Error:" + } + + @staticmethod + def _name2level_map(): + return {"fatal": InfoLevel.FATAL, + "error": InfoLevel.ERROR, + "warning": InfoLevel.WARNING, + "info": InfoLevel.INFO} + + def header(self): + # cannot put the dict in the class + # as it willbe interpreted as another enum value. + return self._headers().get(self, "???")
+ + +class AbstractErrorHandler(IErrorHandler): + TRACE_HEADER = "--" + + def __init__(self, output_level=InfoLevel.INFO): + IErrorHandler.__init__(self) + self._trace_enabled = False + + self._number_of_errors = 0 + self._number_of_warnings = 0 + self._number_of_fatals = 0 + self._output_level = InfoLevel.INFO + self._is_print_suspended = False + self._postponed = [] + self.set_output_level(output_level) + + @property + def number_of_warnings(self): + """ Returns the number of warnings. + """ + return self._number_of_warnings + + @property + def number_of_errors(self): + """ Returns the number of errors. + """ + return self._number_of_errors + + @property + def number_of_fatals(self): + return self._number_of_fatals + + def get_output_level(self): + return self._output_level + + def set_output_level(self, output_level_arg): + output_level = InfoLevel.parse(output_level_arg) + if output_level != self._output_level: + self._output_level = output_level + + def set_trace_mode(self, trace_mode): + self._trace_enabled = trace_mode + + def enable_trace(self): + self.set_trace_mode(True) + + def disable_trace(self): + self.set_trace_mode(False) + + def is_trace_enabled(self): + return self._trace_enabled + + def set_quiet(self): + """ Changes the output level to enable only error messages. + """ + self.set_output_level(InfoLevel.ERROR) + + def reset(self): + self._number_of_errors = 0 + self._number_of_warnings = 0 + self._number_of_fatals = 0 + + def _internal_is_printed(self, level): + return self._output_level.value <= level.value + + def _internal_print_if(self, level, msg, args): + if self._internal_is_printed(level): + self._internal_print(level, msg, args) + + def _internal_print(self, level, msg, args): + # resolve message w/ args + header = level.header() + self._internal_print_header(header, msg, args) + + def _internal_print_header(self, header, msg, args): + resolved_message = resolve_pattern(msg, args) + mline = '%s %s' % (header, resolved_message) + if self._is_print_suspended: + self._postponed.append(mline) + else: + print(mline) + + def trace_header(self): + return self.TRACE_HEADER + + def trace(self, msg, args=None): + if self.is_trace_enabled(): + self._internal_print_header(self.trace_header(), msg, args) + + def info(self, msg, args=None): + self._internal_print_if(InfoLevel.INFO, msg, args) + + def warning(self, msg, args=None): + self._number_of_warnings += 1 + self._internal_print_if(InfoLevel.WARNING, msg, args) + + def error(self, msg, args=None): + docplex_error_stop_here() + self._number_of_errors += 1 + self._internal_print_if(InfoLevel.ERROR, msg, args) + + def fatal(self, msg, args=None): + self._number_of_fatals += 1 + resolved_message = resolve_pattern(msg, args) + docplex_error_stop_here() + raise DOcplexException(resolved_message) + + def fatal_limits_exceeded(self, nb_vars, nb_constraints): + docplex_error_stop_here() + raise DOcplexLimitsExceeded(nb_vars, nb_constraints) + + + def ok(self): + """ Checks whether the handler has not recorded any error. + """ + return self._number_of_errors == 0 and self._number_of_fatals == 0 + + def prints_trace(self): + return self.is_trace_enabled() + + def prints_info(self): + return self._internal_is_printed(InfoLevel.INFO) + + def prints_warning(self): + return self._internal_is_printed(InfoLevel.WARNING) + + def prints_error(self): + return self._internal_is_printed(InfoLevel.ERROR) + + def suspend(self): + self._is_print_suspended = True + + def flush(self): + self._is_print_suspended = False + for m in self._postponed: + print(m) + self._postponed = [] + + +def docplex_error_stop_here(): + # INTERNAL, use to set breakpoints + pass + + +def docplex_add_trivial_infeasible_ct_here(): + # INTERNAL: set breakpoint here to inspect ct + pass + +def docplex_fatal(msg, *args): + resolved_message = resolve_pattern(msg, args) + docplex_error_stop_here() + raise DOcplexException(resolved_message) + +def is_docplex_debug(): + return not not os.environ.get('DOCPLEX_DEBUG') + + +def docplex_debug_msg(*args): + if is_docplex_debug(): + msg = ' '.join(str(x) for x in args) + #print(f"-- {msg}") + print("-- {0}".format(msg)) + + +
[docs]class DefaultErrorHandler(AbstractErrorHandler): + """ The default error handler class. + + """ + + def __init__(self, output_level=InfoLevel): + AbstractErrorHandler.__init__(self, output_level)
+ + +class SilentErrorHandler(AbstractErrorHandler): + def __init__(self, output_level=InfoLevel): + AbstractErrorHandler.__init__(self, output_level) + + def _internal_print(self, level, msg, args): + # nothing out, this is the point! + pass + + def suspend(self): + pass + + def flush(self): + pass + + +def handle_error(logger, error, msg): + if error == "raise": + raise logger.fatal(msg) + elif error == "warn": + logger.warn(msg) + elif error =="ignore": + pass + else: + print("unknown error directive: {0}, expecting ignore|warn|raise, msg is: {1}".format(error, msg)) + + + +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/kpi.html b/docs/2.24.232/mp/_modules/docplex/mp/kpi.html new file mode 100644 index 0000000..798a5e5 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/kpi.html @@ -0,0 +1,302 @@ + + + + + + + + + docplex.mp.kpi — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.kpi

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+
+from docplex.mp.operand import Operand
+from docplex.mp.error_handler import docplex_fatal, DOcplexException
+
+from docplex.mp.utils import is_number, is_string, is_function, str_maxed
+from docplex.mp.basic import _AbstractNamable, _AbstractValuable
+
+
+
[docs]class KPI(_AbstractNamable, _AbstractValuable): + """ Base class for key performance indicators (KPIs). + + Each KPI has a unique name. A KPI is attached to a model instance and can compute a numerical value, + using the :func:`compute` method. The `compute` method takes an optional solution argument; + if passed a valid SolveSolution object, this solution is used to evaluate the KPI, else compute() + will attempt to access th emodel's solution. If the model has no attached solution, then an exception + is raised by `compute`. + + """ + def __init__(self, name=None): + self._name = name + + def _set_name(self, new_name): + self._name = new_name + + @property + def name(self): + return self._name + + @name.setter + def name(self, new_name): + self.set_name(new_name) + +
[docs] def get_model(self): + """ + Returns: + The model instance on which the KPI is defined. + :rtype: :class:`docplex.mp.model.Model` + """ + raise NotImplementedError # pragma: no cover
+ + @property + def model(self): + return self.get_model() + + def compute(self, s=None): + raise NotImplementedError # pragma: no cover + + # def _get_solution_value(self, s=None): # pragma: no cover + # # to be removed + # return self._raw_solution_value(s) + + def _raw_solution_value(self, s=None): # pragma: no cover + return self.compute(s) + + def _ensure_solution(self, s, do_raise=True): + # INTERNAL + if s is not None: + return s + else: + ms = self.get_model()._solution + if ms is not None: + return ms + elif do_raise: + self.get_model().fatal("KPI.compute() requires a solution, but model is not solved and no solution was passed") + else: + return None + + def check_name(self, name_arg): + self.model._checker.typecheck_string(name_arg, accept_none=False, accept_empty=False, caller="KPI.name") + +
[docs] def is_decision_expression(self): + """ returns True if the KPI is based on a decision expression or variable. + + """ + raise NotImplementedError # pragma: no cover
+ + def copy(self, new_model, var_map): + raise NotImplementedError # pragma: no cover + + def clone(self): + raise NotImplementedError # pragma: no cover + + @staticmethod + def new_kpi(model, kpi_arg, kpi_name): + # static factory method to build a new concrete instance of KPI + if isinstance(kpi_arg, KPI): + if not kpi_name: + return kpi_arg + else: + cloned = kpi_arg.clone() + cloned.name = kpi_name + return cloned + elif is_function(kpi_arg): + return FunctionalKPI(kpi_arg, model, kpi_name) + else: + # try a linear expr conversion + try: + expr = model._lfactory._to_expr(kpi_arg) + return DecisionKPI(expr, kpi_name) + except DOcplexException: + model.fatal("Cannot interpret this as a KPI: {0!r}. expecting expression, variable or function", kpi_arg) + + def notify_removed(self): + pass
+ + +
[docs]class DecisionKPI(KPI): + """ Specialized class of Key Performance Indicator, based on expressions. + + This subclass is built from a decision variable or a linear expression. + The :func:`compute` method evaluate the value of the KPI in a solution. This solution can either be passed + to the `compute` method, or using th emodel's solution. In the latter case, the model must have been solved + with a solution. + + """ + def __init__(self, kpi_op, name=None): + expr = None + if is_number(kpi_op): + expr = self.get_model().linear_expr(arg=kpi_op) + elif isinstance(kpi_op, Operand): + expr = kpi_op + expr.notify_used(self) # kpi is a subscriber + if hasattr(kpi_op, 'name'): + name = name or getattr(kpi_op, 'name') + + else: + self.get_model().fatal('cannot interpret this as kpi: {0!r}, expecting number or operand', kpi_op) + super().__init__(name) + self._expr = expr + + def notify_expr_modified(self, expr, event): + # do nothing + pass + + def notify_removed(self): + self._expr.notify_unsubscribed(self) + +
[docs] def get_model(self): + return self._expr.model
+ +
[docs] def compute(self, s=None): + """ Redefinition of the abstract `compute(s=None)` method. + + Returns: + float: The value of the decision expression in the solution. + + Note: + Evaluating a KPI requires a solution object. This solution can either be passed explicitly + in the `s` argument, otherwise the model solution is used. In the latter case, th emodel must + have been solved with a solution, otherwise an exception is raised. + + Raises: + Evaluating a KPI raises an exception if no `s` solution has been passed + and the underlying model has not been solved with a solution. + + See Also: + :class:`docplex.mp.solution.SolveSolution` + """ + es = self._ensure_solution(s, do_raise=True) + return self._expr._raw_solution_value(es)
+ +
[docs] def is_decision_expression(self): + return True
+ + def to_expr(self): + return self._expr + + as_expression = to_expr + + def to_linear_expr(self): + return self._expr.to_linear_expr() + + def copy(self, new_model, var_map): + expr_copy = self._expr.copy(new_model, var_map) + return DecisionKPI(kpi_op=expr_copy, name=self.name) + + def clone(self): + return DecisionKPI(self._expr, self.name) + + def __repr__(self): + return "{0}(name={1},expr={2!s})".format(self.__class__.__name__, self.name, str_maxed(self._expr, maxlen=64))
+ + +
[docs]class FunctionalKPI(KPI): + # Functional KPIs store a function that takes a model to compute a number + # Functional KPIs do not require a successful solve. + + def __init__(self, fn, model, name): + KPI.__init__(self, name) + self._function = fn + self._model = model + +
[docs] def get_model(self): + return self._model
+ + def compute(self, s=None): + es = self._ensure_solution(s) + return self._function(self._model, es) + +
[docs] def is_decision_expression(self): + return False
+ + def copy(self, new_model, var_map): + return FunctionalKPI(fn=self._function, model=new_model, name=self.name) + + def clone(self): + return FunctionalKPI(fn=self._function, model=self._model, name=self.name) + + def to_expr(self): + docplex_fatal("This KPI cannot be used as an expression: {0!r}".format(self))
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/linear.html b/docs/2.24.232/mp/_modules/docplex/mp/linear.html new file mode 100644 index 0000000..f84697f --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/linear.html @@ -0,0 +1,1585 @@ + + + + + + + + + docplex.mp.linear — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.linear

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+
+# pylint: disable=too-many-lines
+from docplex.mp.constants import ComparisonType, UpdateEvent
+from docplex.mp.basic import Expr, ModelingObjectBase, _SubscriptionMixin
+from docplex.mp.operand import LinearOperand
+from docplex.mp.utils import is_int, is_number, iter_emptyset, is_quad_expr
+from docplex.mp.dvar import is_var
+# ----------------------------
+# kept for compatibility
+from docplex.mp.dvar import Var
+# ------------------
+from docplex.mp.sttck import StaticTypeChecker
+
+
+
[docs]class DOCplexQuadraticArithException(Exception): + # INTERNAL + pass
+ + +# noinspection PyAbstractClass +
[docs]class AbstractLinearExpr(LinearOperand, Expr): + __slots__ = ('_discrete_locked',) + +
[docs] def get_coef(self, dvar): + """ Returns the coefficient of a variable in the expression. + + Note: + If the variable is not present in the expression, the function returns 0. + + :param dvar: The variable for which the coefficient is being queried. + + :return: A floating-point number. + """ + self.model._typecheck_var(dvar) + return self.unchecked_get_coef(dvar)
+ + def __getitem__(self, dvar): + # direct access to a variable coef x[var] + return self.unchecked_get_coef(dvar) + + def __iter__(self): + # INTERNAL: this is necessary to prevent expr from being an iterable. + # as it follows getitem protocol, it can mistakenly be interpreted as an iterable + # but this would make sum loop forever. + raise TypeError + + def lock_discrete(self): + # intern al: used for any expression used in linear constraints inside equivalences + self._discrete_locked = True + + def is_discrete_locked(self): + return getattr(self, '_discrete_locked', False) + + def check_discrete_lock_frozen(self, item=None): + self.get_linear_factory().check_expr_discrete_lock(self, item) + + def relaxed_copy(self, relaxed_model, var_map): + return self.copy(relaxed_model, var_map) + + def set_coefficients(self, var_coef_seq): + for dv, k in var_coef_seq: + self.set_coefficient(dv, k)
+ + +
[docs]class MonomialExpr(_SubscriptionMixin, AbstractLinearExpr): + # INTERNAL + + def _raw_solution_value(self, s=None): + return self.coef * self._dvar._raw_solution_value(s) + + # INTERNAL class + __slots__ = ('_dvar', '_coef', '_subscribers') + + # noinspection PyMissingConstructor + def __init__(self, model, dvar, coeff, safe=False): + self._model = model # faster than to call recursively init methods... + self._name = None + self._dvar = dvar + self._subscribers = [] + if safe: + self._coef = coeff + else: + validfn = model._checker.get_number_validation_fn() + self._coef = validfn(coeff) if validfn else coeff + +
[docs] def number_of_variables(self): + return 1
+ + def __hash__(self): + # py3 requires this function + return id(self) + + @property + def var(self): + return self._dvar + + @property + def coef(self): + return self._coef + + @property + def constant(self): + # for compatibility + return 0 + + def as_variable(self): + # INTERNAL + return self._dvar if 1 == self._coef else None + + def clone(self): + return self.__class__(self.model, self._dvar, self._coef, safe=True) + + def copy(self, target_model, var_mapping): + copy_var = var_mapping[self._dvar] + return MonomialExpr(target_model, dvar=copy_var, coeff=self._coef, safe=True) + + def iter_terms(self): + yield self._dvar, self._coef + + iter_sorted_terms = iter_terms + + def unchecked_get_coef(self, dvar): + return self._coef if dvar is self._dvar else 0 + +
[docs] def contains_var(self, dvar): + return self._dvar is dvar
+ + def is_normalized(self): + # INTERNAL + return self._coef != 0 # pragma: no cover + + def is_discrete(self): + return self._dvar.is_discrete() and is_int(self._coef) + + # arithmetics + def negate(self): + self._coef = - self._coef + self.notify_modified(event=UpdateEvent.LinExprCoef) + return self + + def plus(self, e): + if isinstance(e, LinearOperand) or is_number(e): + return self.to_linear_expr().add(e) + else: + return e.plus(self) + + def minus(self, e): + if isinstance(e, LinearOperand) or is_number(e): + expr = self.to_linear_expr() + expr.subtract(e) + return expr + else: + return e.rminus(self) + + def times(self, e): + if is_number(e): + if 0 == e: + return self.get_linear_factory().new_zero_expr() + else: + # return a fresh instance + return MonomialExpr(self._model, self._dvar, self._coef * e, safe=True) + elif isinstance(e, LinearExpr): + return e.times(self) + elif is_var(e): + return self.model._qfactory.new_var_product(e, self) + elif isinstance(e, MonomialExpr): + return self.model._qfactory.new_monomial_product(self, e) + else: + expr = self.to_linear_expr() + return expr.multiply(e) + + def square(self): + return self.model._qfactory.new_monomial_product(self, self) + + def quotient(self, e): + # returns a new instance + self._model._typecheck_as_denominator(e, self) + inverse = 1.0 / float(e) + return MonomialExpr(self._model, self._dvar, self._coef * inverse, safe=True) + + def __add__(self, e): + return self.plus(e) + + def __radd__(self, e): + return self.__add__(e) + + def __sub__(self, e): + return self.minus(e) + + def __rsub__(self, e): + return self.get_linear_factory()._to_linear_operand(e, force_clone=True).minus(self) + + def __neg__(self): + opposite = self.clone() + return opposite.negate() + + def __mul__(self, e): + return self.times(e) + + def __rmul__(self, e): + return self.times(e) + + def __div__(self, e): + return self.quotient(e) + + def __truediv__(self, e): + # for py3 + # INTERNAL + return self.__div__(e) # pragma: no cover + + def __rtruediv__(self, e): + # for py3 + self.model.cannot_be_used_as_denominator_error(self, e) # pragma: no cover + + def __rdiv__(self, e): + self.model.cannot_be_used_as_denominator_error(self, e) # pragma: no cover + + # changing a coef + def set_coefficient(self, dvar, coef): + m = self._model + m._typecheck_var(dvar) + m._typecheck_num(coef, 'Expr.set_coefficient()') + return self._set_coefficient(dvar, coef) + + set_coef = set_coefficient + + def _set_coefficient(self, dvar, coef): + self.check_discrete_lock_frozen(item=coef) + if dvar is self._dvar: + self._coef = coef + self.notify_modified(event=UpdateEvent.LinExprCoef) + if not coef: + self.notify_replaced(new_expr=self.lfactory.new_zero_expr()) + elif coef: + # monomail is extended to a linear expr + new_self = self.to_linear_expr() + new_self._add_term(dvar, coef) + # beware self is modified here + self.notify_replaced(new_self) + # noinspection PyMethodFirstArgAssignment + self = new_self + return self + + # -- arithmetic to self + def __iadd__(self, other): + return self._add_to_self(other) + + def _add_to_self(self, other): + self.check_discrete_lock_frozen(item=other) + if isinstance(other, LinearOperand) or is_number(other): + added = self.to_linear_expr().add(other) + else: + added = other.plus(self) + self.notify_replaced(added) + return added + + def add(self, other): + return self._add_to_self(other) + + def __isub__(self, other): + return self._sub_to_self(other) + + def _sub_to_self(self, other): + # INTERNAL + self.check_discrete_lock_frozen(item=other) + if isinstance(other, LinearOperand) or is_number(other): + expr = self.to_linear_expr() + expr.subtract(other) + subtracted = expr + else: + subtracted = other.rminus(self) + self.notify_replaced(subtracted) + return subtracted + + def subtract(self, other): + return self._sub_to_self(other) + + def __imul__(self, e): + return self.multiply(e) + + def multiply(self, e): + self.check_discrete_lock_frozen(e) + if is_number(e): + if 0 == e: + product = self.get_linear_factory().new_zero_expr() + else: + self._coef *= e + self.notify_modified(event=UpdateEvent.LinExprCoef) + product = self + elif isinstance(e, LinearExpr): + product = e.times(self) + elif is_var(e): + product = self.model._qfactory.new_var_product(e, self) + elif isinstance(e, MonomialExpr): + product = self.model._qfactory.new_monomial_product(self, e) + elif is_quad_expr(e): + if e.has_quadratic_term(): + StaticTypeChecker.mul_quad_lin_error(self._model, self, e) + else: + product = self.model._qfactory.new_monomial_product(self, e.linear_part) + else: + product = self.to_linear_expr().multiply(e) + self.notify_replaced(product) + return product + + mul = multiply + + def __idiv__(self, other): + return self.divide(other) # pragma: no cover + + def __itruediv__(self, other): # pragma: no cover + # for py3 + return self.divide(other) + + def divide(self, other): + self._model._typecheck_as_denominator(other, self) + inverse = 1.0 / float(other) + self.check_discrete_lock_frozen(inverse) + self._coef *= inverse + self.notify_modified(event=UpdateEvent.LinExprCoef) + return self + + def equals(self, other): + return isinstance(other, LinearOperand) and \ + other.get_constant() == 0 and \ + other.number_of_terms() == 1 and \ + other.unchecked_get_coef(self._dvar) == self._coef + + # conversion + def to_linear_expr(self): + e = LinearExpr(self._model, e=(self._dvar, self._coef), safe=True, transient=True) + return e + + def to_stringio(self, oss, nb_digits, use_space, var_namer=lambda v: v.lp_name): + self_coef = self._coef + if self_coef != 1: + if self_coef < 0: + oss.write(u'-') + self_coef = - self_coef + if self_coef != 1: + self._num_to_stringio(oss, num=self_coef, ndigits=nb_digits) + if use_space: + oss.write(u' ') + oss.write(str(var_namer(self._dvar))) + + def __repr__(self): + return "docplex.mp.MonomialExpr(%s)" % self.to_string()
+ + +# from private.debug_deco import count_instances +# +# @count_instances +
[docs]class LinearExpr(_SubscriptionMixin, AbstractLinearExpr): + """LinearExpr() + + This class models linear expressions. + This class is not intended to be instantiated. Expressions are built + either using operators or using `Model.linear_expr()`. + """ + + @staticmethod + def _new_terms_dict(model, *args, **kwargs): + return model._lfactory.term_dict_type(*args, **kwargs) + + # @staticmethod + # def _new_empty_terms_dict(model): + # return model._lfactory.term_dict_type() + + def to_linear_expr(self): + return self + + def _get_terms_dict(self): + # INTERNAL + return self._terms + + __slots__ = ('_constant', '_terms', '_transient', '_subscribers') + + def __hash__(self): + # py3 requires this function + return id(self) + + def __init__(self, model, e=None, constant=0, safe=False, transient=False): + ModelingObjectBase.__init__(self, model) + if not safe and constant: + model._typecheck_num(constant, 'LinearExpr()') + self._constant = constant + self._transient = transient + self._subscribers = [] + + if isinstance(e, dict): + if safe: + self._terms = e + else: + self_terms = model._lfactory.term_dict_type() + for (v, k) in e.items(): + model._typecheck_var(v) + model._typecheck_num(k, 'LinearExpr') + if k != 0: + self_terms[v] = k + self._terms = self_terms + return + else: + self._terms = model._lfactory._new_term_dict() + + if e is None: + pass + + elif is_var(e): + self._terms[e] = 1 + + elif is_number(e): + self._constant += e + + elif isinstance(e, MonomialExpr): + # TODO: simplify by self_terms[e.var] = e.coef + self._add_term(e.var, e.coef) + + elif isinstance(e, LinearExpr): + # note that transient is not kept. + self._constant = e.get_constant() + self._terms = self._new_terms_dict(model, e._get_terms_dict()) # make a copy + + elif isinstance(e, tuple): + v, k = e + self._terms[v] = k + + else: + self.fatal("Cannot convert {0!r} to docplex.mp.LinearExpr, type is {1}", e, type(e)) + + def keep(self): + self._transient = False + return self + + def is_kept(self): + # INTERNAL + return not self._transient + + def is_transient(self): # pragma: no cover + # INTERNAL + return self._transient + + def clone_if_necessary(self): + # INTERNAL + if self._transient and not self._model._keep_all_exprs and not self.is_in_use(): + return self + else: + return self.clone() + + # def set_name(self, name): + # Expr.set_name(self, name) + # # an expression with a name is not transient any more + # if name: + # self.keep() + # + # def _get_name(self): + # return self._name + # + # name = property(_get_name, set_name) + + # from private.debug_deco import count_calls + # @count_calls +
[docs] def clone(self): + """ + Returns: + A copy of the expression on the same model. + """ + cloned_terms = self._new_terms_dict(self._model, self._terms) # faster than copy() on OrderedDict() + cloned = LinearExpr(model=self._model, e=cloned_terms, constant=self._constant, safe=True) + return cloned
+ + def copy(self, target_model, var_mapping): + # INTERNAL + copied_terms = self._new_terms_dict(target_model) + for v, k in self.iter_sorted_terms(): + copied_terms[var_mapping[v]] = k + copied_expr = LinearExpr(model=target_model, e=copied_terms, constant=self.constant, safe=True) + return copied_expr + +
[docs] def negate(self): + """ Takes the negation of an expression. + + Changes the expression by replacing each variable coefficient and the constant term + by its opposite. + + Note: + This method does not create any new expression but modifies the `self` instance. + + Returns: + The modified self. + + """ + self._constant = - self._constant + self_terms = self._terms + for v, k in self_terms.items(): + self_terms[v] = -k + self.notify_modified(event=UpdateEvent.LinExprGlobal) + return self
+ + def _clear(self): + """ Clears the expression. + + All variables and coefficients are removed and the constant term is set to zero. + """ + self._constant = 0 + self._terms.clear() + +
[docs] def equals_constant(self, scalar): + """ Checks if the expression equals a constant term. + + Args: + scalar (float): A floating-point number. + Returns: + Boolean: True if the expression equals this constant term. + """ + return self.is_constant() and (scalar == self._constant)
+ + def is_zero(self): + return self.equals_constant(0) + +
[docs] def is_constant(self): + """ + Checks if the expression is a constant. + + Returns: + Boolean: True if the expression consists of only a constant term. + """ + return not self._terms
+ + def as_variable(self): + # INTERNAL: returns True if expression is in fact a variable (1*x) + if 1 == len(self._terms) and not self._constant: + for v, k in self.iter_terms(): + if k == 1: + return v + return None + + def is_normalized(self): + # INTERNAL + return all(k for _, k in self.iter_terms() ) + + def normalize(self): + doomed = [dv for dv, k in self.iter_terms() if not k] + lterms = self._terms + for d in doomed: + del lterms[d] + +
[docs] def number_of_variables(self): + return len(self._terms)
+ + def unchecked_get_coef(self, dvar): + # INTERNAL + return self._terms.get(dvar, 0) + +
[docs] def add_term(self, dvar, coeff): + """ + Adds a term (variable and coefficient) to the expression. + + Args: + dvar (:class:`Var`): A decision variable. + coeff (float): A floating-point number. + + Returns: + The modified expression itself. + """ + if coeff: + self._model._typecheck_var(dvar) + self._model._typecheck_num(coeff) + self._add_term(dvar, coeff) + self.notify_modified(event=UpdateEvent.LinExprCoef) + return self
+ + def _add_term(self, dvar, coef=1): + # INTERNAL + self_terms = self._terms + new_coef = self_terms.get(dvar, 0) + coef + if new_coef: + self_terms[dvar] = new_coef + else: + try: + del self_terms[dvar] + except KeyError: + pass + + def set_coefficient(self, dvar, coeff): + self._model._typecheck_var(dvar) + self._model._typecheck_num(coeff) + self._set_coefficient(dvar, coeff) + + set_coef = set_coefficient + + def _set_coefficient_internal(self, dvar, coeff): + self_terms = self._terms + if coeff or dvar in self_terms: + self_terms[dvar] = coeff + return True + else: + return False + + def _set_coefficient(self, dvar, coeff): + self.check_discrete_lock_frozen(coeff) + if self._set_coefficient_internal(dvar, coeff): + self.notify_modified(event=UpdateEvent.LinExprCoef) + if not coeff: + self.normalize() + + def set_coefficients(self, var_coef_seq): + # TODO: typecheck + self._set_coefficients(var_coef_seq) + + set_coefs = set_coefficients + + def _set_coefficients(self, var_coef_seq): + self.check_discrete_lock_frozen() + nb_changes = 0 + nb_nulls = 0 + for dv, k in var_coef_seq: + if self._set_coefficient_internal(dv, k): + nb_changes += 1 + if not k: + nb_nulls += 1 + if nb_changes: + self.notify_modified(event=UpdateEvent.LinExprCoef) + if nb_nulls: + self.normalize() + + +
[docs] def remove_term(self, dvar): + """ Removes a term associated with a variable from the expression. + + Args: + dvar (:class:`Var`): A decision variable. + + Returns: + The modified expression. + + """ + self.set_coefficient(dvar, 0)
+ + @property + def constant(self): + """ + This property is used to get or set the constant term of the expression. + """ + return self._constant + + @constant.setter + def constant(self, new_constant): + self._set_constant(new_constant) + + def get_constant(self): + return self._constant + + def _set_constant(self, new_constant): + if new_constant != self._constant: + self.check_discrete_lock_frozen(new_constant) + self._constant = new_constant + self.notify_modified(event=UpdateEvent.ExprConstant) + +
[docs] def contains_var(self, dvar): + """ Checks whether a decision variable is part of an expression. + + Args: + dvar (:class:`Var`): A decision variable. + + Returns: + Boolean: True if `dvar` is mentioned in the expression with a nonzero coefficient. + """ + return dvar in self._terms
+ +
[docs] def equals(self, other): + """ + This method is used to test equality between expressions. + Because of the overloading of operator `==` through the redefinition of + the `__eq__` method, you cannot use `==` to test for equality. + The `equals` method to test whether a given expression is equivalent to a variable. + Two linear expressions are equivalent if they have the same coefficient for all + variables. + + Args: + other: a number or any expression. + + + + Returns: + A boolean value, True if the passed expression is equivalent, else False. + + + Note: + A constant expression is considered equivalent to its constant number. + + m.linear_expression(3).equals(3) returns True + """ + if is_number(other): + return self.is_constant() and other == self.constant + else: + if not isinstance(other, LinearOperand): + return False + if self.constant != other.get_constant(): + return False + if self.number_of_terms() != other.number_of_terms(): + return False + for dv, k in self.iter_terms(): + if k != other.unchecked_get_coef(dv): + return False + return True
+ + # noinspection PyPep8 + def to_stringio(self, oss, nb_digits, use_space, var_namer=lambda v: v.lp_name): + # INTERNAL + # Writes unicode representation of self + c = 0 + # noinspection PyPep8Naming + SP = u' ' + + for v, coeff in self.iter_sorted_terms(): + if not coeff: + continue # pragma: no cover + + # 1 separator + if use_space and c > 0: + oss.write(SP) + + # --- + # sign is printed if non-first OR negative + # at the end of this block coeff is positive + if coeff < 0 or c > 0: + oss.write(u'-' if coeff < 0 else u'+') + if coeff < 0: + coeff = -coeff + if use_space and c > 0: + oss.write(SP) + # --- + + if 1 != coeff: + self._num_to_stringio(oss, coeff, nb_digits) + if use_space: + oss.write(SP) + + varname = var_namer(v) + oss.write(str(varname)) + c += 1 + + k = self.constant + if c == 0: + self._num_to_stringio(oss, k, nb_digits) + elif k != 0: + if k < 0: + sign = u'-' + k = -k + else: + sign = u'+' + if use_space: + oss.write(SP) + oss.write(sign) + if use_space: + oss.write(SP) + self._num_to_stringio(oss, k, nb_digits) + + def _add_expr(self, other_expr): + # INTERNAL + self._constant += other_expr.get_constant() + # merge term dictionaries + for v, k in other_expr.iter_terms(): + # use unchecked version + self._add_term(v, k) + + def _add_expr_scaled(self, expr, factor): + # INTERNAL: used by quadratic + if factor: + self._constant += expr.get_constant() * factor + for v, k in expr.iter_terms(): + # use unchecked version + self._add_term(v, k * factor) + + # --- algebra methods always modify self. +
[docs] def add(self, e): + """ Adds an expression to self. + + Note: + This method does not create an new expression but modifies the `self` instance. + + Args: + e: The expression to be added. Can be a variable, an expression, or a number. + + Returns: + The modified self. + + See Also: + The method :func:`plus` to compute a sum without modifying the self instance. + """ + event = UpdateEvent.LinExprGlobal + if is_var(e): + self._add_term(e, coef=1) + elif isinstance(e, LinearOperand): + self._add_expr(e) + if isinstance(e, ZeroExpr): + event = None + elif is_number(e): + validfn = self._model._checker.get_number_validation_fn() + valid_e = validfn(e) if validfn else e + self._constant += valid_e + event = UpdateEvent.ExprConstant + elif is_quad_expr(e): + raise DOCplexQuadraticArithException + else: + try: + self.add(e.to_linear_expr()) + except AttributeError: + self._unsupported_binary_operation(self, "+", e) + + self.notify_modified(event=event) + return self
+ +
[docs] def iter_terms(self): + """ Iterates over the terms in the expression. + + Returns: + An iterator over the (variable, coefficient) pairs in the expression. + """ + return self._terms.items()
+ + def number_of_terms(self): + return len(self._terms) + + @property + def size(self): + return len(self._terms) + bool(self._constant) + +
[docs] def subtract(self, e): + """ Subtracts an expression from this expression. + Note: + This method does not create a new expression but modifies the `self` instance. + + Args: + e: The expression to be subtracted. Can be either a variable, an expression, or a number. + + Returns: + The modified self. + + See Also: + The method :func:`minus` to compute a difference without modifying the `self` instance. + """ + event = UpdateEvent.LinExprCoef + if is_var(e): + self._add_term(e, -1) + elif is_number(e): + self._constant -= e + event = UpdateEvent.ExprConstant + elif isinstance(e, LinearExpr): + if e.is_constant() and 0 == e.get_constant(): + return self + else: + # 1. decr constant + self.constant -= e.constant + # merge term dictionaries + for v, k in e.iter_terms(): + self._add_term(v, -k) + elif isinstance(e, MonomialExpr): + self._add_term(e.var, -e.coef) + elif isinstance(e, ZeroExpr): + event = None + elif is_quad_expr(e): + # + raise DOCplexQuadraticArithException + else: + try: + self.subtract(e.to_linear_expr()) + except AttributeError: + self._unsupported_binary_operation(self, "-", e) + self.notify_modified(event) + return self
+ + def _scale(self, factor): + # INTERNAL: used my multiply + # this method modifies self. + if 0 == factor: + self._clear() + elif factor != 1: + self._constant *= factor + self_terms = self._terms + for v, k in self_terms.items(): + self_terms[v] = k * factor + +
[docs] def multiply(self, e): + """ Multiplies this expression by an expression. + + Note: + This method does not create a new expression but modifies the `self` instance. + + Args: + e: The expression that is used to multiply `self`. + + Returns: + The modified `self`. + + See Also: + The method :func:`times` to compute a multiplication without modifying the `self` instance. + """ + mul_res = self + event = UpdateEvent.LinExprGlobal + self_constant = self.get_constant() + if is_number(e): + self._scale(factor=e) + + elif isinstance(e, LinearOperand): + if e.is_constant(): + # simple scaling + self._scale(factor=e.get_constant()) + elif self.is_constant(): + # self is constant: import other terms , scaled. + # set constant to zero. + if self_constant: + for lv, lk in e.iter_terms(): + self.set_coefficient(dvar=lv, coeff=lk * self_constant) + self._constant *= e.get_constant() + else: + self._scale(factor=0) + + else: + # yields a quadratic + mul_res = self.model._qfactory.new_linexpr_product(self, e) + event = UpdateEvent.LinExprPromotedToQuad + + # elif isinstance(e, ZeroExpr): + # self._scale(factor=0) + + elif is_quad_expr(e): + if not e.number_of_quadratic_terms: + return self.multiply(e.linear_part) + elif self.is_constant(): + return e.multiply(self.get_constant()) + else: + StaticTypeChecker.mul_quad_lin_error(self._model, self, e) + + else: + self.fatal("Multiply expects variable, expr or number, {0!r} was passed (type is {1})", e, type(e)) + + self.notify_modified(event=event) + + return mul_res
+ + def square(self): + return self.model._qfactory.new_linexpr_product(self, self) + +
[docs] def divide(self, e): + """ Divides this expression by an operand. + + Args: + e: The operand by which the self expression is divided. Only nonzero numbers are permitted. + + Note: + This method does not create a new expression but modifies the `self` instance. + + Returns: + The modified `self`. + """ + self.model._typecheck_as_denominator(e, numerator=self) + inverse = 1.0 / float(e) + return self.multiply(inverse)
+ + # operator-based API + def opposite(self): + cloned = self.clone_if_necessary() + cloned.negate() + return cloned + +
[docs] def plus(self, e): + """ Computes the sum of the expression and some operand. + + Args: + e: the expression to add to self. Can be either a variable, an expression or a number. + + Returns: + a new expression equal to the sum of the self expression and `e` + + Note: + This method doe snot modify self. + """ + cloned = self.clone_if_necessary() + try: + return cloned.add(e) + except DOCplexQuadraticArithException: + return e.plus(self)
+ + def minus(self, e): + cloned = self.clone_if_necessary() + try: + return cloned.subtract(e) + except DOCplexQuadraticArithException: + return e.rminus(self) + +
[docs] def times(self, e): + """ Computes the multiplication of this expression with an operand. + + Note: + This method does not modify the `self` instance but returns a new expression instance. + + Args: + e: The expression that is used to multiply `self`. + + Returns: + A new instance of expression. + """ + cloned = self.clone_if_necessary() + return cloned.multiply(e)
+ +
[docs] def quotient(self, e): + """ Computes the division of this expression with an operand. + + Note: + This method does not modify the `self` instance but returns a new expression instance. + + Args: + e: The expression that is used to modify `self`. Only nonzero numbers are permitted. + + Returns: + A new instance of expression. + """ + cloned = self.clone_if_necessary() + cloned.divide(e) + return cloned
+ + def __add__(self, e): + return self.plus(e) + + def __radd__(self, e): + return self.plus(e) + + def __iadd__(self, e): + try: + self.add(e) + return self + except DOCplexQuadraticArithException: + r = e + self + self.notify_replaced(new_expr=r) + return r + + def __sub__(self, e): + return self.minus(e) + + def __rsub__(self, e): + cloned = self.clone_if_necessary() + cloned.subtract(e) + cloned.negate() + return cloned + + def __isub__(self, e): + try: + return self.subtract(e) + except DOCplexQuadraticArithException: + r = -e + self + return r + + def __neg__(self): + return self.opposite() + + def __mul__(self, e): + return self.times(e) + + def __rmul__(self, e): + return self.times(e) + + def __imul__(self, e): + return self.multiply(e) + + def __div__(self, e): + return self.quotient(e) + + def __idiv__(self, other): + return self.divide(other) # pragma: no cover + + def __itruediv__(self, other): + # this is for Python 3.z + return self.divide(other) # pragma: no cover + + def __truediv__(self, e): + return self.__div__(e) # pragma: no cover + + def __rtruediv__(self, e): + self.fatal("Expression {0!s} cannot be used as divider of {1!s}", self, e) # pragma: no cover + + @property + def solution_value(self): + """ This property returns the solution value of the variable. + + Raises: + DOCplexException + if the model has not been solved. + """ + return super().solution_value + + def _raw_solution_value(self, s=None): + # INTERNAL: no checks + val = self._constant + sol = s or self._model.solution + for var, koef in self.iter_terms(): + val += koef * sol._get_var_value(var) + return val + +
[docs] def is_discrete(self): + """ Checks if the expression contains only discrete variables and coefficients. + + Example: + If X is an integer variable, X, X+1, 2X+3 are discrete + but X+0.3, 1.5X, 2X + 0.7 are not. + + Returns: + Boolean: True if the expression contains only discrete variables and coefficients. + """ + self_cst = self._constant + if self_cst != int(self_cst): + # a float constant with integer value is OK + return False + + for v, k in self.iter_terms(): + if not v.is_discrete() or not is_int(k): + return False + return True
+ + def __repr__(self): + return "docplex.mp.LinearExpr({0})".format(self.repr_str()) + + def _iter_sorted_terms(self): + # internal + self_terms = self._terms + for dv in sorted(self_terms.keys(), key=lambda v: v._index): + yield dv, self_terms[dv] + + def iter_sorted_terms(self): + if self._model.keep_ordering: + return self.iter_terms() + else: + return self._iter_sorted_terms()
+ + +LinearConstraintType = ComparisonType + + +
[docs]class ZeroExpr(_SubscriptionMixin, AbstractLinearExpr): + def _raw_solution_value(self, s=None): + return 0 + + def is_zero(self): + return True + + # INTERNAL + __slots__ = ('_subscribers',) + + def __hash__(self): + return id(self) + + def __init__(self, model): + ModelingObjectBase.__init__(self, model) + self._subscribers = [] + + def clone(self): + return self # this is not cloned. + + def copy(self, target_model, var_mapping): + return ZeroExpr(target_model) + + def to_linear_expr(self): + return self # this is a linear expr. + +
[docs] def number_of_variables(self): + return 0
+ + def number_of_terms(self): + return 0 + + def iter_terms(self): + return iter_emptyset() + + def is_constant(self): + return True + + def is_discrete(self): + return True + + def unchecked_get_coef(self, dvar): + return 0 + +
[docs] def contains_var(self, dvar): + return False
+ + @property + def constant(self): + # for compatibility + return 0 + + @constant.setter + def constant(self, newk): + if newk: + cexpr = self.get_linear_factory().constant_expr(newk, safe_number=False) + self.notify_replaced(cexpr) + + def negate(self): + return self + + # noinspection PyMethodMayBeStatic + def plus(self, e): + return e + + def times(self, _): + return self + + # noinspection PyMethodMayBeStatic + def minus(self, e): + return -e + + def to_string(self, nb_digits=None, use_space=False): + return '0' + + def to_stringio(self, oss, nb_digits, use_space, var_namer=lambda v: v.name): + oss.write(self.to_string()) + + # arithmetic + def __sub__(self, e): + return self.minus(e) + + def __rsub__(self, e): + # e - 0 = e ! + return e + + def __neg__(self): + return self + + def __add__(self, other): + return other + + def __radd__(self, other): + return other + + def __mul__(self, other): + return self + + def __rmul__(self, other): + return self + + def __div__(self, other): + return self._divide(other) + + def __truediv__(self, e): + # for py3 + # INTERNAL + return self.__div__(e) # pragma: no cover + + def _divide(self, other): + self.model._typecheck_as_denominator(numerator=self, denominator=other) + return self + + def __repr__(self): + return "docplex.mp.ZeroExpr()" + + def equals(self, other): + return (isinstance(other, LinearOperand) and + (0 == other.get_constant() and (0 == other.number_of_terms()))) or \ + (is_number(other) and other == 0) + + def square(self): + return self + + # arithmetic to self + add = plus + subtract = minus + multiply = times + + def __iadd__(self, other): + linear_other = self.get_linear_factory()._to_linear_operand(other, force_clone=False) + self.notify_replaced(linear_other) + return linear_other + + def __isub__(self, other): + linear_other = self.get_linear_factory()._to_linear_operand(other, force_clone=True) + negated = linear_other.negate() + self.notify_replaced(negated) + return negated
+ + +
[docs]class ConstantExpr(_SubscriptionMixin, AbstractLinearExpr): + __slots__ = ('_constant', '_subscribers') + + def __init__(self, model, cst): + ModelingObjectBase.__init__(self, model=model, name=None) + # assume constant is a number (to be checked upfront) + self._constant = cst + self._subscribers = [] + + @property + def size(self): + return 1 if self._constant else 0 + + # INTERNAL + def _make_new_constant(self, new_value): + return ConstantExpr(self._model, new_value) + + def _raw_solution_value(self, s=None): + return self._constant + + def is_zero(self): + return 0 == self._constant + + def clone(self): + return self.__class__(self._model, self._constant) + + def copy(self, target_model, var_mapping): + return self.__class__(target_model, self._constant) + + def to_linear_expr(self): + return self # this is a linear expr. + +
[docs] def number_of_variables(self): + return 0
+ +
[docs] def iter_variables(self): + return iter_emptyset()
+ + def iter_terms(self): + return iter_emptyset() + + def is_constant(self): + return True + + def is_discrete(self): + return is_int(self._constant) + + def unchecked_get_coef(self, dvar): + return 0 + +
[docs] def contains_var(self, dvar): + return False
+ + def set_coefficients(self, var_coef_seq): + pass + + @property + def constant(self): + return self._constant + + @constant.setter + def constant(self, new_constant): + self._set_constant(new_constant) + + def get_constant(self): + return self._constant + + def _set_constant(self, new_constant): + if new_constant != self._constant: + self.check_discrete_lock_frozen(new_constant) + self._constant = new_constant + self.notify_modified(event=UpdateEvent.ExprConstant) + + def negate(self): + return self._make_new_constant(- self._constant) + + def _apply_op(self, pyop, arg): + if is_number(arg): + return self._make_new_constant(pyop(self.constant, arg)) + else: + return pyop(arg, self._constant) + + # noinspection PyMethodMayBeStatic + def plus(self, e): + import operator + return self._apply_op(operator.add, e) + + def times(self, e): + if is_number(e): + return self.__class__(self._model, e * self._constant) + else: + return e * self._constant + + # noinspection PyMethodMayBeStatic + def minus(self, e): + return self + (-e) + + def to_string(self, nb_digits=None, use_space=False): + return '{0}'.format(self._constant) + + def to_stringio(self, oss, nb_digits, use_space, var_namer=lambda v: v.name): + self._num_to_stringio(oss, self._constant, nb_digits) + + # arithmetic + def __sub__(self, e): + return self.minus(e) + + def __rsub__(self, e): + # e - k = e ! + return e - self._constant + + def __neg__(self): + return self._make_new_constant(- self._constant) + + def __add__(self, other): + return self.plus(other) + + def __radd__(self, other): + return self.plus(other) + + def __mul__(self, other): + return self.times(other) + + def __rmul__(self, other): + return self.times(other) + + def __div__(self, other): + return self._divide(other) + + def __truediv__(self, e): + # for py3 + # INTERNAL + return self.__div__(e) # pragma: no cover + + def _divide(self, other): + self.model._typecheck_as_denominator(numerator=self, denominator=other) + return self._make_new_constant(self._constant / other) + + def __repr__(self): + return 'docplex.mp.linear.ConstantExpr({0})'.format(self._constant) + + def equals_expr(self, other): + return isinstance(other, ConstantExpr) and self._constant == other.constant + + def square(self): + return self._make_new_constant(self._constant ** 2) + + # arithmetci to self + + def _scale(self, factor): + return self._make_new_constant(self._constant * factor) + + def equals(self, other): + if is_number(other): + return self._constant == other + else: + return isinstance(other, LinearOperand) \ + and other.is_constant() and \ + self._constant == other.get_constant() + + # arithmetic to self + def __iadd__(self, other): + return self.add(other) + + def add(self, other): + if is_number(other): + self._constant += other + self.notify_modified(UpdateEvent.ExprConstant) + return self + elif isinstance(other, LinearOperand) and other.is_constant(): + self._constant += other.get_constant() + self.notify_modified(UpdateEvent.ExprConstant) + return self + else: + # replace self by other + self. + added = other.plus(self._constant) + self.notify_replaced(added) + return added + + def subtract(self, other): + if is_number(other): + self._constant -= other + self.notify_modified(UpdateEvent.ExprConstant) + return self + elif isinstance(other, LinearOperand) and other.is_constant(): + self._constant -= other.get_constant() + self.notify_modified(UpdateEvent.ExprConstant) + return self + else: + # replace self by (-other) + self.K + subtracted = other.negate().plus(self._constant) + self.notify_replaced(subtracted) + return subtracted + + def __isub__(self, other): + return self.subtract(other) + + def multiply(self, other): + if is_number(other): + self._constant *= other + self.notify_modified(UpdateEvent.ExprConstant) + return self + elif isinstance(other, LinearOperand) and other.is_constant(): + self._constant *= other.get_constant() + self.notify_modified(UpdateEvent.ExprConstant) + return self + else: + # replace self by (-other) + self.K + multiplied = other * self._constant + self.notify_replaced(multiplied) + return multiplied + + def __imul__(self, other): + return self.multiply(other)
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/model.html b/docs/2.24.232/mp/_modules/docplex/mp/model.html new file mode 100644 index 0000000..307db3c --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/model.html @@ -0,0 +1,7275 @@ + + + + + + + + + docplex.mp.model — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.model

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2018
+# --------------------------------------------------------------------------
+
+# pylint: disable=too-many-lines
+import os
+import sys
+import warnings
+from itertools import chain
+from io import StringIO
+
+from docplex.mp.aggregator import ModelAggregator
+from docplex.mp.constants import SOSType, CplexScope, ObjectiveSense, BasisStatus, EffortLevel,\
+    int_probtype_to_string, ComparisonType
+from docplex.mp.constr import AbstractConstraint, LinearConstraint, RangeConstraint, \
+    IndicatorConstraint, QuadraticConstraint, PwlConstraint, EquivalenceConstraint
+from docplex.mp.context import Context, OverridenOutputContext
+from docplex.mp.dvar import is_var
+
+from docplex.mp.engine_factory import EngineFactory
+from docplex.mp.environment import Environment
+from docplex.mp.error_handler import DefaultErrorHandler, \
+    docplex_add_trivial_infeasible_ct_here, handle_error
+from docplex.mp.format import parse_format
+from docplex.mp.lp_printer import LPModelPrinter
+from docplex.mp.mfactory import ModelFactory
+from docplex.mp.model_stats import ModelStatistics
+from docplex.mp.numutils import round_nearest_towards_infinity1, _NumPrinter, compute_tolerance
+
+from docplex.mp.pwl import PwlFunction
+from docplex.mp.tck import get_typechecker, warn_trivial_feasible, warn_trivial_infeasible, warn_trivial_none
+from docplex.mp.sttck import StaticTypeChecker
+from docplex.mp.utils import DOcplexException, MultiObjective,\
+    DOcplexLimitsExceeded, _var_match_function
+from docplex.mp.utils import is_indexable, is_iterable, is_int, is_string, \
+    make_output_path2, generate_constant, _AutomaticSymbolGenerator, _IndexScope, _to_list, \
+    is_number, str_maxed, normalize_basename, izip2_filled, ordered_sequence_to_list
+from docplex.mp.utils import apply_thread_limitations
+from docplex.mp.vartype import VarType, BinaryVarType, IntegerVarType, \
+    ContinuousVarType, SemiContinuousVarType, SemiIntegerVarType
+from docplex.util.environment import get_environment
+
+
+from docplex.mp.solve_env import CplexLocalSolveEnv
+
+
+# noinspection PyProtectedMember
+
[docs]class Model(object): + """ This is the main class to embed modeling objects. + + The :class:`Model` class acts as a factory to create optimization objects, + decision variables, and constraints. + It provides various accessors and iterators to the modeling objects. + It also manages solving operations and solution management. + + The Model class is a context manager and can be used with the Python `with` statement: + + .. code-block:: python + + with Model() as mdl: + # start modeling... + + When the `with` block is finished, the :func:`end` method is called automatically, and all resources + allocated by the model are destroyed. + If the model is not created with the `with` keyword, then the :func:`end` method should be called to free all CPLEX resources. + + When a model is created without a specified ``context``, a default + ``Context`` is created and initialized as described in :func:`docplex.mp.context.Context.read_settings`. + + Example:: + + # Creates a model named 'my model' with default context + model = Model('my model') + + In this example, we create a model to solve with just 2 threads:: + + context = Context.make_default_context() + context.cplex_parameters.threads = 2 + model = Model(context=context) + + Alternatively, this can be coded as:: + + model = Model() + model.context.cplex_parameters.threads = 2 + + Args: + name (optional): The name of the model. + context (optional): The solve context to be used. If no ``context`` is + passed, a default context is created. + log_output (optional): If ``True``, solver logs are output to + stdout. If this is a stream, solver logs are output to that + stream object. + + checker (optional): If ``off``, then checking is disabled everywhere. Turning off checking + may improve performance but should be done only with extreme caution. + Possible values for the `checker` keyword argument are: + + - `default` (or `std`, or `on`): detects modeling errors, but does not check + numerical values for infinities or NaNs. This is the default value. + - `numeric`: checks that numerical arguments are valid numbers, neither NaN nor + `math.infinity`. This option should be used when data are not trusted. + - `full`: performs all possible checks (This is the union of `std` and `numeric` checks). + - `off`: no typechecking is performed. This options must be used when the model has been + thoroughly tested and numerical data are trusted. + + cts_by_name (optional): a flag which control whether the constraint name dictionary is enabled. + Default is False. + """ + + _name_generator = _AutomaticSymbolGenerator(pattern="docplex_model", offset=1) + + _default_effort_level = EffortLevel.Repair + + @property + def binary_vartype(self): + """ This property returns an instance of :class:`docplex.mp.vartype.BinaryVarType`. + + This type instance is used to build all binary decision variable collections of the model. + """ + return self._binary_vartype + + @property + def integer_vartype(self): + """ This property returns an instance of :class:`docplex.mp.vartype.IntegerVarType`. + + This type instance is used to build all integer variable collections of the model. + """ + return self._integer_vartype + + @property + def continuous_vartype(self): + """ This property returns an instance of :class:`docplex.mp.vartype.ContinuousVarType`. + + This type instance is used to build all continuous variable collections of the model. + """ + return self._continuous_vartype + + @property + def semicontinuous_vartype(self): + """ This property returns an instance of :class:`docplex.mp.vartype.SemiContinuousVarType`. + + This type instance is used to build all semi-continuous variable collections of the model. + """ + return self._semicontinuous_vartype + + @property + def semiinteger_vartype(self): + """ This property returns an instance of :class:`docplex.mp.vartype.SemiIntegerType`. + + This type instance is used to build all semi-integer variable collections of the model. + """ + return self._semiinteger_vartype + + def _vartypes(self): + return (self._binary_vartype, self._integer_vartype, self._continuous_vartype, + self._semicontinuous_vartype, self._semiinteger_vartype) + + def iter_vartypes(self): + return iter(self._vartypes()) + + def _parse_vartype(self, arg): + if isinstance(arg, VarType): + return arg + else: + self._checker.typecheck_string(arg, accept_empty=False, accept_none=False) + argl = arg.lower() + for vt in iter(self.iter_vartypes()): + if argl == vt.short_name.lower() or argl == vt.cplex_typecode.lower(): + return vt + self.fatal("Cannot convert as a variable type: {0!r}", arg) + + def _make_environment(self): + env = Environment.get_default_env() + # rtc-28869 + env.numpy_hook = Model.init_numpy + return env + + def _lazy_get_environment(self): + if self._environment is None: + self._environment = self._make_environment() # pragma: no cover + return self._environment + + _saved_numpy_options = None + + _unknown_status = None + +
[docs] @staticmethod + def init_numpy(): + """ Static method to customize `numpy` for DOcplex. + + This method makes `numpy` aware of DOcplex. + All `numpy` arrays with DOcplex objects will be printed by their string representations + as returned by `str(`) instead of `repr()` as with standard numpy behavior. + + All customizations can be removed by calling the :func:`restore_numpy` method. + + Note: + This method does nothing if `numpy` is not present. + + See Also: + :func:`restore_numpy` + """ + try: + # noinspection PyUnresolvedReferences + import numpy as np + + Model._saved_numpy_options = np.get_printoptions() + np.set_printoptions(formatter={'numpystr': Model._numpy_print_str, 'object': Model._numpy_print_str}) + except ImportError: # pragma: no cover + pass # pragma: no cover
+ + @staticmethod + def _numpy_print_str(arg): + return str(arg) if ModelFactory._is_operand(arg) else repr(arg) + +
[docs] @staticmethod + def restore_numpy(): # pragma: no cover + """ Static method to restore `numpy` to its default state. + + This method is a companion method to :func:`init_numpy`. It restores `numpy` to its original state, + undoing all customizations that were done for DOcplex. + + Note: + This method does nothing if numpy is not present. + + See Also: + :func:`init_numpy` + """ + try: + # noinspection PyUnresolvedReferences + import numpy as np + + if Model._saved_numpy_options is not None: + np.set_printoptions(Model._saved_numpy_options) + except ImportError: # pragma: no cover + pass # pragma: no cover
+ + @property + def environment(self): + # for a closed model with no CPLEX, numpy, etc return ClosedEnvironment + # return get_no_graphics_env() + # from docplex.environment import ClosedEnvironment + # return ClosedEnvironment + return self._lazy_get_environment() + + # ---- type checking + + def _typecheck_var(self, obj): + self._checker.typecheck_var(obj) + + def _typecheck_num(self, arg, caller=None): + self._checker.typecheck_num(arg, caller, safe_number=False) + + def _typecheck_as_denominator(self, denominator, numerator): + StaticTypeChecker.typecheck_as_denominator(self, denominator, numerator) + + def _typecheck_optional_num_seq(self, nums, accept_none=True, expected_size=None, caller=None): + return StaticTypeChecker.typecheck_optional_num_seq(self, nums, accept_none, expected_size, caller) + + + # --- + def unsupported_relational_operator_error(self, left_arg, op, right_arg): + # INTERNAL + self.fatal("Unsupported relational operator: {0!s} {1!s} {2!s}, only <=, ==, >= are allowed", left_arg, op, + right_arg) + + def cannot_be_used_as_denominator_error(self, denominator, numerator): + StaticTypeChecker.cannot_be_used_as_denominator_error(self, denominator, numerator) + + def unsupported_power_error(self, e, power): + self.fatal("Cannot raise {0!s} to the power {1}. A variable's exponent must be 0, 1 or 2.", e, power) + + def _parse_kwargs(self, kwargs): + # parse some arguments from kwargs + for arg_name, arg_val in kwargs.items(): + if arg_name == "float_precision": + self.float_precision = arg_val + elif arg_name in frozenset({'keep_ordering', 'ordering'}): + self._keep_ordering = bool(arg_val) + elif arg_name == "round_solution": + self._round_solution = bool(arg_val) + elif arg_name in frozenset({"info_level", "output_level"}): + self.output_level = arg_val + elif arg_name in {"agent", "solver_agent"}: + self.context.solver.agent = arg_val + elif arg_name == "log_output": + self.context.solver.log_output = arg_val + elif arg_name == "max_str_len": + self._max_str_len = int(arg_val) + elif arg_name == "use_space_str": + self._use_space_str = bool(arg_val) + elif arg_name == "keep_all_exprs": + self._keep_all_exprs = bool(arg_val) + elif arg_name == 'checker': + self._checker_key = arg_val.lower() if is_string(arg_val) else 'default' + elif arg_name == 'full_obj': + self._print_full_obj = bool(arg_val) + elif arg_name == 'lp_line_size': + self._lp_line_length = int(arg_val) + elif arg_name == 'ignore_names': + self._ignore_names = bool(arg_val) + elif arg_name == 'clean_before_solve': + self.clean_before_solve = arg_val + elif arg_name == 'quality_metrics': + self._quality_metrics = bool(arg_val) + elif arg_name in frozenset({'url', 'key'}): + # these two are known, no need to rant + pass + elif arg_name in frozenset({"parameters", 'cplex_parameters'}): + # update parameters either from a params object or a dict + self.context.update_cplex_parameters(arg_val) + elif arg_name == 'cts_by_name': + # safe + pass + else: + self.warning("keyword argument: {0:s}={1!s} - is not recognized (ignored)", arg_name, arg_val) + + def _get_kwargs(self): + kwargs_map = {'float_precision': self.float_precision, + 'keep_ordering': self.keep_ordering, + "round_solution": self._round_solution, + 'output_level': self.output_level, + 'solver_agent': self.solver_agent, + 'log_output': self.log_output, + 'max_str_len': self._max_str_len, + 'use_space_str': self.str_use_space, + 'keep_all_exprs': self._keep_all_exprs, + 'checker': self._checker_key, + 'full_obj': self._print_full_obj, + 'lp_line_size': self._lp_line_length, + 'ignore_names': self._ignore_names, + 'clean_before_solve': self._clean_before_solve + } + return kwargs_map + + + + def _new_engine(self, solver_agent): + return self._make_new_engine_from_agent(solver_agent) + + @classmethod + def _new_linear_factory(cls, mdl, engine): + return ModelFactory(mdl, engine) + + @classmethod + def _new_quadratic_factory(cls, mdl, engine): + from docplex.mp.quadfact import QuadFactory + return QuadFactory(mdl, engine) + + def __init__(self, name=None, context=None, **kwargs): + """Init a new Model. + + Args: + name (optional): The name of the model + context (optional): The solve context to be used. If no ``context`` is + passed, a default context is created. + log_output (optional): if ``True``, solver logs are output to + stdout. If this is a stream, solver logs are output to that + stream object. + """ + if name is None: + name = Model._name_generator.new_symbol() + self._name = name + self._provenance = None + + self._error_handler = DefaultErrorHandler(output_level='warning') + + # type instances + self._binary_vartype = BinaryVarType() + self._integer_vartype = IntegerVarType() + self._continuous_vartype = ContinuousVarType() + self._semicontinuous_vartype = SemiContinuousVarType() + self._semiinteger_vartype = SemiIntegerVarType() + + # + self._container_map = None + self._all_containers = [] + self._origin_map = {} + self._vars_by_name = {} + self._cts_by_name = None + self.__allpwlfuncs = [] + self._benders_annotations = None + self._constraint_priority_dict = {} + + self._lazy_constraints = [] + self._user_cuts = [] + + # -- kpis -- + self._allkpis = [] + + self._progress_listeners = [] + self._qprogress_listeners = [] + self._mipstarts = [] + + # by default, ignore_names is off + self._ignore_names = False + + # clean engine before solve (mip starts) + self._clean_before_solve = False # default is False: faster + + # expression ordering + self._keep_ordering = False + + # -- float formats + self._float_precision = 3 + self._float_meta_format = '{%d:.3f}' + self._num_printer = _NumPrinter(self._float_precision) + + self._environment = self._make_environment() + self_env = self._environment + + # init context + if context is None: + self.context = Context.make_default_context(_env=self_env) + else: + self.context = context + # a flag to indicate whether ot not parameters have been version-checked. + self._synced_params = False + + self._engine_factory = EngineFactory(env=self_env) + + # maximum length for expression in str strings + self._max_str_len = 1e+10 + self._readable_str_len = 48 + + # use spaces in expressions + self._use_space_str = False + + # internal + self._keep_all_exprs = True # use False to get fast clone...with the risk of side effects... + + # full objective lp + self._print_full_obj = False + + # lp line size + self._lp_line_length = 80 + + # checker key + self._checker_key = 'default' + + # quality_metrics + self._quality_metrics = False + + # rond solution or not + self._round_solution = False + self._round_function = round_nearest_towards_infinity1 + + # update from kwargs, before the actual inits. + # pop cts_by name before parse kwargs + _enable_cts_by_name = kwargs.pop('cts_by_name', False) + # ======================================================= + # parse without cts_by_name + self._parse_kwargs(kwargs) + self._cts_by_name = {} if _enable_cts_by_name else None + self._check_mip_for_mipstarts = True + + self._checker = get_typechecker(arg=self._checker_key, logger=self.logger) + + # -- scopes + self._var_scope = _IndexScope("var", cplex_scope=CplexScope.VAR_SCOPE) + self._linct_scope = _IndexScope("linear constraint", + cplex_scope=CplexScope.LINEAR_CT_SCOPE) + self._logical_scope = _IndexScope("logical constraint", + cplex_scope=CplexScope.IND_CT_SCOPE + ) + self._quadct_scope = _IndexScope("quadratic constraint", + cplex_scope=CplexScope.QUAD_CT_SCOPE) + self._pwl_scope = _IndexScope("piecewise constraint", + cplex_scope=CplexScope.PWL_CT_SCOPE) + + self._sos_scope = _IndexScope("SOS", cplex_scope=CplexScope.SOS_SCOPE) + + self._scope_dict = {CplexScope.VAR_SCOPE: self._var_scope, + CplexScope.LINEAR_CT_SCOPE: self._linct_scope, + CplexScope.IND_CT_SCOPE: self._logical_scope, + CplexScope.QUAD_CT_SCOPE: self._quadct_scope, + CplexScope.PWL_CT_SCOPE: self._pwl_scope, + CplexScope.SOS_SCOPE: self._sos_scope + } + + # init engine + engine = self._new_engine(self.solver_agent) + self.__engine = engine + + # after engines + self._lfactory = self._new_linear_factory(self, engine) + self._qfactory = self._new_quadratic_factory(self, engine) + + # after parse kwargs, after factories + self._aggregator = ModelAggregator(self._lfactory, self._qfactory) + + self._quad_count = 0 + self._solution = None + self._solve_details = None + self._last_solve_status = self._unknown_status + + # all the following must be placed after an engine has been set. + self._objective_expr = None + self._multi_objective = MultiObjective.new_empty() + + # engine log level + engineLogLevel = get_environment().get_parameter("oaas.engineLogLevel") + if engineLogLevel is not None and engineLogLevel in {"FINE", "FINER", "FINEST"}: + self.parameters.read.datacheck.set(2) + + self.set_objective(sense=self.default_objective_sense, + expr=self._new_default_objective_expr()) + + model_hook_fn = self.context.model_build_hook + if model_hook_fn: + try: + model_hook_fn(self) + except Exception as me: + print("* Error in model_build_hook: {0!s}".format(me)) + + self._ctstatus_counter = 0 + + def _new_ct_status_index(self): + # INTERNAL + new_ct_status_index = self._ctstatus_counter + 1 + self._ctstatus_counter = new_ct_status_index + return new_ct_status_index + + + @property + def name(self): + """ This property is used to get or set the model name. + """ + return self._name + + def __repr__(self): + return self.to_string() + + def to_string(self): + return "docplex.mp.Model['{0}']".format(self.name) + + def __str__(self): + return self.to_string() + + @property + def lfactory(self): + # INTERNAL + return self._lfactory + + @name.setter + def name(self, name): + self._check_name(name) + self._name = name + + def _check_name(self, new_name): + self._checker.typecheck_string(arg=new_name, accept_empty=False, accept_none=False) + if ' ' in new_name: + self.warning("Model name contains whitespaces: |{0:s}|", new_name) + + @property + def provenance(self): + return self._provenance + + def _get_obj_scope(self, cplex_scope, error='warn'): + # INTERNAL + ct_scope = self._scope_dict.get(cplex_scope) + if not ct_scope and error == 'raise': + raise ValueError("Unexpected scope code: {0}".format(cplex_scope)) + return ct_scope + + def _iter_scopes(self): + # INTERNAL + for _, scope in self._scope_dict.items(): + yield scope + + def _check_scope_indices(self): + for scope in self._iter_scopes(): + scope.check_indices() + + def _iter_constraint_scopes(self): + for cpxsc, scope in self._scope_dict.items(): + if cpxsc.is_constraint_scope(): + yield scope + + def _sync_params(self, params): + # INTERNAL: execute only once + if self.has_cplex(): + params.connect_model(self) + self_env = self._environment + self_cplex_parameters_version = self.context.cplex_parameters.cplex_version + self_engine = self.__engine + installed_cplex_version = self_env.cplex_version + # installed version is different from parameters: reset all defaults + if installed_cplex_version != self_cplex_parameters_version: # pragma: no cover + # cplex is more recent than parameters. must update defaults. + self.info( + "reset parameter defaults, from parameter version: {0} to installed version: {1}" # pragma: no cover + .format(self_cplex_parameters_version, installed_cplex_version)) # pragma: no cover + resets = self_engine._sync_parameter_defaults_from_cplex(params) # pragma: no cover + if resets: + for p, old, new in resets: + if p.name != 'randomseed': # usual practice to change randomseed at each version + self.info('parameter changed, name: {0}, old default: {1}, new default: {2}', + p.name, old, new) + + @property + def infinity(self): + """ This property returns the numerical value used as the upper bound for continuous decision variables. + + Note: + CPLEX usually sets this limit to 1e+20. + """ + return self.__engine.get_infinity() + +
[docs] def get_cplex(self, do_raise=True): + """ Returns the instance of Cplex used by the model, if any. + + In case no local installation of CPLEX can be found, this method either raises an exception, + if parameter `do_raise` is True, or else returns None. + + :param do_raise: An optional flag: if True, raise an exception when no Cplex instance + is available, otherwise return None. + + See Also: + the 'cplex' property calls :func:`get_cplex()` with do_raise=True. + + :return: an instance of Cplex, or None. + """ + return self._get_cplex(do_raise=do_raise)
+ + def _get_cplex(self, do_raise=True, msgfn=None): + try: + cpx = self.__engine.get_cplex() + if cpx: + return cpx + + except DOcplexException: + pass + if do_raise: + if msgfn: + raise_msg = msgfn() + else: + raise_msg = "CPLEX runtime not found - No instance of Cplex is available." + self.fatal(raise_msg) + else: + return None + + @property + def cplex(self): + """ Returns the instance of Cplex used by the model, if any. + + In case no local installation of CPLEX can be found, this method raises an exception., + + :return: a Cplex instance. + + *New in version 2.15* + """ + return self.get_cplex(do_raise=True) + + def has_cplex(self): + return self.get_cplex(do_raise=False) is not None + + def _read_cplex_file(self, name, path, extension, cpx_read_fn): + # INTERNAL + cpx = self._get_cplex(do_raise=True, msgfn=lambda: "CPLEX runtime not found, cannot read CPLEX {0} file: {1}".format(name, path)) + StaticTypeChecker.check_file(self, name=name, path=path, expected_extensions=(extension,)) + cpx_read_fn(cpx, path) + + @property + def cplex_matrix_stats(self): + cpx = self._get_cplex(do_raise=True, msgfn=lambda: "Model.cplex_matrix_stats requires cplex") + return cpx.get_stats() + +
[docs] def read_basis_file(self, bas_path): + """ Read a CPLEX basis status file. + + This method requires the CPLEX runtime. + + :param bas_path: the path of a basis file (extension is '.bas') + + *New in version 2.10* + """ + self._read_cplex_file(name='basis', path=bas_path, + extension='.bas', + cpx_read_fn=lambda cpx_, path_: cpx_.start.read_basis(path_))
+ + +
[docs] def read_priority_order_file(self, ord_path): + """ Read a CPLEX priority order file. + + This method requires the CPLEX runtime. + + :param ord_path: the path of a priority order file (extension is '.ord') + + *New in version 2.10* + """ + self._read_cplex_file(name='priority order', path=ord_path, + extension='.ord', + cpx_read_fn=lambda cpx_, path_: cpx_.order.read(path_))
+ +
[docs] def export_priority_order_file(self, path=None, basename=None): + """ Exports a CPLEX priority order file. + + This method requires the CPLEX runtime. + + Args: + basename: Controls the basename with which the file is printed. + Accepts None, a plain string, or a string format. + If None, the model's name is used. + If passed a plain string, the string is used in place of the model's name. + + path: A path to write the file, expects a string path or None. + Can be a directory, in which case the basename + that was computed with the basename argument, is appended to the directory to produce + the file. + If given a full path, the path is directly used to write the file, and + the basename argument is not used. + If passed None, the output directory will be ``tempfile.gettempdir()``. + + Returns: + The full path of the written file, if successful,, else None. + + *New in version 2.10* + """ + return self._write_cplex_file(name='priority order', path=path, basename=basename, + extension='.ord', + cpx_write_fn=lambda cpx_, path_: cpx_.order.write(path_))
+ + @property + def str_max_len(self): + return self._max_str_len + + @property + def readable_str_len(self): + return self._readable_str_len + + @str_max_len.setter + def str_max_len(self, max_str): + assert max_str >= 1 + self._max_str_len = max_str + + @property + def str_use_space(self): + """ This boolean property controls the use of space separators when displaying the str() + representation of expressions (especially in constraints). + With `str_use_space=False` a constraint is printed as : `2x+3y+5z <= 7` + + With `str_use_space=True` the same constraint is printed as : `2 x + 3 y + 5 z <= 7` + + The default is False, that is print a compact representation. + + :return: True if space separator is used for string representations of expressions. + """ + return self._use_space_str + + @str_use_space.setter + def str_use_space(self, use_space): + self._use_space_str = bool(use_space) + + @property + def str_space(self): + return ' ' if self._use_space_str else '' + + @property + def keep_ordering(self): + return self._keep_ordering + + @keep_ordering.setter + def keep_ordering(self, ordered): # pragma: no cover + # INTERNAL + b_ordered = bool(ordered) + self._keep_ordering = b_ordered + self._lfactory.update_ordering(b_ordered) + self._qfactory.update_ordering(b_ordered) + + @property + def ignore_names(self): + """ This property is used to ignore all names in the model. + + This flag indicates whether names are used or not. + When set to True, all names are ignored. This could lead to performance + improvements when building large models. + The default value of this flag is False. To change its value, add it as + keyword argument when creating the Model instance as in: + + >>> m = Model(name="my_model", ignore_names=True) + + Note: + Once a model instance has been created with `ignore_names=True`, there is no way to restore its names. + This flag only allows to enable or disable name generation while building the model. + """ + return self._ignore_names + + @property + def float_precision(self): + """ This property is used to get or set the float precision of the model. + + The float precision is an integer number of digits, used + in printing the solution and objective. + This number of digits is used for variables and expressions which are not discrete. + Discrete variables and objectives are printed with no decimal digits. + + """ + return self._float_precision + + @float_precision.setter + def float_precision(self, nb_digits): + used_digits = nb_digits + if nb_digits < 0: + self.warning("Negative float precision given: {0}, using 0 instead", nb_digits) + used_digits = 0 + else: + max_digits = self.environment.max_nb_digits + bitness = self.environment.bitness + if nb_digits > max_digits: + self.warning("Given precision of {0:d} goes beyond {1:d}-bit capability, using maximum: {2:d}". + format(nb_digits, bitness, max_digits)) + used_digits = max_digits + self._float_precision = used_digits + # recompute float format + self._float_meta_format = '{%%d:.%df}' % nb_digits + self._num_printer.precision = nb_digits + + @property + def quality_metrics(self): + """ This flag controls whether CPLEX quality metrics are stored into the solve details. + The default is not to store quality metrics. + + *New in version 2.10* + """ + return self._quality_metrics + + @quality_metrics.setter + def quality_metrics(self, use_metrics): + self._quality_metrics = use_metrics + + @property + def clean_before_solve(self): + return self._clean_before_solve + + @clean_before_solve.setter + def clean_before_solve(self, must_clean): + self._clean_before_solve = bool(must_clean) + + @property + def round_solution(self): + """ + This flag controls whether integer and discrete variable values are rounded in solutions, or not. + If not rounded, it may happen that solution value for a binary variable returns 0.99999. + The default is not to round discrete values. + + *New in version 2.15* + """ + return self._round_solution + + @round_solution.setter + def round_solution(self, do_round): + self._round_solution = bool(do_round) + + def _round_element_value_if_necessary(self, elt, elt_value): + # INTERNAL + if self.round_solution and elt_value and elt.is_discrete() and elt_value != int(elt_value): + return self._round_function(elt_value) + else: + return elt_value + + def has_cts_by_name_dict(self): + return self._cts_by_name is not None + + def enable_cts_by_name_dict(self): + self._ensure_cts_name_dir() + + def solved_stopped_by_limit(self): + sd = self.solve_details + return sd and sd.has_hit_limit() + + @property + def time_limit(self): + """ This property is used to get/set the time limit for this model. + """ + return self.time_limit_parameter.get() + + @time_limit.setter + def time_limit(self, new_time_limit): + self.set_time_limit(new_time_limit) + + @property + def time_limit_parameter(self): + # INTERNAL + return self.parameters.timelimit + +
[docs] def get_time_limit(self): + """ + Returns: + The time limit for the model. + + """ + return self.time_limit_parameter.get()
+ +
[docs] def set_time_limit(self, time_limit): + """ Set a time limit for solve operations. + + Args: + time_limit: The new time limit; must be a positive number. + + """ + self._checker.typecheck_num(time_limit) + if time_limit < 0: + self.fatal("Negative time limit: {0}", time_limit) + elif time_limit < 1: + self.warning("Time limit too small: {0} - using 1 instead", time_limit) + time_limit = 1 + else: + pass + + self.time_limit_parameter.set(time_limit)
+ + @property + def lp_line_length(self): + """ This property lets you get or set the maximum line length of LP files generated by DOcplex. + The default is 80. + + *New in version 2.11* + + """ + return self._lp_line_length + + @lp_line_length.setter + def lp_line_length(self, new_length): + if 70 <= new_length <= 512: + self._lp_line_length = new_length + else: + lpz = min(128, max(new_length, 70)) + print(" LP line size set to: {0}, should be in [70..512], {1} was passed".format(lpz, new_length)) + self._lp_line_length = lpz + + @property + def solver_agent(self): + return self.context.solver.agent + + def _set_solver_agent(self, new_agent): + assert new_agent + self.context.solver.agent = new_agent + + @property + def error_handler(self): + return self._error_handler + + @property + def logger(self): + return self._error_handler + + @property + def solution(self): + """ This property returns the current solution of the model or None if the model has not yet been solved + or if the last solve has failed. + """ + return self._solution + + def _get_solution(self): + # INTERNAL + return self._solution + + def new_solution(self, var_value_dict=None, objective_value=None, name=None, **kwargs): + return self._lfactory.new_solution(var_value_dict=var_value_dict, + objective_value=objective_value, name=name, **kwargs) + + +
[docs] def import_solution(self, source_solution, match="auto", error="raise"): + """ Imports a solution from another model. + + There must a a way to map variables from the solution model to the target model, + either by name, index or some other custom manner. + The simplest case is where the other model is a clone of the target model. + In that case, an index-based mapping is used. + + :param source_solution: the imported solution, built on some othe rmodel, + different from target model. + :param match: described the mapping used for variables, accepts either a string for + predefined mappings: "index" for index mapping, "name" for name mapping, or "auto" for + automatic. Also accepts a function taking two arguments: the source variable, and the target model, + returning th eimage of the source variable in the target model. + :param error: A string describing how errors are handled. Accepts "raise", "warn", or "ignore" + + :return: A solution object, instance of :class:`SolveSolution`, built on the target model, + from values and variables mapped from the source model to the target model. + + *New in version 2.21* + """ + target_model = self + find_matching_var = _var_match_function(source_model=source_solution.model, + target_model=target_model, match=match) + source_var_values = {} + source_keep_zeros = source_solution._keep_zeros + for dv, dvv in source_solution.iter_var_values(): + target_var = find_matching_var(dv, target_model) + if target_var is None: + msg = "Cannot find matching variable in target model for {0!r}".format(dv) + handle_error(target_model, error, msg) + elif dvv or source_keep_zeros: + source_var_values[target_var] = dvv + source_obj = source_solution.objective_value + source_solved_by = source_solution.solved_by + + newsol = target_model.new_solution(source_var_values, source_obj, keep_zeros=source_keep_zeros) + newsol._solved_by = source_solved_by + return newsol
+ +
[docs] def populate_solution_pool(self, **kwargs): + """ Populates and return a solution pool. + + returns either a solutiion pool object, or None if the model solve fails. + + This method accepts the same keyword arguments as :meth:`Model.solve`. + See the documentation of :meth:`Model.solve` for more details. + + :return: an instance of :class:`docplex.mp.solution.SolutionPool`, or None. + + See Also: + :class:`docplex.mp.solution.SolutionPool`. + + *New in version 2.16* + """ + self_mname = 'Model.populate_solution_pool' + if not self.has_cplex(): + self.fatal("{0} requires CPLEX, but a local CPLEX installation could not be found" + .format(self_mname)) + pb_type = self._get_cplex_problem_type() + if pb_type not in {'MILP'}: + self.fatal("{0} only for MILP problems, model '{1}' is a {2}", + self_mname, self.name, pb_type) + elif self.has_multi_objective(): + self.fatal("M{0}} is not available for multi-objective problems, model '{1}' has {2} objectives", + self_mname, self.name, self.number_of_multi_objective_exprs) + + context = self.prepare_actual_context(**kwargs) + parameters = apply_thread_limitations(context) + raw_params = self.context._get_raw_cplex_parameters() + + self_engine = self.__engine + sol = None + if raw_params and parameters is not raw_params: + saved_params = {p: p.get() for p in raw_params} + else: + saved_params = {} + + log_stream = context.solver.log_output_as_stream + with OverridenOutputContext(self, log_stream): + + used_clean_before_solve = kwargs.get('clean_before_solve', self.clean_before_solve) + try: + used_parameters = parameters or raw_params + # assert used_parameters is not None + self._apply_parameters_to_engine(used_parameters) + sol, solnpool = self.__engine.populate(clean_before_solve=used_clean_before_solve) + assert (sol is not None) == bool(solnpool) + + finally: + solve_details = self_engine.get_solve_details() + self._notify_solve_hit_limit(solve_details) + self._solve_details = solve_details + self._set_solution(sol) + if saved_params: + for p, v in saved_params.items(): + self_engine.set_parameter(p, v) + + + return solnpool
+ + populate = populate_solution_pool + + def restore_solution(self, sol, restore_all=True): + try: + if self != sol.model: + self.fatal("Model.restore_solution(): Expecting solution attached to model {0}, but attached to {1}" + .format(self.name, sol.model.name)) + # check solution is linked to this model + sol.restore(self, restore_all=restore_all) + except AttributeError: + self.fatal("Model.restore_solution(): Expecting solution, {0!r} was passed", sol) + + def fatal(self, msg, *args): + self._error_handler.fatal(msg, args) + + def fatal_ce_limits(self, *args): + nb_vars = self.number_of_variables + nb_constraints = self.number_of_constraints + + self._error_handler.fatal_limits_exceeded(nb_vars, nb_constraints) + + def error(self, msg, *args): + self._error_handler.error(msg, args) + + def warning(self, msg, *args): + self._error_handler.warning(msg, args) + + def info(self, msg, *args): + self._error_handler.info(msg, args) + + @property + def number_of_warnings(self): + return self._error_handler.number_of_warnings + + @property + def number_of_errors(self): + return self._error_handler._number_of_errors + + + def trace(self, msg, *args): + self.logger.trace(msg, args) + + @property + def output_level(self): + return self._error_handler.get_output_level() + + @output_level.setter + def output_level(self, new_output_level): + self._error_handler.set_output_level(new_output_level) + + def set_quiet(self): + self.logger.set_quiet() + + def set_log_output(self, out=None): + self.context.solver.log_output = out + outs = self.context.solver.log_output_as_stream + self.__engine.set_streams(outs) + + @property + def log_output(self): + return self.context.solver.log_output_as_stream + + @log_output.setter + def log_output(self, out): + self.set_log_output(out) + + def set_log_output_as_stream(self, outs): + self.__engine.set_streams(outs) + + def is_logged(self): + return self.context.solver.log_output_as_stream is not None + + def clear_engine_before_solve(self): + # INTERNAL + try: + self.__engine.clean_before_solve() + except AttributeError: + pass + +
[docs] def clear(self): + """ Clears the model of all modeling objects. + """ + self._clear_internal()
+ + def _clear_internal(self, terminate=False): + self._container_map = None + self._all_containers = [] + self._origin_map = {} + self._vars_by_name = {} + self._cts_by_name = None + self.__allpwlfuncs = [] + self._benders_annotations = None + self._allkpis = [] + self.clear_kpis() + self._last_solve_status = self._unknown_status + self._solution = None + self._mipstarts = [] + self._clear_scopes() + self._lazy_constraints = [] + self._user_cuts = [] + self._quad_count = 0 + if not terminate: + self.set_objective(sense=self.default_objective_sense, + expr=self._new_default_objective_expr()) + self._set_engine(self._make_new_engine_from_agent(self.solver_agent, self.context)) + else: + self._terminate_engine() + + def _clear_scopes(self): + for a_scope in self._iter_scopes(): + a_scope.clear() + + def set_checker(self, checker_key): + # internal + if checker_key != self._checker_key: + new_checker = get_typechecker(arg=checker_key, logger=self.logger) + self._checker_key = checker_key + self._checker = new_checker + self._aggregator._checker = new_checker + self._lfactory._checker = new_checker + self._qfactory._checker = new_checker + + def _make_new_engine_from_agent(self, solver_agent, context=None): + ctx = context or self.context + new_engine = self._engine_factory.new_engine(solver_agent, self.environment, model=self, context=ctx) + new_engine.set_streams(self.context.solver.log_output_as_stream) + return new_engine + + def _set_engine(self, e2): + # INTERNAL + old_engine = self.get_engine() + self.__engine = e2 + self._lfactory.update_engine(e2) + self._qfactory.update_engine(e2) + try: + self._static_terminate_engine(old_engine) + finally: + pass + + def _terminate_engine(self): + # INTERNAL + old_engine = self.__engine + self._static_terminate_engine(old_engine) + self.__engine = None + + @classmethod + def _static_terminate_engine(cls, engine_to_terminate): + if engine_to_terminate is not None: + # dispose of old engine. + engine_to_terminate.end() + # from Ryan + del engine_to_terminate + + def _parse_agent(self, new_agent): + if not new_agent: + new_agent = self.context.solver.agent + elif not is_string(new_agent): + self.fatal('unexpected value for agent: {0!r} was passed, expecting string or None', new_agent) + return new_agent + + def _set_new_engine_from_agent(self, agent_arg): + new_agent = self._parse_agent(agent_arg) + new_engine = self._make_new_engine_from_agent(new_agent, self.context) + self._set_engine(new_engine) + + @property + def solves_with(self): + return self.__engine.name + + def get_engine(self): + # INTERNAL for testing + return self.__engine + +
[docs] def print_information(self): + """ Prints general informational statistics on the model. + + Prints the number of variables and their breakdown by type. + Prints the number of constraints and their breakdown by type. + + """ + print("Model: %s" % self.name) + self.get_statistics().print_information() + + # --- annotations + self_anno = self._benders_annotations + if self_anno: + anno_stats = self.get_annotation_stats() + print(" - annotations: {0}".format(len(self_anno))) + print(" - {0}".format(', '.join('{0}: {1}'.format(cpx.descr, anno_stats[cpx]) for cpx in CplexScope if cpx in anno_stats))) + + + # --- parameters + self_params = self.context._get_raw_cplex_parameters() + if self_params and self_params.has_nondefaults(): + print(" - parameters:") + self_params.print_information(indent_level=5) # 3 for " - " + 2 = 5 + else: + print(" - parameters: defaults") + + # ------------------- objective + minmax = "minimize" if self.is_minimized() else "maximize" + obj_s = minmax + if self.has_multi_objective(): + n_objs = self.number_of_multi_objective_exprs + obj_s = "{0} multiple[{1}]".format(minmax, n_objs) + elif not self.is_optimized(): + obj_s = "none" + else: + try: + if self.objective_expr.is_quad_expr(): + obj_s = "{0} quadratic".format(minmax) + except AttributeError: + pass + print(" - objective: {0}".format(obj_s)) + + # ------------------- problem type + cpx_probtype = self._get_cplex_problem_type(fallback=None, do_raise=False) + if cpx_probtype: + print(" - problem type is: {0}".format(cpx_probtype))
+ + + def _get_cplex_problem_type(self, fallback="unknown", do_raise=False): + # INTERNAL + cpx = self.get_cplex(do_raise=do_raise) + return self._problem_type_from_cplex(cpx, fallback) + + def _problem_type_from_cplex(self, cpx, fallback=None): + # INTERNAL: decode cplex type code. + if cpx: + cpx_probtype_code = cpx.get_problem_type() # this is an int + return int_probtype_to_string(cpx_probtype_code) + else: + return fallback + + @property + def problem_type(self): + """ Returns a string describing the type of problem. + + This method requyires that CPLEX is installed and + available in PYTHONPATH. If the CPLEX runtime cannot be found, an exception is raised. + + Possible values: LP, MILP, QP, MIQP, QCP, MIQCP, + + *New in version 2.20* + """ + cpx = self._get_cplex(do_raise=True, msgfn=lambda: "Model.problem_type requites CPLEX") + return self._problem_type_from_cplex(cpx, fallback=None) + + def __notify_new_model_object(self, descr, + mobj, mindex, mobj_name, + name_dir, idx_scope, + is_name_safe=False): + """ + Notifies the return af an object being create on the engine. + :param descr: A string describing the type of the object being created (e.g. Constraint, Variable). + :param mobj: The newly created modeling object. + :param mindex: The index as returned by the engine. + :param name_dir: The directory of objects by name (e.g. name -> constraint directory). + :param idx_scope: The index scope + """ + mobj._set_index(mindex) + + if name_dir is not None: + mobj_name = mobj_name or mobj.name + if mobj_name: + # in some cases, names are checked before register + if not is_name_safe: + if mobj_name in name_dir: + old_name_value = name_dir[mobj_name] + # Duplicate constraint name: foo + self.warning("Duplicate {0} name: {1} already used for {2!r}", descr, mobj_name, old_name_value) + + name_dir[mobj_name] = mobj + + # store in idx dir if any + if idx_scope: + idx_scope.notify_obj_index(mobj, mindex) + + def _register_one_var(self, var, var_index, var_name): + self.__notify_new_model_object("variable", var, var_index, var_name, self._vars_by_name, self._var_scope) + + # @profile + def _register_block_vars(self, allvars, indices, allnames): + if allnames: + varname_dict = self._vars_by_name + for var, var_index, var_name in zip(allvars, indices, allnames): + var._index = var_index + if var_name: + if var_name in varname_dict: + old_name_value = varname_dict[var_name] + # Duplicate constraint name: foo + self.warning("Duplicate variable name: {0} already used for {1!s}", var_name, old_name_value) + varname_dict[var_name] = var + else: + for var, var_index in zip(allvars, indices): + var._index = var_index + self._var_scope.notify_obj_indices(objs=allvars, indices=indices) + + def _register_one_constraint(self, ct, ct_index, is_ctname_safe=False): + """ + INTERNAL + :param ct: The new constraint to register. + :param ct_index: The index as returned by the engine. + :param is_ctname_safe: True if ct name has been checked for duplicates already. + :return: + """ + self.__notify_new_model_object( + "constraint", ct, ct_index, None, + self._cts_by_name, ct._get_index_scope(), + is_name_safe=is_ctname_safe) + + def _ensure_cts_name_dir(self): + # INTERNAL: make sure the constraint name dir is present. + if self._cts_by_name is None: + self._cts_by_name = {ct.name: ct for ct in self.iter_constraints() if ct.has_user_name()} + return self._cts_by_name + + def _register_block_cts(self, scope, cts, indices): + # INTERNAL: assert len(cts) == len(indices) + ct_name_map = self._cts_by_name + # -- + if ct_name_map: + for ct, ct_index in zip(cts, indices): + ct._set_index(ct_index) + ct_name = ct.name + if ct_name: + ct_name_map[ct_name] = ct + else: + for ct, ct_index in zip(cts, indices): + ct._index = ct_index + + scope.notify_obj_indices(cts, indices) + + def _register_implicit_equivalence_ct(self, eqct, eqctx): + self._register_one_constraint(eqct, eqctx, is_ctname_safe=True) + + # iterators + def _ensure_var_to_container_map(self): + # lazy build of var -> container mapping + ctn_map = self._container_map + if ctn_map is None: + ctn_map = {} + doomed_ctns = set() + for ctn in self.iter_var_containers(): + try: + for dv in ctn: + ctn_map[dv] = ctn + except RuntimeError: # occurs when the dict has been modified + doomed_ctns.add(ctn) + self.warning(f"Modified variable container has been removed, name='{ctn.name}', shape={ctn.shape}") + self._container_map = ctn_map + # remove those var containers + if doomed_ctns: + #print(f"-- removing {len(doomed_ctns)} modified containers") + clean_containers = [c for c in self.iter_var_containers() if c not in doomed_ctns] + self._all_containers = clean_containers + + return ctn_map + + def iter_var_containers(self): + # INTERNAL + return iter(self._all_containers) + + def _get_number_of_var_containers(self): + # INTERNAL + return len(self._all_containers) + + def get_var_container(self, dvar): + # INTERNAL + ctn_map = self._ensure_var_to_container_map() + return ctn_map.get(dvar) + + def set_var_container(self, dvar, ctn): + pass + + def _add_var_container(self, var_container): + # INTERNAL + if var_container is not None: + self._all_containers.append(var_container) + ctn_map = self._container_map + if ctn_map is not None: + # increment var container map, if any + assert isinstance(ctn_map, dict) + for dv in var_container: + ctn_map[dv] = var_container + @staticmethod + def origin_key(obj): + # use id()here to allow case where index is not valid + return id(obj) + + def get_obj_origin(self, obj): + # INTERNAL: retrieve origin of object + objkey = self.origin_key(obj) + return self._origin_map.get(objkey) + + def set_obj_origin(self, obj, new_origin): + # INTERNAL: set origin of object + okey = self.origin_key(obj) + if new_origin is not None: + self._origin_map[okey] = new_origin + elif obj in self._origin_map: + del self._origin_map[okey] + + def _is_binary_var(self, dvar): + return dvar.cplex_typecode == 'B' + + def _is_integer_var(self, dvar): + return dvar.cplex_typecode == 'I' + + def _is_continuous_var(self, dvar): + return dvar.cplex_typecode == 'C' + + def _is_semicontinuous_var(self, dvar): + return dvar.cplex_typecode == 'S' + + def _is_semiinteger_var(self, dvar): + return dvar.cplex_typecode == 'N' + + @property + def number_of_generated_variables(self): + return sum(1 for v in self.iter_variables() if v.is_generated()) + + def _count_variables_w_code(self, cpxcode): + cnt = 0 + for v in self.iter_variables(): + if v.cplex_typecode == cpxcode: + cnt += 1 + return cnt + + def _iter_variables_w_code(self, cpxcode): + for v in self.iter_variables(): + if v.cplex_typecode == cpxcode: + yield v + + @property + def number_of_variables(self): + """ This property returns the total number of decision variables, all types combined. + + """ + return self._var_scope.size + + @property + def number_of_binary_variables(self): + """ This property returns the total number of binary decision variables added to the model. + """ + return self._count_variables_w_code('B') + + @property + def number_of_integer_variables(self): + """ This property returns the total number of integer decision variables added to the model. + """ + return self._count_variables_w_code('I') + + @property + def number_of_continuous_variables(self): + """ This property returns the total number of continuous decision variables added to the model. + """ + return self._count_variables_w_code('C') + + @property + def number_of_semicontinuous_variables(self): + """ This property returns the total number of semi-continuous decision variables added to the model. + """ + return self._count_variables_w_code('S') + + @property + def number_of_semiinteger_variables(self): + """ This property returns the total number of semi-integer decision variables added to the model. + """ + return self._count_variables_w_code('N') + + @property + def number_of_user_variables(self): + return sum(1 for _ in self.generate_user_variables()) + + # def _has_discrete_var(self): + # # INTERNAL + # return any(v.is_discrete for v in self.iter_variables()) + + def _contains_discrete_artefacts(self): + if hasattr(self._lfactory, "_cached_justifier_discrete_var"): + return self._lfactory._cached_justifier_discrete_var is not None + elif self.number_of_sos or self._has_piecewise(): + return True + for v in self.iter_variables(): + if v.cplex_typecode in 'IBNS': + self._lfactory._cached_justifier_discrete_var = v + return True + return False + + def _clear_cached_discrete_var(self): + lfactory = self._lfactory + if hasattr(lfactory, "_cached_justifier_discrete_var"): + lfactory._cached_justifier_discrete_var = None + + def _has_piecewise(self): + return self._pwl_scope.size > 0 + + def _solved_as_mip(self): + # INTERNAL: is the model solved as mip (incl. engine status) + return self._contains_discrete_artefacts() or self.__engine.solved_as_mip() + + def _solved_as_lp(self): + # INTERNAL: is the model solved as mip (incl. engine status) + if self.has_cplex(): + return self.get_engine().solved_as_lp() + else: + return not self._contains_discrete_artefacts() + + def is_quadratic(self): + # returns true if model is quadratic, that is + # either has atleast one quadratic constraint, or has a quadrtic objective. + return self._is_qc() or self._is_qp() + + def _is_qc(self): + return self._quadct_scope.size > 0 + + def _is_qp(self): + if self.has_multi_objective(): + return any(ex.is_quad_expr() for ex in self.iter_multi_objective_exprs()) + else: + return self._objective_expr.is_quad_expr() + + def _make_new_stats(self): + # INTERNAL + from collections import Counter + + vartype_count = Counter(type(dv.vartype) for dv in self.iter_variables()) + nbbvs = vartype_count[BinaryVarType] + nbivs = vartype_count[IntegerVarType] + nbcvs = vartype_count[ContinuousVarType] + nbscvs = vartype_count[SemiContinuousVarType] + nbsivs = vartype_count[SemiIntegerVarType] + + linct_count = Counter(ct.cplex_code for ct in self.iter_binary_constraints()) + nb_le_cts = linct_count['L'] + nb_eq_cts = linct_count['E'] + nb_ge_cts = linct_count['G'] + nb_rng_cts= self.number_of_range_constraints + nb_ind_cts = self.number_of_indicator_constraints + nb_equiv_cts = self.number_of_equivalence_constraints + nb_quad_cts = self.number_of_quadratic_constraints + stats = ModelStatistics(nbbvs, nbivs, nbcvs, nbscvs, nbsivs, + nb_le_cts, nb_ge_cts, nb_eq_cts, nb_rng_cts, + nb_ind_cts, nb_equiv_cts, nb_quad_cts) + return stats + + @property + def statistics(self): + """ Returns statistics on the model. + + :returns: A new instance of :class:`docplex.mp.model_stats.ModelStatistics`. + """ + return self._make_new_stats() + + def get_statistics(self): + return self.statistics + +
[docs] def iter_pwl_functions(self): + """ Iterates over all the piecewise linear functions in the model. + + Returns the PWL functions in the order they were added to the model. + + Returns: + An iterator object. + """ + return iter(self.__allpwlfuncs)
+ +
[docs] def iter_variables(self): + """ Iterates over all the variables in the model. + + Returns the variables in the order they were added to the model, + regardless of their type. + + Returns: + An iterator object. + """ + return self._var_scope.iter_objects()
+ +
[docs] def iter_binary_vars(self): + """ Iterates over all binary decision variables in the model. + + Returns the variables in the order they were added to the model. + + Returns: + An iterator object. + """ + return self._iter_variables_w_code('B')
+ +
[docs] def iter_integer_vars(self): + """ Iterates over all integer decision variables in the model. + + Returns the variables in the order they were added to the model. + + Returns: + An iterator object. + """ + return self._iter_variables_w_code('I')
+ +
[docs] def iter_continuous_vars(self): + """ Iterates over all continuous decision variables in the model. + + Returns the variables in the order they were added to the model. + + Returns: + An iterator object. + """ + return self._iter_variables_w_code('C')
+ +
[docs] def iter_semicontinuous_vars(self): + """ Iterates over all semi-continuous decision variables in the model. + + Returns the variables in the order they were added to the model. + + Returns: + An iterator object. + """ + return self._iter_variables_w_code('S')
+ +
[docs] def iter_semiinteger_vars(self): + """ Iterates over all semi-integer decision variables in the model. + + Returns the variables in the order they were added to the model. + + Returns: + An iterator object. + """ + return self._iter_variables_w_code('N')
+ + +
[docs] def get_var_by_name(self, name): + """ Searches for a variable from a name. + + Returns a variable if it finds one with exactly this name, or None. + + Args: + name (str): The name of the variable being searched for. + + :returns: A variable (instance of :class:`docplex.mp.dvar.Var`) or None. + """ + return self._vars_by_name.get(name, None)
+ + def _disambiguate_varname(self, candidate_name): + # INTERNAL + varname_map = self._vars_by_name + if varname_map is None: + return candidate_name + safe_name = candidate_name + p = 1 + while safe_name in varname_map: + safe_name = f"{candidate_name}#{p}" + p += 1 + if p >= 1001: + raise DOcplexException(f"disambiguation fails for name: {candidate_name}") + return safe_name + + def generate_user_variables(self): + # internal + for dv in self.iter_variables(): + if not dv.is_generated(): + yield dv + + def generate_user_linear_constraints(self): + # internal + for lct in self.iter_linear_constraints(): + if not lct.is_generated(): + yield lct + +
[docs] def find_matching_vars(self, pattern, match_case=False): + """ Finds all variables whose name contain a given string + + This method searches for all variables whose name + is not null and contains the passed ``pattern`` string. Anonymous variables + are not considered. + + :param pattern: a non-empty string. + :param match_case: optional flag to match case (or not). Default is to not match case. + + :return: A list of decision variables. + """ + return self._find_matching_objs(self.generate_user_variables, pattern, match_case, caller='Model.find_matching_vars')
+ +
[docs] def find_re_matching_vars(self, regexpr): + """ Finds all variables whose name match a regular expression. + + This method searches for all variables with a name that + matches the given regular expression. Anonymous variables + are not counted as matching. + + :param regexpr: a regular expression, as define din module ``re`` + + :return: A list of decision variables. + + *New in version 2.9* + """ + matches = [] + for dv in self.generate_user_variables(): + dvname = dv.name + if dvname and regexpr.match(dvname): + matches.append(dv) + return matches
+ + def _find_matching_objs(self, obj_iter, pattern, match_case=False, caller=None): + # internal + self._checker.typecheck_string(pattern, accept_empty=False, accept_none=False, caller=caller) + key_pattern = pattern if match_case else pattern.lower() + matches = [] + for obj in obj_iter(): + obj_name = obj.name + if obj_name: + matched = obj_name if match_case else obj_name.lower() + if key_pattern in matched: + matches.append(obj) + return matches + +
[docs] def find_matching_linear_constraints(self, pattern, match_case=False): + """ Finds all linear constraints whose name contain a given string + + This method searches for all linear constraints whose name + is not empty and contains the passed ``pattern`` string. Anonymous linear constraints + are not considered. + + :param pattern: a non-empty string. + :param match_case: optional flag to match case (or not). Default is to not match case. + + :return: A list of linear constraints. + + *New in version 2.9* + """ + return self._find_matching_objs(self.generate_user_linear_constraints, pattern, match_case, + caller='Model.find_matching_linear_constraints')
+ +
[docs] def find_matching_quadratic_constraints(self, pattern, match_case=False): + """ Finds all quadratic constraints whose name contain a given string + + This method searches for all quadratic constraints whose name + is not empty and contains the passed ``pattern`` string. Anonymous constraints + are not considered. + + :param pattern: a non-empty string. + :param match_case: optional flag to match case (or not). Default is to not match case. + + :return: A list of quadratic constraints. + + *New in version 2.23* + """ + return self._find_matching_objs(self.iter_quadratic_constraints, pattern, match_case, + caller='Model.find_matching_quadratic_constraints')
+ + + def get_var_by_index(self, idx): + # INTERNAL + return self._var_by_index(idx) + + def _var_by_index(self, idx): + # INTERNAL + return self._var_scope.get_object_by_index(idx) + + def _set_var_type(self, dvar, new_vartype_): + # INTERNAL + new_vartype = self._parse_vartype(new_vartype_) + if new_vartype != dvar.vartype: + self._change_var_types_internal((dvar,), (new_vartype,)) + return dvar + + def change_var_types(self, dvars, vartype_args): + parsefn = self._parse_vartype + checked_vars = list(self._checker.typecheck_var_seq(dvars, caller="Model.change_var_types")) + if is_iterable(vartype_args, accept_string=False): + new_vartypes = [parsefn(vt_arg) for vt_arg in vartype_args] + else: + new_vartype1 = parsefn(vartype_args) + new_vartypes = [new_vartype1] * len(checked_vars) + self._change_var_types_internal(checked_vars, new_vartypes) + + def _change_var_types_internal(self, dvars, new_vartypes): + assert isinstance(dvars, (list, tuple)) + # one batch call to engine + self.__engine.change_var_types(dvars, new_vartypes) + # update bounds, if necessary + for dv, nvt in zip(dvars, new_vartypes): + dv._set_vartype_internal(nvt) + self._update_var_bounds_from_type(dv, nvt) + self._clear_cached_discrete_var() + + def set_var_name(self, dvar, new_name): + # INTERNAL: use var.name to set variable names + if new_name != dvar.name: + self.__engine.rename_var(dvar, new_name) + dvar._set_name(new_name) + + def set_linear_constraint_name(self, linct, new_name): + # INTERNAL: use lct.name to set a linear constraint's name + if new_name != linct.name: + self.__engine.rename_linear_constraint(linct, new_name) + linct._set_name(new_name) + + # ---- batch operations on variable bounds ---------------- +
[docs] def change_var_lower_bounds(self, dvars, lbs, check_bounds=True): + """ Changes lower bounds for a collection of variables in one call. + + + :param dvars: an iterable over decision variables (a list, or a comprehension) + :param lbs: accepts either an iterable over numbers, a single number, + in which case the new bound is applied to all variables, + or None. If passed None, the lower bound of each variable is reset to + its type's default. + :param check_bounds: an optional flag to enable or disable checking of + new lower bounds (default is True: check) + + The logic for checking new lower bounds is as follows: new bounds are checked if and only if + `check` is True and the model checker is not 'off'. + + Example: + >>> ivars = m.integer_var_list(3, lb=7) + >>> m.change_var_lower_bounds(ivars, [1,2,3]) # sets lower bounds to 1,2,3 resp. + + *New in 2.20* + + """ + checker = self._checker + var_list = checker.typecheck_var_seq(dvars, vtype=None, caller='Model.change_variable_lower_bounds') + if is_iterable(lbs): + lbs_ = (float(lb_) for lb_ in checker.typecheck_num_seq(lbs)) + elif lbs is None: + lbs_ = (dv.vartype.default_lb for dv in dvars) + else: + checker.typecheck_num(lbs, caller='Model.change_variable_lower_bounds') + lbs_ = generate_constant(float(lbs), count_max=None) + + def checked_lb(dvar, candidate_lb): + return dvar.vartype.resolve_lb(candidate_lb, self) + + def to_float2(dvar, candidate_lb): + return candidate_lb + + if check_bounds and checker.check_new_variable_bound(): + # check bounds if not forced disable AND if checker allows it. + bound_transformer = checked_lb + else: + bound_transformer = to_float2 + + var_lb_dict = {dv: bound_transformer(dv, lb) for dv, lb in zip(var_list, lbs_)} + + engine = self.get_engine() + engine.change_var_lbs(var_lb_dict) + # now update vars + for dv, lb2 in var_lb_dict.items(): + dv._lb = lb2
+ +
[docs] def change_var_upper_bounds(self, dvars, ubs, check_bounds=True): + """ Changes upper bounds for a collection of variables in one call. + + :param dvars: an iterable over decision variables (a list, or a comprehension) + :param ubs: accepts either an iterable over numbers, a single number, + in which case the new bound is applied to all variables, + or None. If passed None, the upper bound of each variable is reset to + its type's default. + :param check_bounds: an optional flag to enable or disable checking of + new upper bounds (default is True: check) + + The logic for checking new upper bounds is as follows: new bounds are checked if and only if + `check` is True and the model checker is not 'off'. + + Example: + >>> ivars = m.integer_var_list(3, lb=7) + >>> m.change_var_upper_bounds(ivars, [101,102,103]) # sets upper bounds to 1,01,102,103 resp. + + *New in 2.20* + + """ + checker = self._checker + var_list = checker.typecheck_var_seq(dvars, vtype=None, caller='Model.change_variable_upper_bounds') + if is_iterable(ubs): + ubs_ = (float(ub_) for ub_ in checker.typecheck_num_seq(ubs)) + elif ubs is None: + ubs_ = (dv.vartype.default_ub for dv in dvars) + else: + checker.typecheck_num(ubs, caller='Model.change_variable_upper_bounds') + ubs_ = generate_constant(float(ubs), count_max=None) + + def checked_ub(dvar, candidate_ub): + return dvar.vartype.resolve_ub(candidate_ub, self) + def to_float2(dvar, candidate_ub): + return candidate_ub + + if check_bounds and checker.check_new_variable_bound(): + # check bounds if not forced disable AND if checker allows it. + bound_transformer = checked_ub + else: + bound_transformer = to_float2 + var_ub_dict = {dv: bound_transformer(dv, ub) for dv, ub in zip(var_list, ubs_)} + + self.get_engine().change_var_ubs(var_ub_dict) + # now update vars + for dv, ub2 in var_ub_dict.items(): + dv._ub = ub2
+ + def set_var_lb(self, var, candidate_lb): + # INTERNAL: use var.lb to set lb + new_lb = var.vartype.resolve_lb(candidate_lb, self) + self._set_var_lb(var, new_lb) + return new_lb + + def _set_var_lb(self, var, new_lb): + # INTERNAL + self.__engine.set_var_lb(var, new_lb) + var._internal_set_lb(new_lb) + + def set_var_ub(self, var, candidate_ub): + # INTERNAL: use var.ub to set ub + new_ub = var.vartype.resolve_ub(candidate_ub, self) + self._set_var_ub(var, new_ub) + return new_ub + + def _set_var_ub(self, var, new_ub): + # INTERNAL + self.__engine.set_var_ub(var, new_ub) + var._internal_set_ub(new_ub) + + + def _update_var_bounds_from_type(self, dvar, new_vartype, force_binary01=False): + # INTERNAL + old_lb, old_ub = dvar.lb, dvar.ub + if new_vartype == self.binary_vartype and force_binary01: + new_lb, new_ub = 0, 1 + else: + new_lb = new_vartype.resolve_lb(old_lb, logger=self) + new_ub = new_vartype.resolve_ub(old_ub, logger=self) + if new_lb != old_lb: + self._set_var_lb(dvar, new_lb) + if new_ub != old_ub: + self._set_var_ub(dvar, new_ub) + +
[docs] def get_constraint_by_name(self, name): + """ Searches for a constraint from a name. + + Returns the constraint if it finds a constraint with exactly this name, or None + if no constraint has this name. + + This function will not raise an exception if the named constraint is not found. + + Note: + The constraint name dicitonary in class Model is disabled by default. However, + calling `get_constraint_by_name` will compute one dicitonary on the fly, + but without warning for duplicate names. To enable the constraint + name dicitonary from the start (and get duplicate constraint messages), + add the `cts_by_name` keyword argument when creating the model, as in + + >>> m = Model(name='my_model', cts_by_name=True) + + This enables the constraint name dicitonary, and checks for duplicates when a named + constraint is added. + + Args: + name (str): The name of the constraint being searched for. + + Returns: + A constraint or None. + """ + return self._ensure_cts_name_dir().get(name)
+ +
[docs] def get_constraint_by_index(self, idx): + """ Searches for a linear constraint from an index. + + Returns the linear constraint with `idx` as index, or None. + This function will not raise an exception if no constraint with this index is found. + + Note: remember that linear constraints, logical constraints, and quadratic constraints + each have separate index spaces. + + :param idx: a valid index (greater than 0). + + :return: A linear constraint, or None. + """ + return self._linct_scope.get_object_by_index(idx, self._checker)
+ + def get_logical_constraint_by_index(self, idx): + return self._logical_scope.get_object_by_index(idx, self._checker) + + def get_pwl_constraint_by_index(self, idx): + return self._pwl_scope.get_object_by_index(idx, self._checker) + +
[docs] def get_quadratic_constraint_by_index(self, idx): + """ Searches for a quadratic constraint from an index. + + Returns the quadratic constraint with `idx` as index, or None. + This function will not raise an exception if no constraint with this index is found. + + Note: remember that linear constraints, logical constraints, and quadratic constraints + each have separate index spaces. Therefore, a model can contain both a linear constraint + and a quadratic constrait having index 0 + + :param idx: a valid index (greater than 0). + + :return: A quadratic constraint, or None. + """ + return self._quadct_scope.get_object_by_index(idx, self._checker)
+ + @property + def number_of_constraints(self): + """ This property returns the total number of constraints that were added to the model. + + The number includes linear constraints, range constraints, and indicator constraints. + """ + return sum(scope.size for scope in self._iter_constraint_scopes()) + + @property + def number_of_user_constraints(self): + """ This property returns the total number of constraints that were + explicitly added tothe model, not including generated constraints. + + The number includes all types of constraints. + """ + return sum(1 for ct in self.iter_constraints() if not ct.is_generated()) + +
[docs] def iter_constraints(self): + """ Iterates over all constraints (linear, ranges, indicators). + + Returns: + An iterator object over all constraints in the model. + """ + for sc in self._iter_constraint_scopes(): + for obj in sc.iter_objects(): + yield obj
+ + def _count_constraints_with_type(self, scope, cttype): + return scope.count_filtered(pred=lambda ct: isinstance(ct, cttype)) + + @property + def number_of_range_constraints(self): + """ This property returns the total number of range constraints added to the model. + + """ + return self._count_constraints_with_type(self._linct_scope, RangeConstraint) + + @property + def number_of_linear_constraints(self): + """ This property returns the total number of linear constraints added to the model. + + This counts binary linear constraints (<=, >=, or ==) and + range constraints. + + See Also: + :func:`number_of_range_constraints` + + """ + return self._linct_scope.size + +
[docs] def iter_range_constraints(self): + """ + Returns an iterator on the range constraints of the model. + + Returns: + An iterator object. + """ + return self._linct_scope.generate_objects_with_type(RangeConstraint)
+ +
[docs] def iter_binary_constraints(self): + """ + Returns an iterator on the binary constraints (expr1 <op> expr2) of the model. + This does not include range constraints. + + Returns: + An iterator object. + """ + return self._linct_scope.generate_objects_with_type(LinearConstraint)
+ +
[docs] def iter_linear_constraints(self): + """ + Returns an iterator on the linear constraints of the model. + This includes binary linear constraints and ranges but not indicators. + + Returns: + An iterator object. + """ + for c in self.iter_constraints(): + if c.is_linear(): + yield c
+ + + @property + def number_of_nonzeros(self): + return sum(lct.size for lct in self.iter_linear_constraints()) + +
[docs] def iter_indicator_constraints(self): + """ Returns an iterator on indicator constraints in the model. + + Returns: + An iterator object. + """ + return self._logical_scope.generate_objects_with_type(IndicatorConstraint)
+ +
[docs] def iter_equivalence_constraints(self): + """ Returns an iterator on equivalence constraints in the model. + + Returns: + An iterator object. + """ + return self._logical_scope.generate_objects_with_type(EquivalenceConstraint)
+ + @property + def number_of_indicator_constraints(self): + """ This property returns the number of indicator constraints in the model. + """ + return self._count_constraints_with_type(self._logical_scope, IndicatorConstraint) + + @property + def number_of_equivalence_constraints(self): + """ This property returns the number of equivalence constraints in the model. + """ + return self._count_constraints_with_type(self._logical_scope, EquivalenceConstraint) + +
[docs] def iter_quadratic_constraints(self): + """ + Returns an iterator on the quadratic constraints of the model. + + Returns: + An iterator object. + """ + return self._quadct_scope.iter_objects()
+ + @property + def number_of_quadratic_constraints(self): + """ This property returns the number of quadratic constraints in the model. + """ + return self._quadct_scope.size + + def has_quadratic_constraint(self): + return self._quadct_scope.size > 0 + + def iter_logical_constraints(self): + return self._logical_scope.iter_objects() + +
[docs] def var(self, vartype, lb=None, ub=None, name=None): + """ Creates a decision variable and stores it in the model. + + Args: + vartype: The type of the decision variable; + This field expects a concrete instance of the abstract class + :class:`docplex.mp.vartype.VarType`. + + lb: The lower bound of the variable; either a number or None, to use the default. + The default lower bound for all three variable types is 0. + + ub: The upper bound of the variable domain; expects either a number or None to use the type's default. + The default upper bound for Binary is 1, otherwise positive infinity. + + name: An optional string to name the variable. + + :returns: The newly created decision variable. + :rtype: :class:`docplex.mp.dvar.Var` + + Note: + The model holds local instances of BinaryVarType, IntegerVarType, ContinuousVarType which + are accessible by properties (resp. binary_vartype, integer_vartype, continuous_vartype). + + See Also: + :attr:`infinity`, + :attr:`binary_vartype`, + :attr:`integer_vartype`, + :attr:`continuous_vartype` + + """ + self._checker.typecheck_vartype(vartype) + return self._var(vartype, lb, ub, name)
+ + def _var(self, vartype, lb=None, ub=None, name=None): + # INTERNAL + if lb is not None: + self._checker.typecheck_num(lb, caller='Var.lb') + if ub is not None: + self._checker.typecheck_num(ub, caller='Var.ub') + return self._lfactory.new_var(vartype, lb, ub, name) + +
[docs] def continuous_var(self, lb=None, ub=None, name=None): + """ Creates a new continuous decision variable and stores it in the model. + + Args: + lb: The lower bound of the variable, or None. The default is 0. + ub: The upper bound of the variable, or None, to use the default. The default is model infinity. + name (string): An optional name for the variable. + + :returns: A decision variable with type :class:`docplex.mp.vartype.ContinuousVarType`. + :rtype: :class:`docplex.mp.dvar.Var` + """ + return self._var(self.continuous_vartype, lb, ub, name)
+ +
[docs] def integer_var(self, lb=None, ub=None, name=None): + """ Creates a new integer variable and stores it in the model. + + Args: + lb: The lower bound of the variable, or None. The default is 0. + ub: The upper bound of the variable, or None, to use the default. The default is model infinity. + name: An optional name for the variable. + + :returns: An instance of the :class:`docplex.mp.dvar.Var` class with type `IntegerVarType`. + :rtype: :class:`docplex.mp.dvar.Var` + """ + return self._var(self.integer_vartype, lb, ub, name)
+ +
[docs] def binary_var(self, name=None): + """ Creates a new binary decision variable and stores it in the model. + + Args: + name (string): An optional name for the variable. + + :returns: A decision variable with type :class:`docplex.mp.vartype.BinaryVarType`. + :rtype: :class:`docplex.mp.dvar.Var` + """ + return self._var(self.binary_vartype, name=name)
+ +
[docs] def semicontinuous_var(self, lb, ub=None, name=None): + """ Creates a new semi-continuous decision variable and stores it in the model. + + Args: + lb: The lower bound of the variable (which must be strictly positive). + ub: The upper bound of the variable, or None, to use the default. The default is model infinity. + name (string): An optional name for the variable. + + :returns: A decision variable with type :class:`docplex.mp.vartype.SemiContinuousVarType`. + :rtype: :class:`docplex.mp.dvar.Var` + """ + self._checker.typecheck_num(lb) # lb cannot be None + return self._var(self.semicontinuous_vartype, lb, ub, name)
+ +
[docs] def semiinteger_var(self, lb, ub=None, name=None): + """ Creates a new semi-integer decision variable and stores it in the model. + + Args: + lb: The lower bound of the variable (which must be strictly positive). + ub: The upper bound of the variable, or None, to use the default. The default is model infinity. + name (string): An optional name for the variable. + + :returns: A decision variable with type :class:`docplex.mp.vartype.SemiIntegerVarType`. + :rtype: :class:`docplex.mp.dvar.Var` + """ + self._checker.typecheck_num(lb) # lb cannot be None + return self._var(self.semiinteger_vartype, lb, ub, name)
+ + def var_list(self, keys, vartype, lb=None, ub=None, name=str, key_format=None): + self._checker.typecheck_vartype(vartype) + return self._var_list(keys, vartype, lb, ub, name, key_format) + + def _var_list(self, keys, vartype, lb=None, ub=None, name=str, key_format=None): + return self._lfactory.var_list(keys, vartype, lb, ub, name, key_format) + + def var_dict(self, keys, vartype, lb=None, ub=None, name=str, key_format=None): + self._checker.typecheck_vartype(vartype) + return self._var_dict(keys, vartype, lb, ub, name, key_format) + + def _var_dict(self, keys, vartype, lb=None, ub=None, name=str, key_format=None): + return self._lfactory.new_var_dict(keys, vartype, lb, ub, name, key_format, ordered=self._keep_ordering) + +
[docs] def binary_var_list(self, keys, lb=None, ub=None, name=str, key_format=None): + """ Creates a list of binary decision variables and stores them in the model. + + Args: + keys: Either a sequence of objects or an integer. + name: Used to name variables. Accepts either a string or + a function. If given a string, the variable name is formed by appending the string + to the string representation of the key object (if keys is a sequence) or the + index of the variable within the range, if an integer argument is passed. + key_format: A format string or None. This format string describes how keys contribute to variable names. + The default is "_%s". For example if name is "x" and each key object is represented by a string + like "k1", "k2", ... then variables will be named "x_k1", "x_k2",... + + Example: + If you want each key string to be surrounded by {}, use a special key_format: "_{%s}", + the %s denotes where the key string will be formatted and appended to `name`. + + :returns: A list of :class:`docplex.mp.dvar.Var` objects with type :class:`doc.mp.vartype.BinaryVarType`. + + Example: + `mdl.binary_var_list(3, "z")` returns a list of size 3 + containing three binary decision variables with names `z_0`, `z_1`, `z_2`. + + """ + return self._var_list(keys, self.binary_vartype, name=name, lb=lb, ub=ub, key_format=key_format)
+ +
[docs] def integer_var_list(self, keys, lb=None, ub=None, name=str, key_format=None): + """ Creates a list of integer decision variables with type `IntegerVarType`, stores them in the model, + and returns the list. + + Args: + keys: Either a sequence of objects or a positive integer. If passed an integer, + it is interpreted as the number of variables to create. + lb: Lower bounds of the variables. Accepts either a floating-point number, + a list of numbers with the same size as keys, + a function (which will be called on each key argument), or None. + ub: Upper bounds of the variables. Accepts either a floating-point number, + a list of numbers with the same size as keys, + a function (which will be called on each key argument), or None. + name: Used to name variables. Accepts either a string or + a function. If given a string, the variable name is formed by appending the string + to the string representation of the key object (if `keys` is a sequence) or the + index of the variable within the range, if an integer argument is passed. + key_format: A format string or None. This format string describes how keys contribute to variable names. + The default is "_%s". For example if name is "x" and each key object is represented by a string + like "k1", "k2", ... then variables will be named "x_k1", "x_k2",... + + Note: + Using None as the lower bound means the default lower bound (0) is used. + Using None as the upper bound means the default upper bound (the model's positive infinity) + is used. + + :returns: A list of :class:`doc.mp.linear.Var` objects with type `IntegerVarType`. + + Example: + >>> m.integer_var_list(3, name="ij") + returns a list of three integer variables from 0 to 1e+20, named ij_0, ij_1, ij_2. + The default behavior, when a string argument is passed as name, + is to concatenate the string , a "-" separator and a string representation + of the key; this allows to build arbitrary name strings from keys. + + >>> m.integer_var_list(3, name=lambda i: "__name_{}__".format(i)) + uses a functional name argument, producing names: "__name_0__", + "__name_1__", "_name__2__". + + >>> m.integer_var_list(3, name="q", lb=1, ub=100) + returns a list of three integer variables from 1 to 100, named q_0, q_1, q_3 + + >>> m.integer_var_list([1,2,3], name="q", lb=lambda k: k, ub=lambda k: k*k+1) + returns a list of three integer variables q_1[1,1**1 +1], q2[2, 2*2+1], q3[3, 3*3+1] + + The last example use the functional lb and ub, to compute bounds that + depend on the keys. + """ + return self._var_list(keys, self.integer_vartype, lb, ub, name, key_format)
+ +
[docs] def continuous_var_list(self, keys, lb=None, ub=None, name=str, key_format=None): + """ + Creates a list of continuous decision variables with type :class:`docplex.mp.vartype.ContinuousVarType`, + stores them in the model, and returns the list. + + Args: + keys: Either a sequence of objects or a positive integer. If passed an integer, + it is interpreted as the number of variables to create. + + lb: Lower bounds of the variables. Accepts either a floating-point number, + a list of numbers, a function, or None. + Use a number if all variables share the same lower bound. + Otherwise either use an explicit list of numbers + or use a function if lower bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + None means using the default lower bound (0) is used. + + ub: Upper bounds of the variables. Accepts either a floating-point number, + a list of numbers, a function, or None. + Use a number if all variables share the same upper bound. + Otherwise either use an explicit list of numbers + or use a function if upper bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + None means the default upper bound (model infinity) is used. + + name: Used to name variables. Accepts either a string or + a function. If given a string, the variable name is formed by appending the string + to the string representation of the key object (if `keys` is a sequence) or the + index of the variable within the range, if an integer argument is passed. + If passed a function, this function is called on each key object to generate a name. + The default behavior is to call :func:`str()` on each key object. + + key_format: A format string or None. This format string describes how keys contribute to variable names. + The default is "_%s". For example if name is "x" and each key object is represented by a string + like "k1", "k2", ... then variables will be named "x_k1", "x_k2",... + Note: + When `keys` is either an empty list or the integer 0, an empty list is returned. + + + :returns: A list of :class:`docplex.mp.dvar.Var` objects with type :class:`docplex.mp.vartype.ContinuousVarType`. + + See Also: + :attr:`infinity` + + """ + return self._var_list(keys, self.continuous_vartype, lb, ub, name, key_format)
+ +
[docs] def semicontinuous_var_list(self, keys, lb, ub=None, name=str, key_format=None): + """ + Creates a list of semi-continuous decision variables with type :class:`docplex.mp.vartype.SemiContinuousVarType`, + stores them in the model, and returns the list. + + Args: + keys: Either a sequence of objects or a positive integer. If passed an integer, + it is interpreted as the number of variables to create. + + lb: Lower bounds of the variables. Accepts either a floating-point number, + a list of numbers, or a function. + Use a number if all variables share the same lower bound. + Otherwise either use an explicit list of numbers or + use a function if lower bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + Note that the lower bound of a semi-continuous variable must be strictly positive. + + ub: Upper bounds of the variables. Accepts either a floating-point number, + a list of numbers, a function, or None. + Use a number if all variables share the same upper bound. + Otherwise either use an explicit list of numbers or + use a function if upper bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + None means the default upper bound (model infinity) is used. + + name: Used to name variables. Accepts either a string or + a function. If given a string, the variable name is formed by appending the string + to the string representation of the key object (if `keys` is a sequence) or the + index of the variable within the range, if an integer argument is passed. + If passed a function, this function is called on each key object to generate a name. + The default behavior is to call :func:`str()` on each key object. + + key_format: A format string or None. This format string describes how keys contribute to variable names. + The default is "_%s". For example if name is "x" and each key object is represented by a string + like "k1", "k2", ... then variables will be named "x_k1", "x_k2",... + Note: + When `keys` is either an empty list or the integer 0, an empty list is returned. + + + :returns: A list of :class:`docplex.mp.dvar.Var` objects with type :class:`docplex.mp.vartype.SemiContinuousVarType`. + + See Also: + :attr:`infinity` + + """ + return self._var_list(keys, self.semicontinuous_vartype, lb, ub, name, key_format)
+ +
[docs] def semiinteger_var_list(self, keys, lb, ub=None, name=str, key_format=None): + """ + Creates a list of semi-integer decision variables with type :class:`docplex.mp.vartype.SemiIntegerVarType`, + stores them in the model, and returns the list. + + Args: + keys: Either a sequence of objects or a positive integer. If passed an integer, + it is interpreted as the number of variables to create. + + lb: Lower bounds of the variables. Accepts either a floating-point number, + a list of numbers, or a function. + Use a number if all variables share the same lower bound. + Otherwise either use an explicit list of numbers or + use a function if lower bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + Note that the lower bound of a semi-integer variable must be strictly positive. + + ub: Upper bounds of the variables. Accepts either a floating-point number, + a list of numbers, a function, or None. + Use a number if all variables share the same upper bound. + Otherwise either use an explicit list of numbers or + use a function if upper bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + None means the default upper bound (model infinity) is used. + + name: Used to name variables. Accepts either a string or + a function. If given a string, the variable name is formed by appending the string + to the string representation of the key object (if `keys` is a sequence) or the + index of the variable within the range, if an integer argument is passed. + If passed a function, this function is called on each key object to generate a name. + The default behavior is to call :func:`str()` on each key object. + + key_format: A format string or None. This format string describes how keys contribute to variable names. + The default is "_%s". For example if name is "x" and each key object is represented by a string + like "k1", "k2", ... then variables will be named "x_k1", "x_k2",... + Note: + When `keys` is either an empty list or the integer 0, an empty list is returned. + + + :returns: A list of :class:`docplex.mp.dvar.Var` objects with type :class:`docplex.mp.vartype.SemiIntegerVarType`. + + See Also: + :attr:`infinity` + + """ + return self._var_list(keys, self.semiinteger_vartype, lb, ub, name, key_format)
+ +
[docs] def continuous_var_dict(self, keys, lb=None, ub=None, name=None, key_format=None): + """ Creates a dictionary of continuous decision variables, indexed by key objects. + + Creates a dictionary that allows retrieval of variables from business + model objects. Keys can be either a Python collection, an iterator, or a generator. + + A key can be any Python object, with the exception of None. + Keys are used in the naming of variables. + + Note: + If `keys` is empty, this method returns an empty dictionary. + The returned dictionary should not be modified. + + Args: + keys: Either a sequence of objects, an iterator, or a positive integer. If passed an integer, + it is interpreted as the number of variables to create. + + lb: Lower bounds of the variables. Accepts either a floating-point number, + a list of numbers, or a function. + Use a number if all variables share the same lower bound. + Otherwise either use an explicit list of numbers or + use a function if lower bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + None means the default lower bound (0) is used. + + ub: Upper bounds of the variables. Accepts either a floating-point number, + a list of numbers, a function, or None. + Use a number if all variables share the same upper bound. + Otherwise either use an explicit list of numbers or + use a function if upper bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + None means the default upper bound (model infinity) is used. + + name: Used to name variables. Accepts either a string or + a function. If given a string, the variable name is formed by appending the string + to the string representation of the key object (if `keys` is a sequence) or the + index of the variable within the range, if an integer argument is passed. + If passed a function, this function is called on each key object to generate a name. + The default behavior is to call :func:`str` on each key object. + + key_format: A format string or None. This format string describes how `keys` contribute to variable names. + The default is "_%s". For example if name is "x" and each key object is represented by a string + like "k1", "k2", ... then variables will be named "x_k1", "x_k2",... + + :returns: A dictionary of :class:`docplex.mp.dvar.Var` objects (with type `ContinuousVarType`) indexed by + the objects in `keys`. + + See Also: + :class:`docplex.mp.dvar.Var`, + :attr:`infinity` + """ + return self._var_dict(keys, self.continuous_vartype, lb=lb, ub=ub, name=name, key_format=key_format)
+ +
[docs] def integer_var_dict(self, keys, lb=None, ub=None, name=None, key_format=None): + """ Creates a dictionary of integer decision variables, indexed by key objects. + + Creates a dictionary that allows retrieval of variables from business + model objects. Keys can be either a Python collection, an iterator, or a generator. + + A key can be any Python object, with the exception of None. + Keys are used in the naming of variables. + + Note: + If `keys` is empty, this method returns an empty dictionary. + The returned dictionary should not be modified. + + Args: + keys: Either a sequence of objects, an iterator, or a positive integer. If passed an integer, + it is interpreted as the number of variables to create. + + lb: Lower bounds of the variables. Accepts either a floating-point number, + a list of numbers, or a function. + Use a number if all variables share the same lower bound. + Otherwise either use an explicit list of numbers or + use a function if lower bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + None means the default lower bound (0) is used. + + ub: Upper bounds of the variables. Accepts either a floating-point number, + a list of numbers, a function, or None. + Use a number if all variables share the same upper bound. + Otherwise either use an explicit list of numbers or + use a function if upper bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + None means the default upper bound (model infinity) is used. + + + name: Used to name variables. Accepts either a string or + a function. If given a string, the variable name is formed by appending the string + to the string representation of the key object (if `keys` is a sequence) or the + index of the variable within the range, if an integer argument is passed. + If passed a function, this function is called on each key object to generate a name. + The default behavior is to call :func:`str` on each key object. + + key_format: A format string or None. This format string describes how keys contribute to variable names. + The default is "_%s". For example if name is "x" and each key object is represented by a string + like "k1", "k2", ... then variables will be named "x_k1", "x_k2",... + + :returns: A dictionary of :class:`docplex.mp.dvar.Var` objects (with type `IntegerVarType`) indexed by the + objects in `keys`. + + See Also: + :attr:`infinity` + """ + return self._var_dict(keys, self.integer_vartype, lb=lb, ub=ub, name=name, key_format=key_format)
+ +
[docs] def binary_var_dict(self, keys, lb=None, ub=None, name=None, key_format=None): + """ Creates a dictionary of binary decision variables, indexed by key objects. + + Creates a dictionary that allows retrieval of variables from business + model objects. Keys can be either a Python collection, an iterator, or a generator. + + A key can be any Python object, with the exception of None. + Keys are used in the naming of variables. + + Note: + If `keys` is empty, this method returns an empty dictionary. + The returned dictionary should not be modified. + + Args: + keys: Either a sequence of objects, an iterator, or a positive integer. If passed an integer, + it is interpreted as the number of variables to create. + + name (string): Used to name variables. Accepts either a string or + a function. If given a string, the variable name is formed by appending the string + to the string representation of the key object (if `keys` is a sequence) or the + index of the variable within the range, if an integer argument is passed. + + key_format: A format string or None. This format string describes how keys contribute to variable names. + The default is "_%s". For example if name is "x" and each key object is represented by a string + like "k1", "k2", ... then variables will be named "x_k1", "x_k2",... + + :returns: A dictionary of :class:`docplex.mp.dvar.Var` objects with type + :class:`docplex.mp.vartype.BinaryVarType` indexed by the objects in `keys`. + """ + return self._var_dict(keys, self.binary_vartype, lb=lb, ub=ub, name=name, key_format=key_format)
+ +
[docs] def semiinteger_var_dict(self, keys, lb, ub=None, name=str, key_format=None): + """ Creates a dictionary of semi-integer decision variables, indexed by key objects. + + Creates a dictionary that allows retrieval of variables from business + model objects. Keys can be either a Python collection, an iterator, or a generator. + + A key can be any Python object, with the exception of None. + Keys are used in the naming of variables. + + Note: + If `keys` is empty, this method returns an empty dictionary. + The returned dictionary should not be modified. + + Args: + keys: Either a sequence of objects, an iterator, or a positive integer. If passed an integer, + it is interpreted as the number of variables to create. + + lb: Lower bounds of the variables. Accepts either a floating-point number, + a list of numbers, or a function. + Use a number if all variables share the same lower bound. + Otherwise either use an explicit list of numbers or + use a function if lower bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + + ub: Upper bounds of the variables. Accepts either a floating-point number, + a list of numbers, a function, or None. + Use a number if all variables share the same upper bound. + Otherwise either use an explicit list of numbers or + use a function if upper bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + None means the default upper bound (model infinity) is used. + + + name: Used to name variables. Accepts either a string or + a function. If given a string, the variable name is formed by appending the string + to the string representation of the key object (if `keys` is a sequence) or the + index of the variable within the range, if an integer argument is passed. + If passed a function, this function is called on each key object to generate a name. + The default behavior is to call :func:`str` on each key object. + + key_format: A format string or None. This format string describes how keys contribute to variable names. + The default is "_%s". For example if name is "x" and each key object is represented by a string + like "k1", "k2", ... then variables will be named "x_k1", "x_k2",... + + :returns: A dictionary of :class:`docplex.mp.dvar.Var` objects (with type `SemiIntegerVarType`) indexed by the + objects in `keys`. + + See Also: + :attr:`infinity` + """ + return self._var_dict(keys, self.semiinteger_vartype, lb, ub, name, key_format)
+ +
[docs] def semiinteger_var_matrix(self, keys1, keys2, lb, ub=None, name=None, key_format=None): + """ Creates a dictionary of semiinteger decision variables, indexed by pairs of key objects. + + Creates a dictionary that allows the retrieval of variables from a tuple + of two keys, the first one from `keys1`, the second one from `keys2`. + In short, variables are indexed by the Cartesian product of the two key sets. + + A key can be any Python object, with the exception of None. + + Arguments `lb`, `ub`, `name`, and `key_format` are interpreted as in :func:`semiinteger_var_dict`. + + *New in version 2.9* + """ + return self._var_multidict(self.semiinteger_vartype, [keys1, keys2], lb, ub, name, key_format)
+ +
[docs] def semicontinuous_var_dict(self, keys, lb, ub=None, name=str, key_format=None): + """ Creates a dictionary of semi-continuous decision variables, indexed by key objects. + + Creates a dictionary that allows retrieval of variables from business + model objects. Keys can be either a Python collection, an iterator, or a generator. + + A key can be any Python object, with the exception of None. + Keys are used in the naming of variables. + + Note: + If `keys` is empty, this method returns an empty dictionary. + The returned dictionary should not be modified. + + Args: + keys: Either a sequence of objects, an iterator, or a positive integer. If passed an integer, + it is interpreted as the number of variables to create. + + lb: Lower bounds of the variables. Accepts either a floating-point number, + a list of numbers, or a function. + Use a number if all variables share the same lower bound. + Otherwise either use an explicit list of numbers or + use a function if lower bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + + ub: Upper bounds of the variables. Accepts either a floating-point number, + a list of numbers, a function, or None. + Use a number if all variables share the same upper bound. + Otherwise either use an explicit list of numbers or + use a function if upper bounds vary depending on the key, in which case, + the function will be called on each `key` in `keys`. + None means the default upper bound (model infinity) is used. + + + name: Used to name variables. Accepts either a string or + a function. If given a string, the variable name is formed by appending the string + to the string representation of the key object (if `keys` is a sequence) or the + index of the variable within the range, if an integer argument is passed. + If passed a function, this function is called on each key object to generate a name. + The default behavior is to call :func:`str` on each key object. + + key_format: A format string or None. This format string describes how keys contribute to variable names. + The default is "_%s". For example if name is "x" and each key object is represented by a string + like "k1", "k2", ... then variables will be named "x_k1", "x_k2",... + + :returns: A dictionary of :class:`docplex.mp.dvar.Var` objects (with type `SemiIntegerVarType`) indexed by the + objects in `keys`. + + See Also: + :attr:`infinity` + """ + return self._var_dict(keys, self.semicontinuous_vartype, lb, ub, name, key_format)
+ +
[docs] def semicontinuous_var_matrix(self, keys1, keys2, lb, ub=None, name=None, key_format=None): + """ Creates a dictionary of semicontinuous decision variables, indexed by pairs of key objects. + + Creates a dictionary that allows the retrieval of variables from a tuple + of two keys, the first one from `keys1`, the second one from `keys2`. + In short, variables are indexed by the Cartesian product of the two key sets. + + A key can be any Python object, with the exception of None. + + Arguments `lb`, `ub`, `name`, and `key_format` are interpreted as in :func:`semiinteger_var_dict`. + + *New in version 2.9* + """ + return self._var_multidict(self.semicontinuous_vartype, [keys1, keys2], lb, ub, name, key_format)
+ +
[docs] def var_hypercube(self, vartype_spec, seq_of_keys, lb=None, ub=None, name=None, key_format=None): + """ + Creates a dictionary of decision variables, indexed by tuples + of arbitrary size. + + Arguments are analogous to methods of the type xxx_var_matrix, + except a type argument has to be passed. + + Args: + vartype_spec: type specificsation: accepts either an instance of class + `docplex.mp.VarType`, or a string that can be translated into a vartype. + Possible strings are: + + - cplex type codes, e.g. B,I,C,N,S or type short names + (e.g.: binary, integer, continuous, semicontinuous, semiinteger) + + seq_of_keys: a sequence of sequence of keys. Typically of length >= 4, + as other dimensions are handled by the 'list', 'matrix' and 'cube' + series of methods. + Variables are indexed by tuples formed by the cartesian product of elements + form the sequences; all sequences of keys must be non-empty. + + All other arguments have the same meaning as for all the "xx_var_matrix" family of methods. + + Example: + >>> hc = Model().var_hypercube(vartype_spec='B', seq_of_keys=[[1,2], [3], ['a','b'], [1,2,3,4]] + >>> len(hc) + 16 + returns a dict of 2x2x4 = 16 variables indexed by tuples formed by the cartesian product + of the four lists, for example (1,3,'a',4)is a valid key for the hypercube. + + *New in 2.19* + + See Also: + :class:`docplex.mp.vartype.VarType` + + """ + vartype = self._parse_vartype(vartype_spec) + self._checker.typecheck_vartype(vartype) + self._checker.typecheck_iterable(seq_of_keys) + lkeys = list(seq_of_keys) + arity = len(lkeys) + if arity == 0: + self.fatal("Variable hypercube with zero dimension") + + return self._var_multidict(vartype, lkeys, lb, ub, name, key_format)
+ + def _var_multidict(self, vartype, keys, lb=None, ub=None, name=None, key_format=None): + assert isinstance(vartype, VarType) + return self._lfactory.new_var_multidict(keys, vartype, lb, ub, name, key_format, ordered=self._keep_ordering) + + def var_matrix(self, vartype, keys1, keys2, lb=None, ub=None, name=None, key_format=None): + return self._var_multidict(vartype, keys=[keys1, keys2], + lb=lb, ub=ub, name=name, key_format=key_format) + +
[docs] def binary_var_matrix(self, keys1, keys2, name=None, key_format=None): + """ Creates a dictionary of binary decision variables, indexed by pairs of key objects. + + Creates a dictionary that allows the retrieval of variables from a tuple + of two keys, the first one from `keys1`, the second one from `keys2`. + In short, variables are indexed by the Cartesian product of the two key sets. + + A key can be any Python object, with the exception of None. + Keys are used in the naming of variables. + + Note: + If either of `keys1` or `keys2` is empty, this method returns an empty dictionary. + + Args: + keys1: Either a sequence of objects, an iterator, or a positive integer. If passed an integer N, + it is interpreted as a range from 0 to N-1. + + keys2: Same as `keys1`. + + name: Used to name variables. Accepts either a string or + a function. If given a string, the variable name is formed by appending the string + to the string representation of the key object (if keys is a sequence) or the + index of the variable within the range, if an integer argument is passed. + + key_format: A format string or None. This format string describes how keys contribute to variable names. + The default is "_%s". For example if name is "x" and each key object is represented by a string + like "k1", "k2", ... then variables will be named "x_k1", "x_k2",... + + :returns: A dictionary of :class:`docplex.mp.dvar.Var` objects with type + :class:`docplex.mp.vartype.BinaryVarType` indexed by + all couples `(k1, k2)` with `k1` in `keys1` and `k2` in `keys2`. + """ + return self._var_multidict(self.binary_vartype, [keys1, keys2], 0, 1, name=name, key_format=key_format)
+ +
[docs] def integer_var_matrix(self, keys1, keys2, lb=None, ub=None, name=None, key_format=None): + """ Creates a dictionary of integer decision variables, indexed by pairs of key objects. + + Creates a dictionary that allows the retrieval of variables from a tuple + of two keys, the first one from `keys1`, the second one from `keys2`. + In short, variables are indexed by the Cartesian product of the two key sets. + + A key can be any Python object, with the exception of None. + + Arguments `lb`, `ub`, `name`, and `key_format` are interpreted as in :func:`integer_var_dict`. + """ + return self._var_multidict(self.integer_vartype, [keys1, keys2], lb, ub, name, key_format)
+ +
[docs] def continuous_var_matrix(self, keys1, keys2, lb=None, ub=None, name=None, key_format=None): + """ Creates a dictionary of continuous decision variables, indexed by pairs of key objects. + + Creates a dictionary that allows retrieval of variables from a tuple + of two keys, the first one from `keys1`, the second one from `keys2`. + In short, variables are indexed by the Cartesian product of the two key sets. + + A key can be any Python object, with the exception of None. + + Arguments `lb`, `ub`, `name`, and `key_format` are interpreted the same as in :func:`integer_var_dict`. + + """ + return self._var_multidict(self.continuous_vartype, [keys1, keys2], lb, ub, name, key_format)
+ +
[docs] def continuous_var_cube(self, keys1, keys2, keys3, lb=None, ub=None, name=None, key_format=None): + """ Creates a dictionary of continuous decision variables, indexed by triplets of key objects. + + Same as :func:`continuous_var_matrix`, except that variables are indexed by triplets of + the form `(k1, k2, k3)` with `k1` in `keys1`, `k2` in `keys2`, `k3` in `keys3`. + """ + return self._var_multidict(self.continuous_vartype, [keys1, keys2, keys3], lb, ub, name, key_format)
+ +
[docs] def integer_var_cube(self, keys1, keys2, keys3, lb=None, ub=None, name=str): + """ Creates a dictionary of integer decision variables, indexed by triplets. + + Same as :func:`integer_var_matrix`, except that variables are indexed by triplets of + the form `(k1, k2, k3)` with `k1` in `keys1`, `k2` in `keys2`, `k3` in `keys3`. + + See Also: + :func:`integer_var_matrix` + """ + return self._var_multidict(self.integer_vartype, [keys1, keys2, keys3], lb, ub, name)
+ +
[docs] def binary_var_cube(self, keys1, keys2, keys3, name=None, key_format=None): + """Creates a dictionary of binary decision variables, indexed by triplets. + + Same as :func:`binary_var_matrix`, except that variables are indexed by triplets of + the form `(k1, k2, k3)` with `k1` in `keys1`, `k2` in `keys2`, `k3` in `keys3`. + + :returns: A dictionary of :class:`docplex.mp.dvar.Var` objects (with type :class:`docplex.mp.vartype.BinaryVarType`) indexed by + triplets. + + """ + return self._var_multidict(self.binary_vartype, [keys1, keys2, keys3], name=name, key_format=key_format)
+ +
[docs] def linear_expr(self, arg=None, constant=0, name=None): + ''' Returns a new empty linear expression. + + Args: + arg: an optional argument to convert to a linear expression. Detailt is None, + in which case, an empty expression is returned. + + :returns: An instance of :class:`docplex.mp.linear.LinearExpr`. + ''' + self._checker.typecheck_num(arg=constant, caller='Model.linear_expr()') + if name: + warnings.warn("Naming expressions is deprecated -- ignored") + return self._lfactory.linear_expr(arg=arg, constant=constant)
+ +
[docs] def quad_expr(self, name=None): + ''' Returns a new empty quadratic expression. + + :returns: An empty instance of :class:`docplex.mp.quad.QuadExpr`. + ''' + if name: + if self.is_docplex_debug(): + raise RuntimeError + warnings.warn("Naming expressions is deprecated, use a variable if necessary") + return self._qfactory.new_quad()
+ +
[docs] def abs(self, e): + """ Builds an expression equal to the absolute value of its argument. + + Args: + e: Accepts any object that can be transformed into an expression: + decision variables, expressions, or numbers. + + Returns: + An expression that can be used in arithmetic operators and constraints. + + Note: + Building the expression generates auxiliary decision variables, including binary decision variables, + and this may change the nature of the problem from a LP to a MIP. + + """ + self._checker.typecheck_operand(e, caller="Model.abs", accept_numbers=True) + return self._lfactory.new_abs_expr(e)
+ +
[docs] def min(self, *args): + """ Builds an expression equal to the minimum value of its arguments. + + This method accepts a variable number of arguments. + + If no arguments are provided, returns positive infinity (see :attr:`infinity`). + + Args: + args: A variable list of arguments, each of which is either an expression, a variable, + or a container. + + If passed a container or an iterator, this container or iterator must be the unique argument. + + If passed one dictionary, returns the minimum of the dictionary values. + + + Returns: + An expression that can be used in arithmetic operators and constraints. + + Example: + `model.min()` -> returns `model.infinity`. + + `model.min(e)` -> returns `e`. + + `model.min(e1, e2, e3)` -> returns a new expression equal to the minimum of the values of `e1`, `e2`, `e3`. + + `model.min([x1,x2,x3])` where `x1`, `x2` .. are variables or expressions -> returns the minimum of these expressions. + + `model.min([])` -> returns `model.infinity`. + + Note: + Building the expression generates auxiliary variables, including binary decision variables, + and this may change the nature of the problem from a LP to a MIP. + """ + min_args = args + nb_args = len(args) + if 0 == nb_args: + pass + elif 1 == nb_args: + unique_arg = args[0] + if is_iterable(unique_arg): + if isinstance(unique_arg, dict): + min_args = unique_arg.values() + else: + min_args = _to_list(unique_arg) + for a in min_args: + self._checker.typecheck_operand(a, caller="Model.min()") + else: + self._checker.typecheck_operand(unique_arg, caller="Model.min()") + + else: + for arg in args: + self._checker.typecheck_operand(arg, caller="Model.min") + + return self._lfactory.new_min_expr(*min_args)
+ +
[docs] def max(self, *args): + """ Builds an expression equal to the maximum value of its arguments. + + This method accepts a variable number of arguments. + + Args: + args: A variable list of arguments, each of which is either an expression, a variable, + or a container. + + If passed a container or an iterator, this container or iterator must be the unique argument. + + If passed one dictionary, returns the maximum of the dictionary values. + + If no arguments are provided, returns negative infinity (see :attr:`infinity`). + + + Example: + `model.max()` -> returns `-model.infinity`. + + `model.max(e)` -> returns `e`. + + `model.max(e1, e2, e3)` -> returns a new expression equal to the maximum of the values of `e1`, `e2`, `e3`. + + `model.max([x1,x2,x3])` where `x1`, `x2` .. are variables or expressions -> returns the maximum of these expressions. + + `model.max([])` -> returns `-model.infinity`. + + + Note: + Building the expression generates auxiliary variables, including binary decision variables, + and this may change the nature of the problem from a LP to a MIP. + """ + max_args = args + nb_args = len(args) + if 0 == nb_args: + pass + elif 1 == nb_args: + unique_arg = args[0] + if is_iterable(unique_arg): + if isinstance(unique_arg, dict): + max_args = unique_arg.values() + else: + max_args = _to_list(unique_arg) + for a in max_args: + self._checker.typecheck_operand(a, caller="Model.max") + else: + self._checker.typecheck_operand(unique_arg, caller="Model.max") + else: + for arg in args: + self._checker.typecheck_operand(arg, caller="Model.max") + + return self._lfactory.new_max_expr(*max_args)
+ + +
[docs] def logical_and(self, *args): + """ Builds an expression equal to the logical AND value of its arguments. + + This method takes a variable number of arguments, and accepts + binary variables, other logical expressions, or discrete constraints. + + Args: + args: A variable list of logical operands. + + Note: + If passed an empty number of arguments, this method an expression equal to 1. + + Returns: + An expression, equal to 1 if and only if all of its + arguments are equal to 1, else equal to 0. + + See Also: + :func:`logical_or` + :func:`logical_not` + + Example:: + + # return logical XOR or two binary variables. + def logxor(m, b1, b2): + return m.logical_and(m.logical_or(b1, b2), m.logical_not(m.logical_and(b1, b2))) + + """ + bvars = self._checker.typecheck_logical_op_seq(args, caller='Model.logical_and') + return self._lfactory.new_logical_and_expr(bvars)
+ +
[docs] def logical_or(self, *args): + """ Builds an expression equal to the logical OR value of its arguments. + + This method takes a variable number of arguments, and accepts + binary variables, other logical expressions, or discrete constraints. + + Args: + args: A variable list of logical operands. + + Note: + If passed an empty number of arguments, this method a zero expression. + + Returns: + An expression, equal to 1 if and only if at least one of its + arguments is equal to 1, else equal to 0. + + See Also: + :func:`logical_and` + :func:`logical_not` + + *New in version 2.11* + + """ + bvars = self._checker.typecheck_logical_op_seq(args, caller='Model.logical_or') + return self._lfactory.new_logical_or_expr(bvars)
+ +
[docs] def logical_not(self, arg): + """ Builds an expression equal to the logical negation of its argument. + + This method accepts either a binary variable, or another logical expression. + + Args: + arg: A binary variable, or a logical expression, + e.g. an expression built by logical_and, logical_or, logical_not + + Returns: + An expression, equal to 1 if its argument is 0, else 0. + + See Also: + :func:`logical_and` + :func:`logical_or` + + """ + StaticTypeChecker.typecheck_logical_op(self, arg, 'Model.logical_not') + return self._lfactory.new_logical_not_expr(arg)
+ +
[docs] def scal_prod(self, terms, coefs): + """ + Creates a linear expression equal to the scalar product of a sequence of decision variables + and a sequence of coefficients. + + This method accepts different types of input for both arguments. `terms` can be any + iterable returning expressions or variables, and `coefs` is usually + an iterable returning numbers. + `cal_prod` also accept one number as `coefs`, in which case + the scalar product reduces to a sum times this coefficient. + + :param terms: An iterable returning variables or expressions. + :param coefs: An iterable returning numbers, or a number. + + Note: + - both iterables are iterated at the same time, so the order in which terms and numbers + are returned must be consistent: using unordered collections (e.g. sets) could lead to unexpected results. + + - Iteration stops as soon as one iterable stops. If both iterables are empty, the method returns 0. + + :return: A linear expression or 0. + """ + self._checker.check_ordered_sequence(arg=terms, caller='Model.scal_prod() requires a list of expressions/variables') + return self._aggregator.scal_prod(terms, coefs)
+ +
[docs] def dot(self, terms, coefs): + """ Synonym for :func:`scal_prod`. + + """ + return self.scal_prod(terms, coefs)
+ +
[docs] def dotf(self, var_dict, coef_fn, assume_alldifferent=True): + """ Creates a scalar product from a dictionary of variables and a function. + + This method is a functional variant of `dot`. I takes as asrgument a dictionary of variables, + as returned by xxx_var_dict or xx_var_var_matrix (where xxx is a type), and a function. + + :param var_dict: a dictionary of variables, as returned by all the xxx_var_dict methods (e.g. integer_var_dict), + but also multi-dimensional dictionaries, such as xxx_var_matrix (or var_cube). + :param coef_fn: A function that takes one dictionary key and returns anumber. One-dimension dictionaries (such + as integer_var_dict) have plain object as keys, but multi-dimensional dictionaries have tuples keys. + For example, a binary_var_matrix returns a dictionary, the keys of which are 2-tuples. + :param assume_alldifferent: an optional flag whichi ndicates whether variables values in the dictionary + can be assumed to be all different. This is true when the dicitonary has been built by Docplex's + Model.xxx_var-dict(), and thi sis the default behavior. + For a custom-built dictionary, set the flag to False. A wrong flag value may yield incorrect results. + + :return: an expression, built as a scalar product of all variables in the dictionay, multiplied by the result of the function. + + Examples: + + >>> m1 = m.binary_var_matrix(keys1=range(1,3), keys2=range(1,3), name='b') + >>> s = m.dotf(m1, lambda keys: keys[0] + keys[1]) + + returns 2 b_1_1 + 3 b_1_2 +3 b_2_1 + 4 b_2_2 + + """ + StaticTypeChecker.typecheck_callable\ + (self, coef_fn, + "Functional scalar product requires a function taking variable keys as argument. A non-callable was passed: {0!r}".format( + coef_fn)) + return self._aggregator._scal_prod_f(var_dict, coef_fn, assume_alldifferent)
+ + scal_prod_f = dotf + + +
[docs] def scal_prod_vars_all_different(self, terms, coefs): + """ Fastly creates a scalar product from a dictionary of variables and a list of coefficients or a coefficient. + :param terms: An iterable returning variables or expressions. + :param coefs: An iterable returning numbers, or a number. + """ + self._checker.check_ordered_sequence(arg=terms, + caller='Model.scal_prod() requires a list of expressions/variables') + var_seq = self._checker.typecheck_var_seq_all_different(terms) + return self._aggregator._scal_prod_vars_all_different(var_seq, coefs)
+ +
[docs] def sum(self, args): + """ Creates an expression summing over an iterable over expressions or variables. + This method expects an iterable over any type of expression: quadrayic expression, + linear expression, variables, constants. + + Note: + The returned expression is quadratic as soon as the result is quadratic, otherwise it returns + a linear expression, or 0 if the iterable is empty. + + :param args: An iterable over expressions (quadratic or linear), variables, or constants. + + :return: A Docplex expression. + """ + return self._aggregator.sum(args)
+ +
[docs] def sums(self, *args): + """ Same as `Model.sum()` but accepts a variable number of arguments. + + :param args: A variable number of expressions (quadratic or linear), variables, or constants. + :return: A Docplex expression. + + Example: + Assuming x is a variable. + >>> m.sums(x**2, x, 1) + is identical to + >> m.sum([x**2, x, 1]) + + Both return a quadratic expression "x^2+x+1" + + *New in version 2.22* + """ + return self.sum(args)
+ +
[docs] def sumsq(self, args): + """ Creates a quadratic expression summing squares of expressions. + + Each element of the list is squared and added to the result. Quadratic expressions + are not accepted, as they cannot be squared. + + Note: + This method returns 0 if the argument is an empty list or iterator. + + :param args: An iterable returning linear expressions, variables or numbers. + + :return: A quadratic expression (possibly constant). + """ + return self._aggregator.sumsq(args)
+ + sum_squares = sumsq + +
[docs] def sum_vars(self, dvars): + """ Creates a linear expression that sums variables. + + This method is faster than `Model.sum()` but accepts only variables. + + :param dvars: an iterable returning variables. + + :return: a linear expression equal to the sum of the variables. + + *New in version 2.10* + """ + return self._aggregator._sum_vars(dvars)
+ + +
[docs] def sum_vars_all_different(self, terms): + """ + Creates a linear expression equal to sum of a list of decision variables. + The variable sequence is a list or an iterator of variables. + + This method is faster than the standard generic summation method due to the fact that it takes only + variables and does not take expressions as arguments. + + :param terms: A list or an iterator on variables only, with no duplicates. + + :return: a linear expression equal to the sum of the variables. + + Note: + If the variable iteration contains duplicates, this function returns an incorrect result. + + """ + if isinstance(terms, dict): + return self.sum_vars_all_different(terms.values()) + else: + var_seq = self._checker.typecheck_var_seq_all_different(terms) + return self._aggregator._sum_vars_all_different(var_seq)
+ +
[docs] def le_constraint(self, lhs, rhs, name=None): + """ Creates a "less than or equal to" linear constraint. + + Note: + This method returns a constraint object, that is not added to the model. + To add it to the model, use the :func:`add_constraint` method. + + Args: + lhs: An object that can be converted to a linear expression, typically a variable, + a member of an expression. + rhs: An object that can be converted to a linear expression, typically a variable, + a member of an expression. + name (string): An optional string to name the constraint. + + :returns: An instance of :class:`docplex.mp.linear.LinearConstraint`. + """ + return self._lfactory.new_le_constraint(lhs, rhs, name)
+ +
[docs] def ge_constraint(self, lhs, rhs, name=None): + """ Creates a "greater than or equal to" linear constraint. + + Note: + This method returns a constraint object that is not added to the model. + To add it to the model, use the :func:`add_constraint` method. + + Args: + lhs: An object that can be converted to a linear expression, typically a variable, + a member of an expression. + rhs: An object that can be converted to a linear expression, typically a variable, + a number of an expression. + name (string): An optional string to name the constraint. + + :returns: An instance of :class:`docplex.mp.linear.LinearConstraint`. + """ + return self._lfactory.new_ge_constraint(lhs, rhs, name)
+ +
[docs] def eq_constraint(self, lhs, rhs, name=None): + """ Creates an equality constraint. + + Note: + This method returns a constraint object that is not added to the model. + To add it to the model, use the :func:`add_constraint` method. + + :param lhs: An object that can be converted to a linear expression, typically a variable, + a member of an expression. + :param rhs: An object that can be converted to a linear expression, typically a variable, + a member of an expression. + :param name: An optional string to name the constraint. + + :returns: An instance of :class:`docplex.mp.linear.LinearConstraint`. + """ + return self._lfactory.new_eq_constraint(lhs, rhs, name)
+ +
[docs] def linear_constraint(self, lhs, rhs, ctsense, name=None): + """ Creates a linear constraint. + + Note: + This method returns a constraint object that is not added to the model. + To add it to the model, use the :func:`add_constraint` method. + + Args: + lhs: An object that can be converted to a linear expression, typically a variable, + a member of an expression. + rhs: An object that can be converted to a linear expression, typically a variable, + a number of an expression. + ctsense: A constraint sense; accepts either a + value of type `ComparisonType` or a string (e.g 'le', 'eq', 'ge'). + + name (string): An optional string to name the constraint. + + :returns: An instance of :class:`docplex.mp.linear.LinearConstraint`. + """ + return self._lfactory.new_binary_constraint(lhs, ctsense, rhs, name)
+ + + def not_equal_constraint(self, lhs, rhs, name=None): + return self._lfactory.new_neq_constraint(lhs, rhs, name) + + def _create_engine_constraint(self, ct): + # INTERNAL + eng = self.__engine + if isinstance(ct, LinearConstraint): + return eng.create_linear_constraint(ct) + elif isinstance(ct, RangeConstraint): + return eng.create_range_constraint(ct) + elif isinstance(ct, IndicatorConstraint): + # here check whether linear ct is trivial. if yes do not send to CPLEX + indicator = ct + linct = indicator.linear_constraint + if linct.is_trivial(): + is_feasible = linct._is_trivially_feasible() + if is_feasible: + self.warning("Indicator constraint {0!s} has a trivially feasible constraint (no effect)", + indicator) + return -2 + else: + self.warning( + "indicator constraint {0!s} has a trivially infeasible constraint; variable invalidated", + indicator) + indicator.invalidate() + return -4 + return eng.create_logical_constraint(ct, is_equivalence=False) + elif isinstance(ct, EquivalenceConstraint): + return eng.create_logical_constraint(ct, is_equivalence=True) + + elif isinstance(ct, QuadraticConstraint): + return eng.create_quadratic_constraint(ct) + elif isinstance(ct, PwlConstraint): + return eng.create_pwl_constraint(ct) + else: + self.fatal("Expecting binary constraint, indicator or range, got: {0!s}", ct) # pragma: no cover + + def _notify_trivial_constraint(self, ct, ctname, is_feasible): + self_trivial_warn_level = self._checker.check_trivial_constraints() + if is_feasible and self_trivial_warn_level > warn_trivial_feasible: + return + elif self_trivial_warn_level > warn_trivial_infeasible: + return + # --- + # hereafter we are sure to warn + if ct is None: + arg = None + # elif ctname: + # arg = ctname + # elif ct.has_name(): + # arg = ct.name + else: + arg = str_maxed(ct, maxlen=24) + # --- + ct_typename = ct.short_typename if ct is not None else "constraint" + ct_rank = self.number_of_constraints + 1 + # BEWARE: do not use if arg here + # because if arg is a constraint, boolean conversion won't work. + trivial_msg = "Adding trivially {3}feasible {2}: '{0!s}', pos: {1}" + if arg is not None: + if is_feasible: + self.info(trivial_msg, arg, ct_rank, ct_typename, '') + else: + self.error(trivial_msg, arg, ct_rank, ct_typename, 'in') + docplex_add_trivial_infeasible_ct_here() + else: + if is_feasible: + self.info(trivial_msg, '', ct_rank, ct_typename, '') + else: + self.error(trivial_msg, '', ct_rank, ct_typename, 'in') + docplex_add_trivial_infeasible_ct_here() + + def _register_ct_name(self, ct, ctname, arg_checker): + checker = arg_checker or self._checker + if ctname: + ct_name_map = self._cts_by_name + if ct_name_map is not None: + checker.check_duplicate_name(ctname, ct_name_map, "constraint") + ct_name_map[ctname] = ct + ct._set_name(ctname) + + def _prepare_constraint(self, ct, ctname, check_for_trivial_ct, arg_checker=None): + # INTERNAL + checker = arg_checker or self._checker + if ct is True: + # sum([]) == 0 + self._notify_trivial_constraint(ct=None, ctname=ctname, is_feasible=True) + return False + + elif ct is False: + # happens with sum([]) and constant e.g. sum([]) == 2 + self._notify_trivial_constraint(ct=None, ctname=ctname, is_feasible=False) + msg = "Adding a trivially infeasible constraint" + if ctname: + msg += ' with name: {0}'.format(ctname) + # analogous to 0 == 1, model is sure to fail + self.fatal(msg) + + else: + checker.typecheck_ct_to_add(ct, self, 'add_constraint') + # -- watch for trivial cts e.g. linexpr(0) <= linexpr(1) + if check_for_trivial_ct and ct.is_trivial(): + if ct._is_trivially_feasible(): + self._notify_trivial_constraint(ct, ctname, is_feasible=True) + elif ct._is_trivially_infeasible(): + self._notify_trivial_constraint(ct, ctname, is_feasible=False) + + # check for already posted cts. + if not self._check_new_ct_index(ct): + return False + # --- name management --- + self._register_ct_name(ct, ctname, checker) + # --- + return True + + def _check_new_ct_index(self, ct): + ok = True + if ct._index >= 0: + self.warning("constraint has already been posted: {0!s}, index is: {1}", ct, ct.index) # pragma: no cover + ok = False + return ok + + def _check_trivial_constraints(self): + check_trivial = self._checker.check_trivial_constraints() + return check_trivial < warn_trivial_none + + def _add_constraint_internal(self, ct, ctname=None): + used_ct_name = None if self._ignore_names else ctname + if not isinstance(ct, AbstractConstraint) and hasattr(ct, 'as_logical_operand'): + ct1 = self._lfactory.logical_expr_to_constraint(ct, ctname) + return self._post_constraint(ct1) + + check_trivial = self._check_trivial_constraints() + if self._prepare_constraint(ct, used_ct_name, check_for_trivial_ct=check_trivial): + self._post_constraint(ct) + return ct + elif ct is True or ct is False: + return None + else: + return ct + + def _post_constraint(self, ct): + ct_engine_index = self._create_engine_constraint(ct) + self._register_one_constraint(ct, ct_engine_index, is_ctname_safe=True) + return ct + + def _remove_constraint_internal(self, ct): + self._remove_constraints_internal(cts_to_remove=(ct,)) + + def _resolve_ct(self, ct_arg, silent=False, check_index=True, caller=None): + verbose = not silent + if caller: + s_caller = caller + ": " + else: + s_caller = "" + + ct = None + if isinstance(ct_arg, AbstractConstraint): + ct = ct_arg + elif is_string(ct_arg): + ct = self.get_constraint_by_name(ct_arg) + if ct is None and verbose: + self.error("{0}no constraint with name: \"{1}\" - ignored", s_caller, ct_arg) + + elif is_int(ct_arg): + if ct_arg >= 0: + ct_index = ct_arg + ct = self.get_constraint_by_index(ct_index) + if ct is None and verbose: + self.error("{0}no constraint with index: \"{1}\" - ignored", s_caller, ct_arg) + else: + self.error("{0}not a valid index: \"{1}\" - ignored", s_caller, ct_arg) + else: + if verbose: + self.error("{0}unexpected argument {1!s}, expecting string or constraint", s_caller, ct_arg) + if ct is not None and ct.has_valid_index() or not check_index: + return ct + else: + return None + +
[docs] def remove_constraint(self, ct_arg): + """ Removes a constraint from the model. + + Args: + ct_arg: The constraint to remove. Accepts either a constraint object or a string. + If passed a string, looks for a constraint with that name. + + """ + ct = self._resolve_ct(ct_arg, silent=False, caller="remove_constraint") + if ct is not None: + self._checker.typecheck_in_model(self, ct, caller="constraint") + self._remove_constraints_internal(cts_to_remove=(ct,))
+ +
[docs] def clear_constraints(self): + """ + This method removes all constraints from the model. + """ + self.__engine.remove_constraints(cts=None) # special case to denote all + # clear containers + self._cts_by_name = None + # clear constraint index scopes. + for ctscope in self._iter_constraint_scopes(): + ctscope.clear()
+ +
[docs] def remove_constraints(self, cts=None, error='warn'): + """ + This method removes a batch of constraints from the model. + + :param cts: an iterable of constraints (linear, range, quadratic, indicators) + """ + if cts is not None: + # resolve constraints + if not is_iterable(cts): + self.fatal("Model.remove_constraints expects an iterable with constraints or strings") + lcts = [] + + for cta in cts: + ct = self._resolve_ct(cta, silent=True, caller='Model.remove_constraints') + if ct is not None: + lcts.append(ct) + elif error == 'raise': + self.fatal("Model.remove_constraints: cannot resolve this as a constraint: {0}", repr(cta)) + elif error == 'warn': + self.warning("Model.remove_constraints: cannot resolve this as a constraint: {0}", repr(cta)) + + self._remove_constraints_internal(lcts)
+ + def _remove_constraints_internal(self, cts_to_remove): + if cts_to_remove: + assert all(ct_.has_valid_index() for ct_ in cts_to_remove) + self.__engine.remove_constraints(cts_to_remove) + # INTERNAL + self_cts_by_name = self._cts_by_name + + if self_cts_by_name: + for d in cts_to_remove: + dname = d.name + if dname: + try: + del self_cts_by_name[dname] + except KeyError: + pass + + actual_touched_scopes = set() + #removed_ids = set() + from collections import defaultdict + idxs_by_scope = defaultdict(set) + for d in cts_to_remove: + #removed_ids.add(id(d)) + idxs_by_scope[d.cplex_scope].add(d.index) + actual_touched_scopes.add(d._get_index_scope()) + + for sc, delset in idxs_by_scope.items(): + scope = self._get_obj_scope(sc) + scope.notify_delete_set(delset) + + for d in cts_to_remove: + d.notify_deleted() + + if self.is_docplex_debug(): + for sc in self._iter_constraint_scopes(): + sc.check_indices() + +
[docs] def remove(self, removed): + """ + This method removes a constraint or a collection of constraints from the model. + + :param removed: accapts either a constraint or an iterable on constraints (linear, range, quadratic, indicators) + """ + if is_iterable(removed): + self.remove_constraints(removed) + else: + self.remove_constraint(removed)
+ +
[docs] def add_range(self, lb, expr, ub, rng_name=None): + """ Adds a new range constraint to the model. + + A range constraint states that a linear + expression has to stay within an interval `[lb..ub]`. + Both `lb` and `ub` have to be float numbers with `lb` smaller than `ub`. + + The method creates a new range constraint and adds it to the model. + + Args: + lb (float): A floating-point number. + expr: A linear expression, e.g. X+Y+Z. + ub (float): A floating-point number, which should be greater than `lb`. + rng_name (string): An optional name for the range constraint. + + :returns: The newly created range constraint. + :rtype: :class:`docplex.mp.constr.RangeConstraint` + + Raises: + An exception if `lb` is greater than `ub`. + + """ + rng = self.range_constraint(lb, expr, ub) + ctname = None if self._ignore_names else rng_name + ct = self._add_constraint_internal(rng, ctname) + return ct
+ +
[docs] def indicator_constraint(self, binary_var, linear_ct, active_value=1, name=None): + """ Creates and returns a new indicator constraint. + + The indicator constraint is not added to the model. + + Args: + binary_var: The binary variable used to control the satisfaction of the linear constraint. + linear_ct: A linear constraint (EQ, LE, GE). + active_value: 0 or 1. The value used to trigger the satisfaction of the constraint. The default is 1. + name (string): An optional name for the indicator constraint. + + :return: + The newly created indicator constraint. + """ + self._checker.typecheck_binary_var(binary_var) + self._checker.typecheck_linear_constraint(linear_ct) + self._checker.typecheck_zero_or_one(active_value) + self._checker.typecheck_in_model(self, binary_var, caller="binary variable") + self._checker.typecheck_in_model(self, linear_ct, caller="linear_constraint") + return self._lfactory.new_indicator_constraint(binary_var, linear_ct, active_value, name)
+ + +
[docs] def add_indicator(self, binary_var, linear_ct, active_value=1, name=None): + """ Adds a new indicator constraint to the model. + + An indicator constraint links (one-way) the value of a binary variable to + the satisfaction of a linear constraint. + If the binary variable equals the active value, then the constraint is satisfied, but + otherwise the constraint may or may not be satisfied. + + Args: + binary_var: The binary variable used to control the satisfaction of the linear constraint. + linear_ct: A linear constraint (EQ, LE, GE). + active_value: 0 or 1. The value used to trigger the satisfaction of the constraint. The default is 1. + name (string): An optional name for the indicator constraint. + + Returns: + The newly created indicator constraint. + """ + self._checker.typecheck_string(name, accept_none=True) + iname = None if self._ignore_names else name + indicator = self.indicator_constraint(binary_var, linear_ct, active_value) + return self._add_indicator(indicator, iname)
+ + _indicator_trivial_feasible_idx = -2 + _indicator_trivial_infeasible_idx = -4 + + def _add_indicator(self, indicator, ind_name, check_trivials=False): + # INTERNAL + linear_ct = indicator.linear_constraint + if check_trivials and self._checker.check_trivial_constraints() and linear_ct.is_trivial(): + is_feasible = linear_ct._is_trivially_feasible() + if is_feasible: + self.warning("Indicator constraint {0!s} has a trivial feasible linear constraint (has no effect)", + indicator) + return self._indicator_trivial_feasible_idx + else: + self.warning("indicator constraint {0!s} has a trivial infeasible linear constraint - invalidated", + indicator) + indicator.invalidate() + return self._indicator_trivial_infeasible_idx + else: + return self._add_constraint_internal(indicator, ind_name) + +
[docs] def add_equivalence(self, binary_var, linear_ct, true_value=1, name=None): + """ Adds a new equivalence constraint to the model. + + An equivalence constraints links two-way the value of a binary variable to + the satisfaction of a discrete linear constraint. + If the binary variable equals the true value, then the constraint is satisfied, + conversely if the constraint is satisfied, the binary variable is equal to the true value. + + Args: + binary_var: The binary variable used to control the satisfaction of the linear constraint. + linear_ct: A linear constraint (EQ, LE, GE). + true_value: 0 or 1. The value used to trigger the satisfaction of the constraint. The default is 1. + name (string): An optional name for the equivalence constraint. + + Returns: + The newly created equivalence constraint. + """ + equiv = self.equivalence_constraint(binary_var, linear_ct, true_value, name=None) + eq_name = None if self.ignore_names else name + eqct = self._add_constraint_internal(equiv, eq_name) + return eqct
+ +
[docs] def equivalence_constraint(self, binary_var, linear_ct, true_value=1, name=None): + """ Creates and returns a new equivalence constraint. + + The newly created equivalence constraint is not added to the model. + + Args: + binary_var: The binary variable used to control the satisfaction of the linear constraint. + linear_ct: A linear constraint (EQ, LE, GE). + true_value: 0 or 1. The value used to mark the satisfaction of the constraint. The default is 1. + name (string): An optional name for the equivalence constraint. + + Returns: + The newly created equivalence constraint. + """ + checker = self._checker + checker.typecheck_binary_var(binary_var) + checker.typecheck_linear_constraint(linear_ct) + checker.typecheck_zero_or_one(true_value) + checker.typecheck_in_model(self, binary_var, caller="binary variable") + checker.typecheck_in_model(self, linear_ct, caller="linear_constraint") + checker.typecheck_string(name, accept_empty=True, accept_none=True) + StaticTypeChecker.typecheck_discrete_constraint(self, linear_ct, + msg='Model.add_equivalence() requires a discrete constraint') + used_name = None if self.ignore_names else name + equiv = self._lfactory.new_equivalence_constraint(binary_var, linear_ct, true_value, used_name) + return equiv
+ +
[docs] def add_equivalences(self, binary_vars, cts, true_values=1, names=None): + """ Adds a batch of equivalence constraints to the model. + + This method adds a batch of equivalence constraints to the model. + + :param binary_vars: a sequence of binary variables. + :param cts: a sequence of discrete linear constraints + :param true_values: the true values to use. Accepts either 1, 0 or a sequence of {0, 1} values. + :param names: an optional sequence of names + + All sequences must have the same length. + + :return: a list of equivalence constraints. + """ + return self._add_batch_logical_cts(binary_vars, cts, names, true_values, is_equivalence=True, + caller='Model.add_equivalences')
+ +
[docs] def add_indicators(self, binary_vars, cts, true_values=1, names=None): + """ Adds a batch of indicator constraints to the model. + + This method adds a batch of indicator constraints to the model. + + :param binary_vars: a sequence of binary variables. + :param cts: a sequence of discrete linear constraints + :param true_values: the true values to use. Accepts either 1, 0 or a sequence of {0, 1} values. + :param names: an optional sequence of names + + All sequences must have the same length. + + :return: a list of indicator constraints. + """ + return self._add_batch_logical_cts(binary_vars, cts, names, true_values, is_equivalence=False, + caller='Model.add_indicators')
+ + def _add_batch_logical_cts(self, binary_vars, cts, names, true_values, is_equivalence, caller=''): + # internal + checker = self._checker + bvars = checker.typecheck_var_seq(binary_vars, vtype='B', caller=caller) + ctseq = checker.typecheck_constraint_seq(cts, check_linear=True) + try: + n_vars = len(bvars) + n_cts = len(cts) + except TypeError: + # if passed iterators, no len() + bvars = list(bvars) + ctseq = list(ctseq) + n_vars = len(bvars) + n_cts = len(ctseq) + + if n_vars != n_cts: + self.fatal('Model.add_equivalences(): binary_vars and linear cts must have same size.') + + if true_values == 0 or true_values == 1: + actual_true_values = generate_constant(true_values, n_vars) + + elif is_iterable(true_values): + actual_true_values = list(true_values) + if len(actual_true_values) != n_vars: + self.fatal('Model.add_equivalences(): true_values has wrong size. expecting: {0}, got: {1}' + .format(n_vars, len(actual_true_values))) + for a in actual_true_values: + checker.typecheck_zero_or_one(a) + else: + self.fatal('Model.add_equivalence(): true_values expects 0|1 or sequence of {{0, 1}}, got: {0!r}'.format(true_values)) + + if names is not None and not self._ignore_names: + c_names = checker.typecheck_string_seq(names, accept_none=True, accept_empty=True, caller=caller) + used_names = [n or '' for n in c_names] + # checker.typecheck_string(n, accept_empty=False, accept_none=True) + # used_names.append(n or '') + else: + used_names = generate_constant(None, n_vars) + + if is_equivalence: + # check discrete + lcts = list(ctseq) + caller = "Model.add_equivalences" if is_equivalence else "Model.add_indicators" + caller += " requires an iterable of discrete constraints" + for ct in lcts: + StaticTypeChecker.typecheck_discrete_constraint(self, ct, caller) + eqcts = self._lfactory.new_batch_equivalence_constraints(bvars, lcts, actual_true_values, used_names) + self.add_equivalence_constraints_(eqcts) + return eqcts + else: + indcts = self._lfactory.new_batch_indicator_constraints(bvars, ctseq, actual_true_values, used_names) + self.add_indicator_constraints_(indcts) + return indcts + +
[docs] def add_indicator_constraints(self, indcts): + """ Adds a batch of indicator constraints to the model + + :param indcts: an iterable returning indicator constraints. + + See Also: + :func:`indicator_constraint` + """ + ind_cts_list_ = list(indcts) + self._checker.typecheck_logical_constraint_seq(ind_cts_list_, true_if_equivalence=False) + ind_indices = self.__engine.create_batch_logical_constraints(ind_cts_list_, is_equivalence=False) + self._register_block_cts(self._logical_scope, ind_cts_list_, ind_indices) + return ind_cts_list_
+ + add_indicator_constraints_ = add_indicator_constraints + +
[docs] def add_equivalence_constraints(self, eqcts): + """ Adds a batch of equivalence constraints to the model + + :param eqcts: an iterable returning equivalence constraints. + + See Also: + :func:`equivalence_constraint` + """ + eqcts_list_ = list(eqcts) # the list is traversed twice + self._checker.typecheck_logical_constraint_seq(eqcts_list_, true_if_equivalence=True) + eq_indices = self.__engine.create_batch_logical_constraints(eqcts_list_, is_equivalence=True) + self._register_block_cts(self._logical_scope, eqcts_list_, eq_indices) + return eqcts_list_
+ + add_equivalence_constraints_ = add_equivalence_constraints + +
[docs] def if_then(self, if_ct, then_ct, negate=False): + """ Creates and returns an if-then constraint. + + An if-then constraint links two constraints ct1, ct2 such that when ct1 is satisfied then ct2 is also satisfied. + + :param if_ct: a linear constraint, the satisfaction of which governs the satisfaction of the `then_ct` + :param then_ct: a linear constraint, which becomes satisfied as soon as `if_ct` is satisfied + (or when it is not, depending on the `negate` flag). + :param negate: an optional boolean flag (default is False). If True, `then_ct` is satisfied when `if_ct` is *not* satisfied. + + :return: + an instance of IfThenConstraint, that is not added to the model. + Use Model.add_constraint() or Model.add() to add it to the model. + + Note: + This constraint relies on the status of the `if_ct` constraint, so this constraint must be discrete, + otherwise an exception will be raised. + """ + checker = self._checker + checker.typecheck_linear_constraint(if_ct) + checker.typecheck_linear_constraint(then_ct) + StaticTypeChecker.typecheck_discrete_constraint(logger=self, ct=if_ct, msg='Model.if_then()') + return self._lfactory.new_if_then_constraint(if_ct, then_ct, bool(negate))
+ +
[docs] def add_if_then(self, if_ct, then_ct, negate=False): + """ Creates a new if-then constraint and adds it to the model + + :param if_ct: a linear constraint, the satisfaction of which governs the satisfaction of the `then_ct` + :param then_ct: a linear constraint, which becomes satisfied as soon as `if_ct` is satisfied + (or when it is not, depending on the `negate` flag). + :param negate: an optional boolean flag (default is False). If True, `then_ct` is satisfied when `if_ct` is *not* satisfied. + + :return: + an instance of IfThenConstraint. + + Note: + This constraint relies on the status of the `if_ct` constraint, so this constraint must be discrete, + otherwise an exception will be raised. On the opposite, the `then_ct` constraint may be non-discrete. + + Also note that this construct relies on the status variable of the `if_ct`, so one extra binary variable is generated. + + *New in 2.16*: when `if_ct` is of the form `bvar == 1` or `bvar ==0`, where `bvar` is a binary variable, + , no extra variable is generated, and a plain indicator constraint is generated. + + An alternative syntax is to use the `>>` operator on linear constraints: + + >>> m.add(c1 >> c2) + + is exactly equivalent to: + + >>> m.add_if_then(c1, c2) + + + """ + ifthen_ct = self.if_then(if_ct, then_ct, negate=negate) + return self._post_constraint(ifthen_ct)
+ +
[docs] def range_constraint(self, lb, expr, ub, rng_name=None): + """ Creates a new range constraint but does not add it to the model. + + A range constraint states that a linear + expression has to stay within an interval `[lb..ub]`. + Both `lb` and `ub` have to be floating-point numbers with `lb` smaller than `ub`. + + The method creates a new range constraint but does not add it to the model. + + Args: + lb: A floating-point number. + expr: A linear expression, e.g. X+Y+Z. + ub: A floating-point number, which should be greater than `lb`. + rng_name: An optional name for the range constraint. + + Returns: + The newly created range constraint. + Raises: + An exception if `lb` is greater than `ub`. + + """ + self._checker.typecheck_num(lb, 'Model.range_constraint') + self._checker.typecheck_num(ub, 'Model.range_constraint') + self._checker.typecheck_string(rng_name, accept_empty=False, accept_none=True) + rng = self._lfactory.new_range_constraint(lb, expr, ub, rng_name) + return rng
+ +
[docs] def add_constraint(self, ct, ctname=None): + """ Adds a new linear constraint to the model. + + Args: + ct: A linear constraint of the form <expr1> <op> <expr2>, where both expr1 and expr2 are + linear expressions built from variables in the model, and <op> is a relational operator + among <= (less than or equal), == (equal), and >= (greater than or equal). + ctname (string): An optional string used to name the constraint. + + Returns: + The newly added constraint. + + See Also: + :func:`add_constraint_` + """ + ct = self._add_constraint_internal(ct, ctname) + return ct
+ +
[docs] def add_constraint_(self, ct, ctname=None): + """ Adds a new linear constraint to the model. + + Args: + ct: A linear constraint of the form <expr1> <op> <expr2>, where both expr1 and expr2 are + linear expressions built from variables in the model, and <op> is a relational operator + among <= (less than or equal), == (equal), and >= (greater than or equal). + ctname (string): An optional string used to name the constraint. + + Note: + This method does the same as `docplex.mp.model.Model.add_constraint()` except that it has no return value. + + See Also: + :func:`add_constraint` + """ + self._add_constraint_internal(ct, ctname)
+ + def add(self, ct, name=None): + if is_iterable(ct): + return self.add_constraints(ct, name) + else: + return self.add_constraint(ct, name) + + def add_(self, ct, name=None): + if is_iterable(ct): + self.add_constraints_(ct, name) + else: + self.add_constraint_(ct, name) + +
[docs] def add_constraints(self, cts, names=None): + """ Adds a batch of linear constraints to the model in one operation. + + Each constraint from the `cts` iterable is added to the model. + If present, the `names` iterable is used to set names to the constraints. + + Example: + # list + >>> m.add_constraints([x >= 1, y<= 3], ["c1", "c2"]) + # comprehension + >>> m.add_constraints((xs[i] >= i for i in range(N))) + + Args: + cts: An iterable of linear constraints; can be a list, a set or a comprehensions. + Any Python object, which can be iterated on and yield consttraint objects. + names: An optional iterable on strings. ANy Python object which can be iterated on + and yield strings. The default value is None, meaning no names are set. + + Returns: + A list of the newly added constraints. + + Note: + This method handles only linear constraints (including range constraints). To add + multiple quadratic constraints, see :func:`add_quadratic_constraints` + + See Also: + :func:`add_constraints_` + + """ + self._checker.typecheck_iterable(cts) + if names is not None and not self.ignore_names: + if not is_iterable(names) or (is_string(names) and not names): + self.fatal("Model.add_constraints() expects a sequence of strings or a non-empty string") + return self._lfactory._new_constraint_block2(cts, names) + else: + return self._lfactory._new_constraint_block1(cts)
+ + + def vector_compare(self, lhss, rhss, sense): + l_lhs = ordered_sequence_to_list(lhss, caller='Model.vector.compare') + l_rhs = ordered_sequence_to_list(rhss, caller='Model.vector.compare') + if len(l_lhs) != len(l_rhs): + self.fatal('Model.vector_compare() requires two lists with same length, left size: {0}, right size: {1}'. + format(len(l_lhs), len(l_rhs))) + ctsense = ComparisonType.parse(sense) + return self._aggregator._vector_compare(l_lhs, l_rhs, ctsense) + + def vector_compare_le(self, lhss, rhss): + return self.vector_compare(lhss, rhss, 'le') + + def vector_compare_ge(self, lhss, rhss): + return self.vector_compare(lhss, rhss, 'ge') + + def vector_compare_eq(self, lhss, rhss): + return self.vector_compare(lhss, rhss, 'eq') + + def _new_xconstraint(self, lhs, rhs, comparaison_type): + isquad = False + if self._quad_count: + try: + isquad = rhs.is_quad_expr() + except AttributeError: + pass + if isquad: + return self._qfactory._new_qconstraint(lhs, comparaison_type, rhs) + else: + return self._lfactory._new_binary_constraint(lhs, comparaison_type, rhs) + +
[docs] def add_constraints_(self, cts, names=None): + """ Adds a batch of linear constraints to the model in one operation. + + Same as `docplex.model.Model.add_constraints()` except that is does not return anything. + """ + self._checker.typecheck_iterable(cts) + if names is not None and not self.ignore_names: + self._lfactory._new_constraint_block2(cts, names) + else: + self._lfactory._new_constraint_block1(cts)
+ + + def add_ranges(self, lbs, exprs, ubs, names=None): + checker = self._checker + lbsl = checker.typecheck_num_seq(lbs, caller="Model.add_ranges.lbs") + ubsl = checker.typecheck_num_seq(ubs, caller="Model.add_ranges.ubs") + checker.typecheck_iterable(exprs) + range_names = names if names and not self.ignore_names else None + return self._lfactory.new_range_block(lbsl, exprs, ubsl, range_names) + + def _post_quadratic_constraint(self, qct): + qcx = self.__engine.create_quadratic_constraint(qct) + self._register_one_constraint(qct, qcx, is_ctname_safe=True) + return qct + +
[docs] def add_quadratic_constraints(self, qcs): + """ Adds a batch of quadratic contraints in one call. + + :param qcs: an iterable on a quadratic constraints. + + Note: + The `Model.add_constraints` method handles only linear constraints. + + New in version 2.16* + """ + lqcs = self._checker.typecheck_quadratic_constraint_seq(qcs) + for qc in lqcs: + self._post_quadratic_constraint(qc)
+ + # ---------------------------------------------------- + # objective + # ---------------------------------------------------- + +
[docs] def minimize(self, expr): + """ Sets an expression as the expression to be minimized. + + The argument is converted to a linear expression. Accepted types are variables (instances of + :class:`docplex.mp.dvar.Var` class), linear expressions (instances of + :class:`docplex.mp.linear.LinearExpr`), or numbers. + + :param expr: A linear expression or a variable. + """ + self.set_objective(ObjectiveSense.Minimize, expr)
+ +
[docs] def maximize(self, expr): + """ + Sets an expression as the expression to be maximized. + + The argument is converted to a linear expression. Accepted types are variables (instances of + :class:`docplex.mp.dvar.Var` class), linear expressions (instances of + :class:`docplex.mp.linear.LinearExpr`), or numbers. + + :param expr: A linear expression or a variable. + """ + self.set_objective(ObjectiveSense.Maximize, expr)
+ + def _make_lex_priorities(self, nb_objectives): + # INTERNAL + return list(range(nb_objectives-1, -1, -1)) + + def _compile_multiobj_expr_list(self, exprs, caller, accept_empty=False): + # INTERNAL + # converts an exprs argument to a (possibly empty) list of expressions. + # no side-effect is performed here, the caller has to take action. + caller_string = "{0} ".format(caller) if caller is not None else "" + if not exprs: + if accept_empty: + self.warning("{0}requires a non-empty list of linear expressions, got: {1!r}", + caller_string, exprs) + return [] + else: + self.fatal("{0}requires a non-empty list of linear expressions, got: {1!r}", + caller_string, exprs) + if is_indexable(exprs): + try: + exprs = [self._lfactory._to_linear_operand(x) for x in exprs] + return exprs + + except (TypeError, DOcplexException): + pass + self.fatal( + "{0}requires an indexable sequence of linear expressions, {1!r} was passed", + caller_string, exprs) + + + @classmethod + def supports_multi_objective(cls): + return cls()._supports_multi_objective() + + def _supports_multi_objective(self): + # INTERNAL + ok, _ = self.__engine.supports_multi_objective() + return ok + + def _check_multi_objective_support(self): + # INTERNAL + ok, why = self.__engine.supports_multi_objective() + if not ok: + assert why + self.fatal(msg=why) + +
[docs] def minimize_static_lex(self, exprs, abstols=None, reltols=None, objnames=None): + """ Sets a list of expressions to be minimized in a lexicographic solve. + exprs must be an ordered sequence of objective functions, that are minimized. + + The argument is converted to a list of linear expressions. Accepted types for the list elements are variables + (instances of :class:`docplex.mp.dvar.Var` class), linear expressions (instances of + :class:`docplex.mp.linear.LinearExpr`), or numbers. + + Warning: + This method requires CPLEX 12.9 or higher + + Args: + exprs: a list of linear expressions or variables + abstols: if defined, a list of absolute tolerances having the same size as the `exprs` argument. + reltols: if defined, a list of relative tolerances having the same size as the `exprs` argument. + objnames: if defined, a list of names for objectives having the same size as the `exprs` argument. + + + *New in version 2.9* + """ + self._set_lex_multi_objective(ObjectiveSense.Minimize, exprs, abstols=abstols, reltols=reltols, names=objnames, + caller='Model.minimize_static_lex()')
+ + +
[docs] def maximize_static_lex(self, exprs, abstols=None, reltols=None, objnames=None): + """ Sets a list of expressions to be maximized in a lexicographic solve. + exprs defines an ordered sequence of objective functions that are maximized. + + The argument is converted to a list of linear expressions. Accepted types for the list elements are variables + (instances of :class:`docplex.mp.dvar.Var` class), linear expressions (instances of + :class:`docplex.mp.linear.LinearExpr`), or numbers. + + Warning: + This method requires CPLEX 12.9 or higher + + Args: + exprs: a list of linear expressions or variables + abstols: if defined, a list of absolute tolerances having the same size as the `exprs` argument. + reltols: if defined, a list of relative tolerances having the same size as the `exprs` argument. + objnames: if defined, a list of names for objectives having the same size as the `exprs` argument. + + + *New in version 2.9* + """ + self._set_lex_multi_objective(ObjectiveSense.Maximize, exprs, abstols=abstols, reltols=reltols, names=objnames, + caller='Model.maximize_static_lex()')
+ + +
[docs] def is_minimized(self): + """ Checks whether the model is a minimization model. + + Note: + This returns True even if the expression to minimize is a constant. + To check whether the model has a non-constant objective, use :func:`is_optimized`. + + Returns: + Boolean: True if the model is a minimization model. + """ + return self._objective_sense is ObjectiveSense.Minimize
+ +
[docs] def is_maximized(self): + """ Checks whether the model is a maximization model. + + Note: + This returns True even if the expression to maximize is a constant. + To check whether the model has a non-constant objective, use :func:`is_optimized`. + + Returns: + Boolean: True if the model is a maximization model. + """ + return self._objective_sense is ObjectiveSense.Maximize
+ +
[docs] def objective_coef(self, dvar): + """ Returns the objective coefficient of a variable. + + The objective coefficient is the coefficient of the given variable in + the model's objective expression. If the variable is not explicitly + mentioned in the objective, it returns 0. + + :param dvar: The decision variable for which to compute the objective coefficient. + + Returns: + float: The objective coefficient of the variable. + """ + self._checker.typecheck_var(dvar) + return self._objective_coef(dvar)
+ + def _objective_coef(self, dvar): + return self._objective_expr.unchecked_get_coef(dvar) + +
[docs] def remove_objective(self): + """ Clears the current objective. + + This is equivalent to setting "minimize 0". + Any subsequent solve will look only for a feasible solution. + You can detect this state by calling :func:`has_objective` on the model. + + """ + self.set_objective(self.default_objective_sense, self._new_default_objective_expr())
+ +
[docs] def is_optimized(self): + """ Checks whether the model has a non-constant objective expression. + + A model with a constant objective will only search for a feasible solution when solved. + This happens either if no objective has been assigned to the model, + or if the objective has been removed with :func:`remove_objective`. + + Returns: + Boolean: True, if the model has a non-constant objective expression. + + """ + return self.has_multi_objective() or not self._objective_expr.is_constant()
+ +
[docs] def set_multi_objective(self, sense, exprs, priorities=None, weights=None, + abstols=None, reltols=None, + names=None): + """ Sets a list of objectives. + + Warning: + This method requires CPLEX 12.9 or higher + + Args: + sense: Either an instance of :class:`docplex.mp.basic.ObjectiveSense` (Minimize or Maximize), + or a string: "min" or "max". + exprs: Is converted to a list of expressions. Accepted types for this list items are variables, + linear expressions or numbers. + priorities: a list of priorities having the same size as the `exprs` argument. Priorities + define how objectives are grouped together into sub-problems, and in which order these sub-problems + are solved (in decreasing order of priorities). If not defined, allexpressions are assumed to share the same priority, + and are combined with `weights`. + weights: if defined, a list of weights having the same size as the `exprs` argument. Weights define + how objectives with same priority are blended together to define the associated sub-problem's + objective that is optimized. If not defined, weights are assumed to be all equal to 1. + abstols: if defined, a list of absolute tolerances having the same size as the `exprs` argument. + reltols: if defined, a list of relative tolerances having the same size as the `exprs` argument. + names: if defined, a list of names for objectives having the same size as the `exprs` argument. + + Note: + When using a number for an objective, the search will not optimize but only look for a feasible solution. + + *New in version 2.9.* + """ + self.set_objective_sense(sense) + self._set_multi_objective_exprs(exprs, priorities=priorities, weights=weights, + abstols=abstols, reltols=reltols, names=names, + caller='Model.set_multi_objective()')
+ +
[docs] def set_lex_multi_objective(self, sense, exprs, abstols=None, reltols=None, names=None): + """ Sets a list of objectives to be solved in a lexicographic fashion. + + Objective expressions are listed in decreasing priority. + + Warning: + This method requires CPLEX 12.9 or higher + + Args: + sense: Either an instance of :class:`docplex.mp.basic.ObjectiveSense` (Minimize or Maximize), + or a string: "min" or "max". + exprs: Is converted to a list of expressions. Accepted types for this list items are variables, + linear expressions or numbers. + + abstols: if defined, a list of absolute tolerances having the same size as the `exprs` argument. + reltols: if defined, a list of relative tolerances having the same size as the `exprs` argument. + names: if defined, a list of names for objectives having the same size as the `exprs` argument. + + Note: + When using a number for an objective, the search will not optimize but only look for a feasible solution. + + *New in version 2.9.* + """ + self._set_lex_multi_objective(sense, exprs, abstols=abstols, reltols=reltols, names=names, caller='Model.set_lex_multi_objective')
+ + def _set_lex_multi_objective(self, sense, exprs, abstols, reltols, names, caller): + # INTERNAL + self.set_objective_sense(sense) + lex_exprs = self._compile_multiobj_expr_list(exprs, caller=caller, accept_empty=False) + lex_priorities = self._make_lex_priorities(len(exprs)) + self._set_multi_objective_internal(lex_exprs, priorities=lex_priorities, + weights=None, + abstols=abstols, reltols=reltols, names=names, + caller=caller) + +
[docs] def set_multi_objective_exprs(self, exprs, priorities=None, weights=None, + abstols=None, reltols=None, names=None): + """ Defines a list of blended objectives. + + Objectives with the same priority are combined using weights. Then, objectives are optimized in a + lexicographic fashion by decreasing priority. + + Args: + exprs: Is converted to a list of linear expressions. Accepted types for this list items are variables, + linear expressions or numbers. + priorities: if defined, a list of priorities having the same size as the `exprs` argument. Priorities + define how objectives are grouped together into sub-problems, and in which order these sub-problems + are solved (in decreasing order of priorities). + weights: if defined, a list of weights having the same size as the `exprs` argument. Weights define + how objectives with same priority are blended together to define the associated sub-problem + objective that is optimized. + abstols: if defined, a list of absolute tolerances having the same size as the `exprs` argument. + reltols:if defined, a list of relative tolerances having the same size as the `exprs` argument. + names: if defined, a list of names for objectives having the same size as the `exprs` argument. + + Note: + When using a number for an objective, the search will not optimize but only look for a feasible solution. + + *New in version 2.9.* + """ + self._set_multi_objective_exprs(exprs, priorities, weights, abstols, reltols, names, caller='Model.set_multi_objective()')
+ + def _set_multi_objective_exprs(self, exprs, priorities=None, weights=None, + abstols=None, reltols=None, names=None, caller=None): + exprs_ = self._compile_multiobj_expr_list(exprs, accept_empty=False, caller=caller) + if priorities: + # check an array of len(exprs) + priorities_ = priorities + else: + priorities_ = [1] * len(exprs_) + + self._set_multi_objective_internal(exprs_, priorities=priorities_, weights=weights, + abstols=abstols, reltols=reltols, names=names, clear_objective=True, + caller=caller) + + def _set_multi_objective_internal(self, exprs, priorities, weights, + abstols, reltols, + names, clear_objective=True, caller=None): + # INTERNAL: assumes exprs is a valid list of linear expressions + if 1 == len(exprs): + expr0 = exprs[0] + self.warning('Multi-objective has been converted to single objective: {0}', str_maxed(expr0, maxlen=16)) + self.set_objective_expr(expr0) + else: + for x in exprs: + x.notify_used(self) + if self.has_objective() and clear_objective: + self._clear_objective_expr() + + def refine_caller(caller_, qualifier): + if not caller_: + return caller_ + elif caller_[-1] == ')': + return '%s.%s' % (caller_[:-2], qualifier) + else: + return '%s.%s' % (caller_, qualifier) + + abstols_ = self._typecheck_optional_num_seq(abstols, accept_none=True, caller=refine_caller(caller, 'abstols')) + reltols_ = self._typecheck_optional_num_seq(reltols, accept_none=True, caller=refine_caller(caller, 'reltols')) + self._set_engine_multi_objective_exprs(exprs, priorities, weights, abstols_, reltols_, names) + self._multi_objective.update(exprs, priorities, weights, abstols, reltols, names) + + def _clear_multi_objective(self): + # INTERNAL + zero_exprs = [self._new_default_objective_expr()] + self._set_engine_multi_objective_exprs(zero_exprs, priorities=[0], weights=[1], + abstols=[0], reltols=[0], names=[None]) + self._multi_objective.clear() + + def _nth_multi_objective(self, multiobj_index): + return self._multi_objective[multiobj_index] + + def _check_has_multi_objective(self, caller): + if not self.has_multi_objective(): + self.fatal("{0} requires model with multi-objective models", caller) + +
[docs] def set_multi_objective_abstols(self, abstols): + """ Changes absolute tolerances for multiple objectives. + + Args: + abstols: new absolute tolerances. Can be either a number (applies to all objectives), + or a sequence of numbers. + A sequence must have the same length as the number of objectives. + *New in version 2.16* + """ + self._check_has_multi_objective(caller='Model.set_multi_objective_abstols') + nb_objectives = self.number_of_multi_objective_exprs + abstols_ = self._typecheck_optional_num_seq(abstols, accept_none=False, expected_size=nb_objectives) + self.get_engine().set_multi_objective_tolerances(abstols_, reltols=None) + # update tolerances in multi obj + self._multi_objective.update(new_abstols=abstols_, new_reltols=None)
+ +
[docs] def set_multi_objective_reltols(self, reltols): + """ Changes relative tolerances for multiple objectives. + + Args: + reltols: new relative tolerances. Can be either a number (applies to all objectives), + or a sequence of numbers. + A sequence must have the same length as the number of objectives. + + *New in version 2.16* + """ + self._check_has_multi_objective(caller='Model.set_multi_objective_reltols') + nb_objectives = self.number_of_multi_objective_exprs + reltols_ = self._typecheck_optional_num_seq(reltols, accept_none=False, expected_size=nb_objectives) + self.get_engine().set_multi_objective_tolerances(abstols=None, reltols=reltols_) + # update tolerances in multi obj + self._multi_objective.update(new_abstols=None, new_reltols=reltols_)
+ + def _set_engine_multi_objective_exprs(self, exprs, priorities, weights, abstols, reltols, names): + # INTERNAL + old_multi_objective_exprs = self._multi_objective.exprs + eng = self.__engine + if eng: + nb_exprs = len(exprs) + eng.set_multi_objective_exprs(new_multiobjexprs=exprs, + old_multiobjexprs=old_multi_objective_exprs, + priorities=priorities, weights=weights, + abstols=MultiObjective.as_optional_sequence(abstols, nb_exprs), + reltols=MultiObjective.as_optional_sequence(reltols, nb_exprs), + objnames=names) + if old_multi_objective_exprs is not None: + for expr in old_multi_objective_exprs: + expr.notify_unsubscribed(subscriber=self) + +
[docs] def clear_multi_objective(self): + """ Clears everything related to multi-objective, if any. + + If the model had previously defined + multi-objectives, resets the model with an objective of zero. + If the model had not defined multi-objectives, this method does nothing. + + *New in version 2.10* + """ + self._clear_multi_objective()
+ + def has_objective(self): + # INTERNAL + return not self._objective_expr.is_zero() + +
[docs] def has_multi_objective(self): + """ Returns True if the model has multi objectives defined + + *New in version 2.10* + """ + return not self._multi_objective.empty()
+ + @property + def number_of_multi_objective_exprs(self): + return self._multi_objective.number_of_objectives + + def iter_multi_objective_tuples(self): + return self._multi_objective.itertuples() + + def iter_multi_objective_exprs(self): + return self._multi_objective.iter_exprs() + +
[docs] def set_objective(self, sense, expr): + """ Sets a new objective. + + Args: + sense: Either an instance of :class:`docplex.mp.basic.ObjectiveSense` (Minimize or Maximize), + or a string: "min" or "max". + expr: Is converted to an expression. Accepted types are variables, + linear expressions, quadratic expressions or numbers. + + Note: + When using a number, the search will not optimize but only look for a feasible solution. + + """ + self.set_objective_sense(sense) + self.set_objective_expr(expr)
+ + def set_objective_sense(self, sense): + actual_sense = self._resolve_sense(sense) + self._objective_sense = actual_sense + eng = self.__engine + if eng: + # when ending the model, the engine is None here + eng.set_objective_sense(actual_sense) + + @property + def objective_sense(self): + """ This property is used to get or set the direction of the optimization as an instance of + :class:`docplex.mp.basic.ObjectiveSense`, either Minimize or Maximize. + + This property also accepts strings as arguments: 'min' for minimize and 'max' for maximize. + + """ + return self._objective_sense + + @objective_sense.setter + def objective_sense(self, new_sense): + self.set_objective_sense(new_sense) + + def set_objective_expr(self, new_objexpr, clear_multiobj=True): + # INTERNAL + if self.has_multi_objective() and clear_multiobj: + # Need also to set all attributes to default values so that the model won't be treated as multi-objective + self._clear_multi_objective() + + if new_objexpr is None: + expr = self._new_default_objective_expr() + else: + expr = self._lfactory._to_expr(new_objexpr) + #expr.keep() + expr.notify_used(self) + + eng = self.__engine + current_objective_expr = self._objective_expr + if eng: + # when ending the model, the engine is None here + eng.set_objective_expr(expr, current_objective_expr) + + if current_objective_expr is not None: + current_objective_expr.notify_unsubscribed(subscriber=self) + self._objective_expr = expr + + def _clear_objective_expr(self): + # INTERNAL + current_objective_expr = self._objective_expr + if not current_objective_expr.is_zero(): + eng = self.__engine + current_objective_expr = self._objective_expr + zero_expr = self._new_default_objective_expr() + if eng: + # when ending the model, the engine is None here + eng.set_objective_expr(new_objexpr=zero_expr, old_objexpr=current_objective_expr) + if current_objective_expr is not None: + current_objective_expr.notify_unsubscribed(subscriber=self) + self._objective_expr = zero_expr + +
[docs] def get_objective_expr(self): + """ This method returns the expression used as the model objective. + + Note: + The default objective is a constant zero expression. + + Returns: + an expression. + """ + return self._objective_expr
+ + @property + def objective_expr(self): + """ This property is used to get or set the expression used as the model objective. + """ + return self._objective_expr + + @objective_expr.setter + def objective_expr(self, new_expr): + self.set_objective_expr(new_expr) + + + def notify_expr_modified(self, expr, event): + # INTERNAL + objexpr = self._objective_expr + if event and expr is objexpr or expr is objexpr.linear_part: + # old and new are the same + self.__engine.update_objective(expr=expr, event=event) + + def notify_expr_replaced(self, old_expr, new_expr): + if old_expr is self._objective_expr: + self.__engine.set_objective_expr(new_objexpr=new_expr, old_objexpr=old_expr) + if not is_var(new_expr): + new_expr.grab_subscribers(old_expr) + + def _new_default_objective_expr(self): + # INTERNAL + return self._lfactory.linear_expr(arg=None, constant=0, safe=True) + + @property + def default_objective_sense(self): + return ObjectiveSense.Minimize + + def _can_solve(self): + return self.has_cplex() + + + def _make_end_infodict(self): # pragma: no cover + return self.solution.as_name_dict() if self.solution is not None else dict() + + def prepare_actual_context(self, **kwargs): + # prepares the actual context that will be used for a solve + + # use the provided context if any, or the self.context otherwise + if not kwargs: + return self.context + + arg_context = kwargs.get('context') or self.context + if not isinstance(arg_context, Context): + self.fatal('Expecting instance of docplex.mp.Context, {0!r} was passed', arg_context) + cloned = False + context = arg_context + + # update the context with provided kwargs + for argname, argval in kwargs.items(): + if argname == 'clean_before_solve': + pass + elif argname != "context" and argval is not None: + if not cloned: + context = context.override() + cloned = True + context.update_key_value(argname, argval) + + return context + +
[docs] def build_multiobj_paramsets(self, timelimits = None, mipgaps = None): + """ Creates a sequence containing pre-filled `ParameterSet` objects to be used with multi objective optimization + only. + + Args: + lex_timelimits (optional): a sequence of time limits + lex_mipgaps (optional): a sequence of mip gaps + """ + return self.__engine._build_multiobj_paramsets(self, timelimits, mipgaps)
+ +
[docs] def create_parameter_sets(self): + """ Creates a sequence containing empty `ParameterSet` objects to be used with multi objective optimization + only. + """ + return self.__engine._create_parameter_sets(self)
+ +
[docs] def solve(self, **kwargs): + """ Starts a solve operation on the model. + + Args: + context (optional): An instance of context to be used in instead of + the context this model was built with. + cplex_parameters (optional): A set of CPLEX parameters to use + instead of the parameters defined as + ``context.cplex_parameters``. + Accepts either a RootParameterGroup object (obtained by cloning the model's + parameters), or a dict of path-like names and values. + checker (optional): a string which controls which type of checking is performed. + Possible values are: + - 'std' (the default) performs type checks on arguments to methods; checks that numerical + arguments are numbers, but will not check for NaN or infinity. + - 'numeric' checks that numerical arguments are valid numbers, neither NaN nor + math.infinity + - 'full' performs all possible checks, the union of 'std' and 'numeric' checks. + - 'off' performs no checking at all. Disabling all checks might improve performance, but only when + it is safe to do so. + log_output (optional): if ``True``, solver logs are output to + stdout. If this is a stream, solver logs are output to that + stream object. Overwrites the ``context.solver.log_output`` + parameter. + clean_before_solve (optional): a boolean (default is False). + Solve normally picks up where the previous solve left, but if this flag is set to ``True``, + a fresh solve is started, forgetting all about previous solves.. + parameter_sets (optional) an iterable of parameterset to be used with multi objective optimization. + See :func:`create_parameter_sets` + + Returns: + A :class:`docplex.mp.solution.SolveSolution` object if the solve operation managed to create + a feasible solution, else None. + The reason why solve returned None includes not only errors, but also proper cases of infeasibilties + or unboundedness. When solve returns None, use Model.solve_details to check the status + of the latest solve operation: Model.solve_details always returns + a :class:`docplex.mp.sdetails.SolveDetails` object, whether or not + a solution has been found. + This object contains detailed information about the latest solve operation, such as status, elapsed time, + and for MILP problems, number of nodes processed and final gap. + + See Also: + :func:`solve_details` + :class:`docplex.mp.sdetails.SolveDetails` + """ + if not self.is_optimized(): + self.info("No objective to optimize - searching for a feasible solution") + + parameter_sets = kwargs.pop('parameter_sets', None) + context = self.prepare_actual_context(**kwargs) + + # log stuff + a_stream = context.solver.log_output_as_stream + with OverridenOutputContext(self, a_stream): + if self.environment.has_cplex: + # take arg clean flag or this model's + used_clean_before_solve = kwargs.get('clean_before_solve', self.clean_before_solve) + return self._solve_local(context, used_clean_before_solve, parameter_sets)# lex_timelimits, lex_mipgaps) + else: + return self.fatal("Cannot solve model: no CPLEX runtime found.")
+ + def _connect_progress_listeners(self): + self.__engine.connect_progress_listeners(self, self._progress_listeners, self._qprogress_listeners) + + def _disconnect_progress_listeners(self): + map (lambda xl: xl._disconnect(), chain(self._progress_listeners, self._qprogress_listeners)) + + def _notify_solve_hit_limit(self, solve_details): + # INTERNAL + if solve_details and solve_details.has_hit_limit(): + self.info("solve: {0}".format(solve_details.status)) + + def _solve_local(self, context, clean_before_solve=None, parameter_sets = None):# lex_timelimits=None, lex_mipgaps=None): + """ Starts a solve operation on the local machine. + + Note: If CPLEX is not available, an error is raised. + + Args: + context: a (possibly new) context whose parameters override those of the modle + during this solve. + + Returns: + A Solution object if the solve operation succeeded, None otherwise. + + """ + local_solve_env = CplexLocalSolveEnv(self) + params_to_use = local_solve_env.before_solve(context) + self_engine = self.__engine + new_solution = None + try: + used_parameters = params_to_use or self.context._get_raw_cplex_parameters() + # assert used_parameters is not None + self._apply_parameters_to_engine(used_parameters) + + new_solution = self_engine.solve(self, + parameters=used_parameters, + clean_before_solve=clean_before_solve, + parameter_sets = parameter_sets) + + # store solve status as returned by the engine. + engine_status = self_engine.get_solve_status() + self._last_solve_status = engine_status + + except DOcplexException as docpx_e: # pragma: no cover + new_solution = None + raise docpx_e + + except Exception as e: + new_solution = None + print("----------------- Python exception: {}".format(str(e))) + raise e + + finally: + self._set_solution(new_solution) + local_solve_env.after_solve(context, new_solution, self_engine) + return new_solution + +
[docs] def get_solve_status(self): + """ Returns the solve status of the last successful solve. + + If the model has been solved successfully, returns the status stored in the + model solution. Otherwise returns None. + + :returns: The solve status of the last successful solve, a enumerated value of type + `docplex.utils.JobSolveStatus` + + Note: The status returned by Cplex is stored as `status` in the solve_details of the model. + + >>> m.solve_details.status + + See Also: + :func:`docplex.mp.SolveDetails.status` to get the Cplex status as a string (eg. "optimal") + :func:`docplex.mp.SolveDetails.status_code` to get the Cplex status as an integer code.. + """ + warnings.warn("Model.get_solve_status() is deprecated with cloud solve, use Model.solve_details instead", DeprecationWarning) + return self._last_solve_status
+ + @property + def solve_status(self): + """ Returns the solve status of the last successful solve. + + If the model has been solved successfully, returns the status stored in the + model solution. Otherwise returns None`. + + :returns: The solve status of the last successful solve, a string, or None. + """ + warnings.warn("Model.solve_status is deprecated with cloud solve, use Model.solve_details instead", DeprecationWarning) + return self._last_solve_status + + @property + def job_solve_status(self): + # INTERNAL WML + return self._last_solve_status + + def notify_start_solve(self): + # INTERNAL + pass + + def notify_solve_failed(self): + pass + + @property + def solve_details(self): + """ + This property returns detailed information about the latest solve, + an instance of :class:`docplex.mp.solution.SolveDetails`. + + When the latest solve did return a Solution instance, this property + returns the solve details corresponding to the solution; when no + solution has been found (in other terms, the latest solve operation + returned None), it still returns a SolveDetails object, containing a + CPLEX code identifying the reason why no solution could be found + (for example, infeasibility or unboundedness). + + See Also: + :class:`docplex.mp.sdetails.SolveDetails` + + """ + from copy import copy as shallow_copy + + return shallow_copy(self._solve_details) + + def get_solve_details(self): + return self.solve_details + + def notify_solve_relaxed(self, relaxed_solution, solve_details): + # INTERNAL: used by relaxer + self._solve_details = solve_details + self._set_solution(relaxed_solution) + if relaxed_solution is not None: + self.notify_start_solve() + else: + self.notify_solve_failed() + + def _resolve_sense(self, sense_arg): + return ObjectiveSense.parse(sense_arg, self.logger) # raise if invalid + +
[docs] def solve_with_goals(self, goals, + senses='min', + abstols=None, + reltols=None, + goal_names=None, + write_pass_files=False, + solution_callbackfn=None, + **kwargs): + """ Performs a solve from an ordered collection of goals. + + :param goals: An ordered collection of linear expressions. + + :param senses: Accepts ither an ordered sequence of senses, one sense, or + None. The default is None, in which case the solve uses a Minimize + sense. Each sense can be either a sense object, that is either + `ObjectiveSense.Minimize` or `Maximize`, or a string "min" or "max". + + :param abstols: if defined, accepts either a number or a list of numbers having the same size as the `exprs` argument, + interpreted as absolute tolerances. If passed asingle number, this tolerance number will be used for all + passes. + + :param reltols: if defined, accepts either a number or a list of numbers having the same size as the `exprs` argument, + interpreted as absolute tolerances. If passed asingle number, this tolerance number will be used for all + passes. + + Note: + tolerances are used at each step to constraint the previous + objective value to be be 'no worse' than the value found in the + last pass. For example, if relative tolerance is 2% and pass #1 has + found an objective of 100, then pass #2 will constraint the first + goal to be no greater than 102 if minimizing, or + no less than 98, if maximizing. + + If one pass fails, return the previous pass' solution. If the solve fails at the first + goal, then return None. + + Return: + If successful, returns a tuple with all pass solutions, reversed else None. + The current solution of the model is the first solution in the tuple. + """ + if not goals: + self.error("solve_with_goals requires a non-empty list of goals, got: {0!r}".format(goals)) + return None + if not is_indexable(goals): + self.fatal("solve_with_goals requires an indexable sequence of goals, got: {0!s}", goals) + + is_verbose = kwargs.pop('verbose', False) + + nb_goals =len(goals) + actual_goal_names = ["goal%d" % (gi + 1) for gi in range(nb_goals)] + if isinstance(goal_names, list): + for i in range(nb_goals): + try: + gn = goal_names[i] + if gn: + actual_goal_names[i] = gn + except (KeyError, TypeError, ValueError): + pass + + actual_goals = [(gn, self._lfactory._to_expr(g)) for gn, g in zip(actual_goal_names, goals)] + # --- senses --- + abstols_ = [] + reltols_ = [] + + # compile tolerances to abstols, reltols + if abstols is not None: + abstols_ = self._typecheck_optional_num_seq(abstols, expected_size=nb_goals, caller='Model.solve_with_goals') + if reltols is not None: + reltols_ = self._typecheck_optional_num_seq(reltols, expected_size=nb_goals, caller='Model.solve_with_goals') + + if not abstols_: + abstols_ = [1e-6] * nb_goals + if not reltols_: + reltols_ = [1e-4] * nb_goals + + old_objective_expr = self._objective_expr + old_objective_sense = self._objective_sense + + pass_count = 0 + m = self + results = [] + + if not is_iterable(senses, accept_string=False): + senses = generate_constant(ObjectiveSense.parse(senses), count_max=nb_goals) + + def lex_info(msg): + if is_verbose: + print("-- lex_goals: {0}".format(msg)) + + # keep extra constraints, in order to remove them at the end. + extra_cts = [] + cplex_param_key = Context.cplex_parameters_key + ctx_params = kwargs.get(cplex_param_key) + iter_pass_params = generate_constant(None, nb_goals) + baseline_params = None # parameters to restore at each iteration, default is to do nothing + if ctx_params and is_iterable(ctx_params): + # must pop out the list as normal solve won't have it. + pass_params = list(kwargs.pop(cplex_param_key)) + if len(pass_params) != nb_goals: + self.fatal("List of parameters should have same length as goals, expecting: {0} but a list of size {1} was passed", + nb_goals, pass_params) + else: + iter_pass_params = iter(pass_params) + baseline_params = m.context.parameters # need to clear/reset parameters at each pass + + # --- main loop --- + prev_step = (None, None, None) + all_solutions = [] + solve_kwargs = kwargs.copy() + current_sol = None + try: + for (goal_name, goal_expr), next_sense, abstol, reltol in zip(actual_goals, senses, abstols_, reltols_): + if goal_expr.is_constant() and pass_count > 1: + self.warning("Constant expression in lexicographic solve: {0!s}, skipped", goal_expr) + continue + pass_count += 1 + + if pass_count > 1: + prev_goal, prev_obj, prev_sense = prev_step + tolerance = compute_tolerance(prev_obj, abstol, reltol) + if prev_sense.is_minimize(): + pass_ct = m._post_constraint(prev_goal <= prev_obj + tolerance) + else: + pass_ct = m._post_constraint(prev_goal >= prev_obj - tolerance) + pass_ct.name = "lex_{0}_ct".format(pass_count) + lex_info("pass #{0} generated constraint with rhs: {1}, tolerance={2:.3g}" + .format(pass_count, str(pass_ct.rhs), tolerance)) + extra_cts.append(pass_ct) + + + sense = self._resolve_sense(next_sense) + + lex_info("starting pass %d, %s: %s" % (pass_count, sense.verb, str_maxed(goal_expr, 64))) + m.set_objective(sense, goal_expr) + + if write_pass_files: # pragma: no cover + pass_basename = f"lex_{self.name}_{goal_name}_pass{pass_count}" + dump_path = self.export_as_lp(path='.', basename=pass_basename) + lex_info("saved pass file: {0}".format(dump_path)) + + # --- update pass parameters, if any + pass_param = next(iter_pass_params) + if pass_param: + # print('applying custom parameters') + # pass_param.print_information() + m.context.update_cplex_parameters(pass_param) + m.context.cplex_parameters.print_information() + # --- + if current_sol and pass_count > 1: + solve_kwargs['lex_mipstart'] = current_sol + + current_sol = m.solve(**solve_kwargs) + # restore params if need be + if baseline_params: + m.context.cplex_parameters = baseline_params + + if current_sol is not None: + current_sol.set_name("lex_{0}_{1}_{2}".format(self.name, goal_name, pass_count)) + current_obj = current_sol.objective_value + results.append(current_obj) + prev_step = (goal_expr, current_obj, sense) + all_solutions.append(current_sol) + lex_info("objective value for pass #{0} is: {1}".format(pass_count, current_sol.objective_value)) + if write_pass_files: + pass_basename = f"lex_{self.name}_{goal_name}_pass{pass_count}" + if self.problem_type == 'LP': + bas_path = self.export_basis(path='.', basename=pass_basename) + lex_info("saved pass basis file: {0}".format(bas_path)) + else: + write_level = kwargs.get('write_level', 'auto') + mst_path = current_sol.export_as_mst(path='.', basename=pass_basename, write_level=write_level) + lex_info("saved pass MST file: {0}".format(mst_path)) + if solution_callbackfn: + solution_callbackfn(current_sol) + + + else: # pragma: no cover + sd = m.solve_details + status = sd.status + self.error("lexicographic: pass {0} fails, status={1} ({2}), stopping", + pass_count, status, sd.status_code) + break + finally: + # print("-> start restoring model at end of lexicographic") + while extra_cts: + # using LIFO logic to avoid holes in indices. + ct_to_remove = extra_cts.pop() + # print("* removing constraint: name: {0}, idx: {1}".format(ct_to_remove.name, ct_to_remove.index)) + self._remove_constraint_internal(ct_to_remove) + # restore objective whatsove + self.set_objective(old_objective_sense, old_objective_expr) + # print("<- end restoring model at end of lexicographic") + + # return a solution or None + return tuple(reversed(all_solutions))
+ + + def _has_solution(self): + # INTERNAL + return self._solution is not None + + def _set_solution(self, new_solution): + """ + INTERNAL: Sets this solution as the model's current solution. + Copies values to variables (for now, let's think more about this) + :param new_solution: + :return: + """ + self._solution = new_solution + + def _check_has_solution(self): + # see if we can refine messages here... + if self._solution is None: + if self._solve_details is None: + self.fatal("Model<{0}> has not been solved yet", self.name) + else: + self.fatal("Model<{0}> did not solve successfully", self.name) + + def _check_solved_as_mip(self, caller, do_raise): + if self._check_mip_for_mipstarts: + if not self._solved_as_mip(): + msg = "{0} is only available for MIP problems".format(caller) + if do_raise: + self.fatal(msg) + else: + self.error(msg) + return False + # either a MIP or we don't care... + return True + +
[docs] def add_mip_start(self, mip_start_sol, effort_level=None, write_level=None, complete_vars=False, eps_zero=1e-6): + """ Adds a (possibly partial) solution to use as a starting point for a MIP. + + This is valid only for models with binary or integer decision variables. + The given solution must contain the value for at least one binary or integer variable. + + This feature is also known as 'warm start'. + + The solution passed in input is copied into a new instance of :class:`docplex.mp.SolveSolution`. + Depending on the "write_level" argument, some filtering operations can be performed: by default (no + explicit write level) only discrete variables are copied. When an explicit level is passed, + the level controls whether zero values are passed ore not: for example "WiteLevel.NonZeroDiscreteVars" specifies + only copying non zero values for discrete variables. + + Args: + mip_start_sol (:class:`docplex.mp.solution.SolveSolution`): The solution object to use as a starting point. + write_level: an optional enumerated value from class :class:`docplex.mp.constants.WriteLevel`, controlling + which variables are copied to the MIP start solution. By default, only discrete variables are copied. + complete_vars: optinal flag. If False (default), only variables mentioned in the solution are copied. If True, + all variables in the model are copied to the MIP start. + effort_level: an optional enumerated value of class :class:`docplex.mp.constants.EffortLevel`, or None. + + Returns: + an instance of :class:`doplex.mp.SolveSolution`, different from the one passed in input if the conversion succeeds, + else None (typically for LP models). + + Examples: + The default values correspond to copying only variables explicitly mentioned in the passed solution, + copying only discrete variables, including zeros. + To exclude zeros, use + + >>> mdl.add_mip_start(sol, write_level=WriteLevel.NonZeroDiscreteVars) + + To include all variables in the model, wincluding continuous ones, use: + + >>> mdl.add_mip_start(sol, write_level=WriteLevel.AllVars, complete_vars=True) + + See Also: + :class:`docplex.mp.constants.EffortLevel` + :class:`docplex.mp.constants.WriteLevel` + :class:`docplex.mp.solution.SolveSolution` + + """ + assert eps_zero >= 0 + assert eps_zero < 1 + mip_start_ = None + if self._check_solved_as_mip(caller="Model.add_mip_start", do_raise=False): + try: + mip_start_ = mip_start_sol.as_mip_start(write_level=write_level, + complete_vars=bool(complete_vars), + eps_zero=eps_zero) + mip_start_.check_as_mip_start() + effort = EffortLevel.parse(effort_level) + self._mipstarts.append((mip_start_, effort)) + except AttributeError: + self.fatal("add_mip_starts expects solution, {0!r} was passed", mip_start_sol) + return mip_start_
+ + @property + def mip_starts(self): + """ This property returns the list of MIP start solutions (a list of instances of :class:`docplex.mp.solution.SolveSolution`) + attached to the model if MIP starts have been defined, possibly an empty list. + """ + warnings.warn("Model.mip_starts is deprecated. Use Model.iter_mip_starts instead" + , DeprecationWarning, stacklevel=2) + return [s for (s, _) in self.iter_mip_starts()] + + @property + def number_of_mip_starts(self): + """ This property returns the number of MIP start associated with the model. + + *New in version 2.10* + """ + return len(self._mipstarts) + +
[docs] def iter_mip_starts(self): + """ This property returns an iterator on the MIP starts associated with + the model. + + It returns tuples of size 2: + - first element is a solution (an instance of :class:`docplex.mp.solution.SolveSolution`) + - second is an enumerated value of type :class:`docplex.mp.constants.EffortLevel` + + *New in version 2.10* + + """ + return iter(self._mipstarts)
+ +
[docs] def clear_mip_starts(self): + """ Clears all MIP starts associated with the model. + + Note: this clears only MIP starts provided by the user via the `Model.add_mip_start` method. + This does not remove interbal solutions found by previous solves. To run a fresh solve, + \and forget all about previous solves, use the `clean_before_solve=True` keyword argument for + :func:`solv()` + + See Also: + :func:`add_mip_start` + :func:`solve` + + """ + self._mipstarts = []
+ +
[docs] def read_mip_starts(self, mst_path): + """ Read MIP starts from a file. + + Reads the file and returns a list of (solution, effort_level) tuples. + + :param mst_path: the path to mip start file (in CPLEX MST file format) + + :return: a list of tuples of size 2; the first element is an instance of `SolveSolution` + and the second element is an enumerated value of type `EffortLevel` + + See Also: + :class:`docplex.mp.constants.EffortLevel` + :class:`docplex.mp.solution.SolveSolution` + + * New in version 2.10* + + """ + self._check_solved_as_mip(caller="Model.read_mip_starts", do_raise=True) + + from docplex.mp.sol_xml_reader import read_mst_file + + self.info("Reading mip starts from file: {0}".format(mst_path)) + mip_starts = read_mst_file(mst_path, self, caller='Model.read_mip_starts') + if mip_starts is not None: + if not mip_starts: + self.warning("Found no MIP starts in file: {0}", mst_path) + else: + self.info("Read {0} MIP starts in file: {1}".format(len(mip_starts), mst_path)) + self._mipstarts = mip_starts + return mip_starts + else: + # do not overwrite current mip starts (IMHO) + return None
+ +
[docs] def set_lp_start_basis(self, dvar_stats, lct_stats): + """ Provides an initial basis for a LP problem. + + :param dvar_stats: an ordered sequence (list) of basis status objects, one for + each decision variable in the model. + :param lct_stats: an ordered sequence (list) of basis status objects, one for each linear constraint + in the model + + Note: + Basis status are values of the enumerated type :class:`docplex.mp.constants.BasisStatus`. + + See Also: + :class:`docplex.mp.constants.BasisStatus`. + + * New in version 2.10* + + """ + l_dvar_stats = StaticTypeChecker.typecheck_initial_lp_stats\ + (logger=self, stats=dvar_stats, stat_type='variable', caller='Model.set_lp_start_basis') + l_lct_stats = StaticTypeChecker.typecheck_initial_lp_stats\ + (logger=self, stats=lct_stats, stat_type='constraint', caller='Model.set_lp_start_basis') + self.__engine.set_lp_start(l_dvar_stats, l_lct_stats)
+ + @property + def objective_value(self): + """ This property returns the value of the objective expression in the solution of the last solve. + In case of a multi-objective, only the value of the first objective is returned + + Raises an exception if the model has not been solved successfully. + + """ + self._check_has_solution() + return self._objective_value() + + def _objective_value(self): + return self.solution.objective_value + + @property + def multi_objective_values(self): + """ This property returns the list of values of the objective expressions in the solution of the last solve. + + Raises an exception if the model has not been solved successfully. + + *New in version 2.9* + """ + self._check_has_solution() + return self._multi_objective_values() + + def _multi_objective_values(self): + # INTERNAL + return self.solution.multi_objective_values + + @property + def blended_objective_values(self): + """ This property returns the list of values of the blended objective expressions based on the decreasing + order of priorities in the solution of the last solve. + + Raises an exception if the model has not been solved successfully. + + *New in version 2.9.* + """ + self._check_has_solution() + blended_obj_values = self.solution.get_blended_objective_value_by_priority() + return blended_obj_values + + def _reported_objective_value(self, failure_obj=0): + return self.solution.objective_value if self.solution else failure_obj + + def _resolve_path(self, path_arg, basename_arg, extension): + # INTERNAL + if is_string(path_arg): + if os.path.isdir(path_arg): + if path_arg == ".": + path_arg = os.getcwd() + return self._make_output_path(extension, basename_arg, path_arg) + else: + # add extension if not present (but not twice!) + return path_arg if path_arg.endswith(extension) else path_arg + extension + else: + assert path_arg is None + return self._make_output_path(extension, basename_arg, path_arg) + + def _make_output_path(self, extension, basename, path=None): + return make_output_path2(self.name, extension, basename, path) + + def _get_printer(self, format_spec, do_raise=False, silent=False): + # INTERNAL + printer_kwargs = {'full_obj': self._print_full_obj} + format_ = parse_format(format_spec) + printer = None + if format_.name == 'LP': + printer = LPModelPrinter(**printer_kwargs) + else: + if do_raise: + self.fatal("Unsupported output format: {0!s}", format_spec) + elif not silent: + self.error("Unsupported output format: {0!s}", format_spec) + return printer + + def dump_as_lp(self, path=None, basename=None): + return self._export_from_cplex(path, basename, format_spec="lp") + +
[docs] def export_as_lp(self, path=None, basename=None, hide_user_names=False): + """ Exports a model in LP format. + + Args: + basename: Controls the basename with which the model is printed. + Accepts None, a plain string, or a string format. + if None, uses the model's name; + if passed a plain string, the string is used in place of the model's name; + if passed a string format (either with %s or {0}, it is used to format the + model name to produce the basename of the written file. + + path: A path to write file, expects a string path or None. + can be either a directory, in which case the basename + that was computed with the basename argument, is appended to the directory to produce + the file. + If given a full path, the path is directly used to write the file, and + the basename argument is not used. + If passed None, the output directory will be ``tempfile.gettempdir()``. + + hide_user_names: A Boolean indicating whether or not to keep user names for + variables and constraints. If True, all names are replaced by `x1`, `x2`, ... for variables, + and `c1`, `c2`, ... for constraints. + + Returns: + The full path of the generated file, or None if an error occured. + + Examples: + Assuming the model's name is `mymodel`: + + >>> m.export_as_lp() + + will write ``mymodel.lp`` in ``gettempdir()``. + + >>> m.export_as_lp(basename="foo") + + will write ``foo.lp`` in ``gettempdir()``. + + >>> m.export_as_lp(basename="foo", path="e:/home/docplex") + + will write file ``e:/home/docplex/foo.lp``. + + >>> m.export_as_lp("e/home/docplex/bar.lp") + + will write file ``e:/home/docplex/bar.lp``. + + >>> m.export_as_lp(basename="docplex_%s", path="e/home/") + + will write file ``e:/home/docplex/docplex_mymodel.lp``. + """ + return self.export(path, basename, hide_user_names=hide_user_names, format_spec='lp')
+ +
[docs] def export_as_sav(self, path=None, basename=None): + """ Exports a model in CPLEX SAV format. + + Exporting to SAV format requires that CPLEX is installed and + available in PYTHONPATH. If the CPLEX runtime cannot be found, an exception is raised. + + Args: + basename: Controls the basename with which the model is printed. + Accepts None, a plain string, or a string format. + If None, the model's name is used. + If passed a plain string, the string is used in place of the model's name. + If passed a string format (either with %s or {0}), it is used to format the + model name to produce the basename of the written file. + + path: A path to write the file, expects a string path or None. + Can be a directory, in which case the basename + that was computed with the basename argument, is appended to the directory to produce + the file. + If given a full path, the path is directly used to write the file, and + the basename argument is not used. + If passed None, the output directory will be ``tempfile.gettempdir()``. + + Returns: + The full path of the generated file, or None if an error occured. + + Examples: + See the documentation of :func:`export_as_lp` for examples of pathname generation. + The logic is identical for both methods. + + """ + return self._export_from_cplex(path, basename, format_spec="sav")
+ + dump_as_sav = export_as_sav + +
[docs] def export_as_mps(self, path=None, basename=None): + """ Exports a model in MPS format. + + Exporting to MPS format requires that CPLEX is installed and + available in PYTHONPATH. If the CPLEX runtime cannot be found, an exception is raised. + + Args: + basename: Controls the basename with which the model is printed. + Accepts None, a plain string, or a string format. + If None, the model's name is used. + If passed a plain string, the string is used in place of the model's name. + If passed a string format (either with %s or {0}), it is used to format the + model name to produce the basename of the written file. + + path: A path to write the file, expects a string path or None. + Can be a directory, in which case the basename + that was computed with the basename argument, is appended to the directory to produce + the file. + If given a full path, the path is directly used to write the file, and + the basename argument is not used. + If passed None, the output directory will be ``tempfile.gettempdir()``. + + Returns: + The full path of the generated file, or None if an error occured. + + Examples: + See the documentation of :func:`export_as_lp` for examples of pathname generation. + The logic is identical for both methods. + + """ + return self._export_from_cplex(path, basename, format_spec="mps")
+ +
[docs] def export_as_savgz(self, path=None, basename=None): + """ Exports a model in compressed SAV format. + + Exporting to SAV compressed format requires that CPLEX is installed and + available in PYTHONPATH. If the CPLEX runtime cannot be found, an exception is raised. + + Arguments 'path' and 'basename' have similar usage as for :func:`export_as_lp`. + + Returns: + The full path of the generated file, or None if an error occured. + + Examples: + See the documentation of :func:`export_as_lp` for examples of pathname generation. + The logic is identical for both methods. + + *New In 2.19* + + """ + return self._export_from_cplex(path, basename, format_spec="sav.gz")
+ + + def _export_from_cplex(self, path=None, basename=None, hide_user_names=False, + format_spec="lp"): + return self._export(path, basename, + use_engine=True, + hide_user_names=hide_user_names, + format_spec=format_spec) + + def export(self, path=None, basename=None, + hide_user_names=False, format_spec="lp"): + # INTERNAL + return self._export(path, basename, + use_engine=False, + hide_user_names=hide_user_names, + format_spec=format_spec) + + def _export(self, path=None, basename=None, + use_engine=False, hide_user_names=False, + format_spec="lp"): + # INTERNAL + # path is either a nonempty path string or None + self._checker.typecheck_string(path, accept_none=True, accept_empty=False) + self._checker.typecheck_string(basename, accept_none=True, accept_empty=False) + # INTERNAL + _format = parse_format(format_spec) + if not _format: + self.fatal("Not a supported exchange format: {0!s}", format_spec) + extension = _format.extension + + # combination of path/directory and basename resolution are done in resolve_path + path = self._resolve_path(path, basename, extension) + ret = self._export_to_path(path, hide_user_names, use_engine, _format) + if ret: + self.trace("model file: {0} overwritten", path) + return ret + + def _export_to_path(self, path, hide_user_names=False, use_engine=False, format_spec="lp"): + # INTERNAL + format_ = parse_format(format_spec) + try: + if use_engine: + # rely on engine for the dump + if self.has_cplex(): + self.__engine.export(path, format_) + else: # pragma: no cover + self.fatal( + "Exporting to {0} requires CPLEX, but a local CPLEX installation could not be found, file: {1} could not be written", + format_.name, path) + return None + else: + # a path is not a stream but anyway it will work + self._export_to_stream(stream=path, hide_user_names=hide_user_names, format_spec=format_) + return path + + except IOError: + self.error("Cannot open file: \"{0}\", model: {1} not exported".format(path, self.name)) + raise + + def _export_to_stream(self, stream, hide_user_names=False, format_spec="lp"): + format_ = parse_format(format_spec) + printer = self._get_printer(format_, do_raise=False, silent=True) + if printer: + printer.set_mangle_names(hide_user_names) + printer.printModel(self, stream) + else: + self.__engine.export(stream, format_spec) + +
[docs] def export_to_stream(self, stream, hide_user_names=False, format_spec="lp"): + """ Export the model to an output stream in LP format. + + A stream can be one of: + - a string, interpreted as a system path, + - None, interpreted as `stdout`, or + - a Python file-type object (e.g. a StringIO() instance). + + Args: + stream: An object defining where the output will be sent. + + hide_user_names: An optional Boolean indicating whether or not to keep user names for + variables and constraints. If True, all names are replaced by `x1`, `x2`, ... for variables, + and `c1`, `c2`, ,... for constraints. Default is to keep user names. + + """ + self._export_to_stream(stream, hide_user_names, format_spec)
+ +
[docs] def export_as_lp_string(self, hide_user_names=False): + """ Exports the model to a string in LP format. + + The output string contains the model in LP format. + + Args: + hide_user_names: An optional Boolean indicating whether or not to keep user names for + variables and constraints. If True, all names are replaced by `x1`, `x2`, ... for variables, + and `c1`, `c2`, ... for constraints. Default is to keep user names. + + Returns: + A string, containing the model exported in LP format. + """ + return self.export_to_string(hide_user_names, "lp")
+ + @property + def lp_string(self): + """ This property returns a string encoding the model in LP format. + + *New in version 2.16* + """ + return self.export_as_lp_string() + +
[docs] def export_as_mps_string(self): + """ Exports the model to a string in MPS format. + + Returns: + A string, containing the model exported in MPS format. + + *New in version 2.13* + """ + return self._export_as_cplex_string("mps")
+ +
[docs] def export_as_sav_string(self): + """ Exports the model to a string of bytes in SAV format. + + Returns: + A string of bytes.. + + *New in version 2.13* + """ + return self._export_as_cplex_string("sav")
+ + def _export_as_cplex_string(self, format_spec): + # INTERNAL + _format = parse_format(format_spec) + if not self.has_cplex(): + self.fatal("Exporting to {0} requires CPLEX, but a local CPLEX installation could not be found", + _format.name) + + from io import BytesIO + bs = BytesIO() + self.__engine.export(bs, _format) + raw_res = bs.getvalue() + if _format.is_binary: + # for b, by in enumerate(raw_res): + # nl = (b % 21 == 20) + # print(f" {by}", end='\n' if nl else '') + # print() + return raw_res + else: + return raw_res.decode(self.parameters.read.fileencoding.get()) + + def export_to_string(self, hide_user_names=False, format_spec="lp"): + # INTERNAL + oss = StringIO() + self._export_to_stream(oss, hide_user_names, format_spec) + return oss.getvalue() + + def export_parameters_as_prm(self, path=None, basename=None): + # path is either a nonempty path string or None + self._checker.typecheck_string(path, accept_none=True, accept_empty=False) + self._checker.typecheck_string(basename, accept_none=True, accept_empty=False) + + # combination of path/directory and basename resolution are done in resolve_path + prm_path = self._resolve_path(path, basename, extension='.prm') + self.parameters.export_prm_to_path(path=prm_path) + return prm_path + + def export_annotations(self, path=None, basename=None): + from docplex.mp.anno import ModelAnnotationPrinter + + self._checker.typecheck_string(path, accept_none=True, accept_empty=False) + self._checker.typecheck_string(basename, accept_none=True, accept_empty=False) + + # combination of path/directory and basename resolution are done in resolve_path + anno_path = self._resolve_path(path, basename, extension='.ann') + ap = ModelAnnotationPrinter() + ap.print_to_stream(self, anno_path) + + return anno_path + + def _check_problem_type(self, feature, requires_solution=True, accept_qxp=True): + if self._solve_details is None: + self.fatal('{0} are not available, model is not solved yet'.format(feature)) + elif requires_solution and self._solution is None: + self.fatal('{0} require a solution, but model is not solved with a solution'.format(feature)) + elif self._solved_as_lp(): + pass + elif not accept_qxp and self.is_quadratic(): + self.fatal('{0} are not available for QP/QCP problems'.format(feature)) + elif self._solved_as_mip(): + self.fatal('{0} are not available for integer problems'.format(feature)) + + def _dual_value1(self, linear_ct): + # PRIVATE + self._check_problem_type(feature='dual values') + self._checker.typecheck_ct_added_to_model(self, linear_ct) + dvs = self._dual_values([linear_ct]) + return dvs[0] + +
[docs] def dual_values(self, cts): + """ Returns the dual values of a sequence of linear constraints. + + Note: the model must a pure LP: no integer or binary variable, no piecewise, no SOS. + The model must also be solved successfully before calling this method. + + :param cts: a sequence of linear constraints. + + :return: a sequence of float numbers + """ + self._check_problem_type(feature='dual values') + checked_lcts = self._checker.typecheck_constraint_seq(cts, check_linear=True) + return self._dual_values(checked_lcts)
+ + def _dual_values(self, cts): + # PRIVATE + checked_lcts = self._checker.typecheck_cts_added_to_model(mdl=self, cts=cts) + sol = self.solution + sol.ensure_dual_values(self, self.get_engine()) + return sol.get_dual_values(checked_lcts) + + def _slack_value1(self, ct): + # private + self._checker.typecheck_ct_added_to_model(mdl=self, ct=ct) + self._check_has_solution() + return self._slack_values([ct])[0] + +
[docs] def slack_values(self, cts): + """ Return the slack values for a sequence of constraints. + + Slack values are available for linear, quadratic and indicator constraints. + The model must be solved successfully before calling this method. + + :param cts: a sequence of constraints. + :return: a list of float values, in the same order as the constraints. + """ + self._check_has_solution() + ckr = self._checker + cts1 = ckr.typecheck_constraint_seq(cts) + cts2 = ckr.typecheck_cts_added_to_model(self, cts1) + return self._slack_values(cts2)
+ + def _slack_values(self, cts): + checked_cts = self._checker.typecheck_constraint_seq(cts) + # --- + sol = self.solution + sol.ensure_slack_values(self, self.get_engine()) + return sol.get_slacks(checked_cts) + + def _reduced_cost1(self, dvar): + # PRIVATE + self._check_problem_type(feature='reduced costs') + #self._checker.typecheck_var(dvar) + rcs = self._reduced_costs([dvar]) + return rcs[0] + +
[docs] def get_cuts(self): + """ Returns the number of cuts under the form of a dict(type -> number). + + Note: The model must also be solved successfully before calling this method. + + :return: the number of cuts under the form of a dict(type -> number). + """ + sol = self.solution + assert sol is not None + return sol.get_cuts()
+ +
[docs] def get_num_cuts(self, cut_type): + """ Returns the number of cuts for a specific type. + + Note: The model must also be solved successfully before calling this method. + + :param cut_type: a cut type. + :return: the number of cuts associated to this type of cut. + """ + sol = self.solution + assert sol is not None + return sol.get_num_cuts(cut_type)
+ +
[docs] def reduced_costs(self, dvars): + """ Returns the reduced costs for a variable iterable. + + Note: the model must a pure LP: no integer or binary variable, no piecewise, no SOS. + The model must also be solved successfully before calling this method. + + :param dvars: a sequence of variables. + :return: a list of float numbers, in the same order as the variable sequence. + """ + self._check_problem_type(feature='reduced costs') + checked_vars = self._checker.typecheck_var_seq(dvars, caller='Model.reduced_costs') + return self._reduced_costs(checked_vars)
+ + def _reduced_costs(self, dvars): + sol = self.solution + assert sol is not None + sol.ensure_reduced_costs(model=self, engine= self.get_engine()) + return sol.get_reduced_costs(dvars) + +
[docs] def quadratic_dual_slacks(self, *args): + """ Returns quadratic dual slacks as a dict of dicts. + + Can be called in two forms: either with no arguments, in which case it returns + quadratic dual slacks for all quadratic constraints in the model, or with + a list of quadratic constraints. In this case it returns only quadratic dual slacks for those constraints + + :param args: accepts either no arguments,or a list of quadratic constraints. + + :return: a Python dictionary, whose keys are quadratic constraints, and + values are dictionaries from variables to quadratic dual slacks. + + *New in version 2.15* + """ + nb_args = len(args) + if 0 == nb_args: + qcts = self.iter_quadratic_constraints() + elif 1 == nb_args: + qcts = args[0] + else: + qcts = None + self.fatal("Model.quadratic_dual_slacks expects either an iteratble on quadratic constraints, or no args.") + + self._check_problem_type('quadratic_dual_slacks', requires_solution=True, accept_qxp=True) + cpx = self._get_cplex(do_raise=True, msgfn=lambda: "Quadratic dual slacks require CPLEX library") + checked_qcts = self._checker.typecheck_quadratic_constraint_seq(qcts) + if checked_qcts: + qixs = [qc.index for qc in checked_qcts] + cpx_qdss = cpx.solution.get_quadratic_dualslack(qixs) + qds_as_dict = {checked_qcts[q]: {self._var_by_index(idx): qds for idx, qds in zip(cpx_sp.ind, cpx_sp.val)} \ + for q, cpx_sp in enumerate(cpx_qdss) } + return qds_as_dict + else: + return {}
+ + def _var_basis_status1(self, dvar): + # internal + return self.var_basis_statuses([dvar])[0] + +
[docs] def var_basis_statuses(self, dvars): + """ + Returns basis status for a batch of variables. + + :param dvars: an iterable returning variables. + :return: a list of basis status, of type :class:`docplex.mp.constants.BasisStatus`. + The order of the list is the order in which variables were returned by the iterable. + + *New in version 2.10* + """ + self._check_problem_type(feature='basis status', requires_solution=False, accept_qxp=False) + checked_vars = self._checker.typecheck_var_seq(dvars) + return self._var_basis_status(checked_vars)
+ + def _var_basis_status(self, dvars): + return self._generic_get_basis_status(dvars, pos=0, + sol_getter=lambda s_, dvs_: s_.get_var_basis_statuses(dvs_)) + +
[docs] def linear_constraint_basis_statuses(self, lcts): + """ + Returns basis status for a batch of linear constraints. + + :param lcts: an iterable returning linear constraints. + :return: a list of basis status, of type :class:`docplex.mp.constants.BasisStatus`. + The order of the list is the order in which constraints were returned by the iterable. + + *New in version 2.10* + """ + self._check_problem_type(feature='basis status', requires_solution=False, accept_qxp=False) + checked_lincts = self._checker.typecheck_constraint_seq(lcts, check_linear=True, accept_range=True) + return self._linearct_basis_status(checked_lincts)
+ + def _linearct_basis_status(self, lcts): + return self._generic_get_basis_status\ + (lcts, pos=1, sol_getter=lambda s_, cts_: s_.get_linearct_basis_statuses(cts_)) + + def _generic_get_basis_status(self, objs, pos, sol_getter): + assert pos in {0, 1} + sol = self.solution + if sol: + sol.ensure_basis_statuses(model=self, engine= self.get_engine()) + return sol_getter(sol, objs) #sol.get_linearct_basis_statuses(lcts) + else: + basis_tuple = self.__engine.get_basis(self) + basis = basis_tuple[pos] + if not len(basis): + self.error("No basis is available") + return [BasisStatus.NotABasisStatus] * len(objs) + else: + return [ BasisStatus.parse(basis.get(obj, -1)) for obj in objs] + +
[docs] def has_basis(self): + """ returns True if the model contains basis information. + + *New in version 2.9* + """ + sol = self.solution + if sol: + sol.ensure_basis_statuses(model=self, engine= self.get_engine()) + return sol._has_basis() + else: + var_basis, linct_basis = self.__engine.get_basis(self) + return len(var_basis) > 0
+ + def _write_cplex_file(self, name, path, basename, extension, cpx_write_fn, check_fn=lambda m_: 0): + check_fn(self) + export_basename = normalize_basename(self.name, force_lowercase=True) + export_path = make_output_path2(actual_name=export_basename, + extension=extension, + path=path, + basename_fmt=basename) + if export_path: + msg = "CPLEX runtime is required for {0} export - file {1} not written".format(name, export_path) + cpx = self._get_cplex(do_raise=True, msgfn=lambda: msg) + try: + cpx_write_fn(cpx, export_path) + except Exception as ex: + print(f"An error occured: '{str(ex)}' -- write aborted") + return None + return export_path + + def _check_basis(self): + if not self.has_basis(): + self.fatal("No basis data is available for model '{0}'- cannot write basis file", + self.name) + + def export_basis(self, path=None, basename=None): + return self._write_cplex_file(name='basis', path=path, basename=basename, + extension='.bas', + cpx_write_fn=lambda cpx_, path_: cpx_.solution.basis.write(path_), + check_fn=lambda m_: m_._check_basis()) + + + DEFAULT_VAR_VALUE_QUOTED_SOLUTION_FMT = ' \"{varname}\"={value:.{prec}f}' + DEFAULT_VAR_VALUE_UNQUOTED_SOLUTION_FMT = ' {varname}={value:.{prec}f}' + DEFAULT_OBJECTIVE_FMT = "{0}: {1:.{prec}f}" + + @classmethod + def supports_logical_constraints(cls): + return cls()._supports_logical_constraints() + + def _supports_logical_constraints(self): + # INTERNAL + ok, _ = self.__engine.supports_logical_constraints() + return ok + + _is_cplex_ce = None + + @classmethod + def is_cplex_ce(cls): + if cls._is_cplex_ce is None: + m = Model() + if not m.has_cplex(): + cls._is_cplex_ce = False + else: + try: + for i in range(1001): + v = m.integer_var() + m.add_constraint(v <= i) + m.solve() + cls._is_cplex_ce = False + except DOcplexLimitsExceeded as e: + cls._is_cplex_ce = True + return cls._is_cplex_ce + + def _check_logical_constraint_support(self): + ok, why = self.__engine.supports_logical_constraints() + if not ok: + assert why + self.fatal(msg=why) + + @classmethod + def is_docplex_debug(cls): + return not not os.environ.get("DOCPLEX_DEBUG") + + def _has_username_with_spaces(self): + for v in self.iter_variables(): + if v.has_user_name() and ' ' in v.name: + return True + return False + +
[docs] def print_solution(self, print_zeros=False, + solution_header_fmt=None, + var_value_fmt=None, + **kwargs): + """ Prints the values of the model variables after a solve. + + Only valid after a successful solve. If the model has not been solved successfully, an + exception is raised. + + Args: + print_zeros (Boolean): If False, only non-zero values are printed. Default is False. + solution_header_fmt: a solution header string in format syntax, or None. + This format will be passed to :func:`docplex.mp.solution.SolveSolution.display`. + var_value_fmt : A format string to format the variable name and value. Again, the default uses the automatically computed precision. + + See also: + :func:`docplex.mp.solution.SolveSolution.display` + """ + self._check_has_solution() + if var_value_fmt is None: + if self._has_username_with_spaces(): + var_value_fmt = self.DEFAULT_VAR_VALUE_QUOTED_SOLUTION_FMT + else: + var_value_fmt = self.DEFAULT_VAR_VALUE_UNQUOTED_SOLUTION_FMT + if not self.has_objective(): + var_value_fmt = var_value_fmt[2:] + # scope of variables. + iter_vars = self.iter_variables() if print_zeros else None + # if some username has a whitespace, use quoted format + self.solution.display(print_zeros=print_zeros, + header_fmt=solution_header_fmt, + value_fmt=var_value_fmt, + iter_vars=iter_vars, **kwargs)
+ +
[docs] def report(self): + """ Prints the value of the objective and the KPIs. + Only valid after a successful solve, otherwise states that the model is not solved. + """ + if self._has_solution(): + if self.has_multi_objective(): + mobj_values = self._multi_objective_values() + prec = self._float_precision + s_mobjs = ", ".join("{0:.{prec}f}".format(mo, prec=prec) for mo in mobj_values) + print("* model {0} solved with objectives = [{1}]".format(self.name, s_mobjs)) + else: + used_prec = self._float_precision + print("* model {0} solved with objective = {1:.{prec}f}".format(self.name, + self._objective_value(), prec=used_prec)) + self.report_kpis() + else: + status = self.solve_details.status + self.info("Model {0} has not been solved successfully, status is: {1}.".format(self.name, status))
+ +
[docs] def report_kpis(self, solution=None, selected_kpis=None, kpi_format='* KPI: {1:<{0}} = '): + """ Prints the values of the KPIs. + + KPIs require a solution to be evaluated. This solution can be passed explicitly as a parameter, + or the model is assumed to be solved with a valid solution. + + :param solution: an instance of `SolveSolution`. If not passed, the model solution is + queried. If the model has no solution, an exception is raised. + :param selected_kpis: an optional iterable returning the KPIs to print. + The default behavior is to print all kpis. + :param kpi_format: an optional format to print the KPi name and its value. + + See Also: + :class:`docplex.mp.solution.SolveSolution` + :func:`new_solution` + """ + kpi_num_format = kpi_format + self._float_meta_format % (2,) + kpi_str_format = kpi_format + '{2!s}' + printed_kpis = list(selected_kpis if is_iterable(selected_kpis) else self.iter_kpis()) + try: + max_kpi_name_len = max(len(k.name) for k in printed_kpis) # max() raises ValueError on empty + except ValueError: + max_kpi_name_len = 0 + for kpi in printed_kpis: + kpi_value = kpi.compute(solution) + if is_number(kpi_value): + k_format = kpi_num_format + else: + k_format = kpi_str_format + + if type(k_format) != type(kpi.name): + # infamous mix of str and unicode. Should happen only + # in py2. Let's convert things + if isinstance(k_format, str): + k_format = k_format.decode('utf-8') + else: + k_format = k_format.encode('utf-8') + + + output = k_format.format(max_kpi_name_len, kpi.name, kpi_value) + try: + print(output) + except UnicodeEncodeError: + encoding = sys.stdout.encoding if sys.stdout.encoding else 'ascii' + print(output.encode(encoding, + errors='backslashreplace'))
+ +
[docs] def kpis_as_dict(self, solution=None, kpi_filter=None, objective_key=None, use_names=True): + """ Returns KPI values in a solution as a dictionary. + + Each KPI has a value in the solution. This method returns a dictionary of KPI values, + indexed by KPI objects. + + :param solution: an instance of solution, as returned by solve(). If not passed, will + use the model's solution. If no solution is present, an exception is raised. + :param kpi_filter: an optional predicate to filter some kpis. + If provided, accepts a function taking one KPI as argument and + returning a boolean. By default, all KPIs are returned. + :param objective_key: an optional string key for th eobjective value. If present, the + value of the objective is added to the dictionary, with this key. By default, this parameter + is None and the objective is *not* appended to the dictionary. + :param use_names: a flag which determines whether keys in the resulting dict + are KPI objects or kpi names. Default is to use KPI names. + + :return: + A dictionary mapping KPIs, or KPI names to values. + + See Also: + :class:`docplex.mp.solution.SolveSolution` + """ + if kpi_filter is None: + kpi_filter = lambda _: True + + if use_names: + kpi_dict = {kpi.name: kpi.compute(solution) for kpi in self.iter_kpis() if kpi_filter(kpi)} + else: + kpi_dict = {kpi: kpi.compute(solution) for kpi in self.iter_kpis() if kpi_filter(kpi)} + if objective_key: + kpi_dict[objective_key] = solution.objective_value + return kpi_dict
+ + def _report_lexicographic_goals(self, goal_name_values, kpi_header_format): # pragma: no cover + kpi_format = kpi_header_format + self._float_meta_format % (1,) # be safe even integer KPIs might yield floats + printed_kpis = goal_name_values if is_iterable(goal_name_values) else self.iter_kpis() + for goal_name, goal_expr in printed_kpis: + goal_value = goal_expr.solution_value + print(kpi_format.format(goal_name, goal_value)) + +
[docs] def iter_kpis(self): + """ Returns an iterator over all KPIs in the model. + + Returns: + An iterator object. + """ + return iter(self._allkpis)
+ +
[docs] def kpi_by_name(self, name, try_match=True, match_case=False, do_raise=True): + """ Fetches a KPI from a string. + + This method fetches a KPI from a string, using either exact naming or trying + to match a substring of the KPI name. + + Args: + name (string): The string to be matched. + try_match (Boolean): If True, returns KPI whose name is not equal to the + argument, but contains it. Default is True. + match_case: If True, looks for a case-exact match, else ignores case. Default is False. + do_raise: If True, raise an exception when no KPI is found. + + Example: + If the KPI name is "Total CO2 Cost" then fetching with argument `co2` and `match_case` to False + will succeed. If `match_case` is True, then no KPI will be returned. + + Returns: + The KPI expression if found. If the search fails, either raises an exception or returns a dummy + constant expression with 0. + """ + matching_kpi = self._matching_kpi(name, try_match, match_case) + if matching_kpi: + return matching_kpi + # no match was found + if do_raise: + self.fatal("Model has no KPI with name matching: '{0:s}'", name) + else: + return self._lfactory.new_zero_expr()
+ + def _matching_kpi(self, name, try_match=True, match_case=False): + # internal + for kpi in iter(reversed(self._allkpis)): + kpi_name = kpi.name + ok = False + if kpi_name == name: + ok = True + elif try_match: + if match_case: + ok = kpi_name.find(name) >= 0 + else: + ok = kpi_name.lower().find(name.lower()) >= 0 + if ok: + return kpi + return None + +
[docs] def kpi_value_by_name(self, name, solution=None, try_match=True, match_case=False): + """ Returns a KPI value from a KPI name. + + This method fetches a KPI value from a string, using either exact naming or trying + to match a substring of the KPI name. + + Args: + name (str): The string to be matched. + solution: an optional solution. If not present, assume the model is solved + and use the model solution. + try_match (Bool): If True, returns KPI whose name is not equal to the + argument, but contains it. Default is True. + match_case: If True, looks for a case-exact match, else ignores case. Default is False. + + Example: + If the KPI name is "Total CO2 Cost" then fetching with argument `co2` and `match_case` to False + will succeed. If `match_case` is True, then no KPI will be returned. + + Note: + KPIs require a solution to be evaluated. This solution can be passed explicitly as a parameter, + or the model is assumed to be solved with a valid solution. + + Returns: + float: The KPI value. + + See Also: + :class:`docplex.mp.solution.SolveSolution` + :func:`new_solution` + """ + kpi = self.kpi_by_name(name, try_match, match_case=match_case, do_raise=True) + return kpi.compute(solution)
+ +
[docs] def add_kpi(self, kpi_arg, publish_name=None): + """ Adds a Key Performance Indicator to the model. + + Key Performance Indicators (KPIs) are objects that can be evaluated after a solve(). + Typical use is with decision expressions, the evaluation of which return the expression's solution value. + + KPI values are displayed with the method :func:`report_kpis`. + + Args: + kpi_arg: Accepted arguments are either an expression, + a lambda function with two arguments (model + solution) + or an instance of a subclass of abstract class KPI. + + publish_name (string, optional): The published name of the KPI. + + Note: + - If no publish_name is provided, DOcplex will try to access a + 'name' attribute of the argument; if none exists, it will use the + string representation of the argument , as returned by `str()`. + - expression KPIs are seperate from the model. In other terms, + adding KPIs does not change the model (and matrix) being solved. + + Examples: + `model.add_kpi(x+y+z, "Total Profit")` adds the expression `(x+y+z)` as a KPI with the name "Total Profit". + + `model.add_kpi(x+y+z)` adds the expression `(x+y+z)` as a KPI with + the name "x+y+z", assuming variables x,y,z have names 'x', 'y', 'z' (resp.) + + Returns: + The newly added KPI instance. + + See Also: + :class:`docplex.mp.kpi.KPI`, + :class:`docplex.mp.kpi.DecisionKPI` + """ + self._checker.typecheck_string(publish_name, accept_empty=False, accept_none=True, caller="Model.add_kpi(): ") + new_kpi = self._lfactory.new_kpi(kpi_arg, publish_name) + new_kpi_name = new_kpi.name + if new_kpi_name in set(kp.name for kp in self._allkpis): + self.fatal("Duplicate KPI name: \"{0!s}\" ", new_kpi_name) + self._allkpis.append(new_kpi) + return new_kpi
+ +
[docs] def remove_kpi(self, kpi_arg): + """ Removes a Key Performance Indicator from the model. + + Args: + kpi_arg: A KPI instance that was previously added to the model. Accepts either a KPI object or a string. + If passed a string, looks for a KPI with that name. + + See Also: + :func:`add_kpi` + :class:`docplex.mp.kpi.KPI`, + :class:`docplex.mp.kpi.DecisionKPI` + """ + if is_string(kpi_arg): + kpi = self.kpi_by_name(kpi_arg) + if kpi: + self._allkpis.remove(kpi) + kpi.notify_removed() + else: + for k, kp in enumerate(self._allkpis): + if kp is kpi_arg: + kx = k + break + else: + kx = -1 + if kx >= 0: + removed_kpi = self._allkpis.pop(kx) + removed_kpi.notify_removed() + + else: + self.warning('Model.remove_kpi(): cannot interpret this either as a string or as a KPI: {0!r} - ignored', kpi_arg)
+ +
[docs] def clear_kpis(self): + ''' Clears all KPIs defined in the model. + + + ''' + self._allkpis = []
+ + @property + def number_of_kpis(self): + return len(self._allkpis) + + +
[docs] def add_progress_listener(self, listener): + """ Adds a progress listener to the model. + + A progress listener is a subclass of :class:~docplex.mp.ProgressListener: + + :param listener: + """ + self._checker.typecheck_progress_listener(listener) + self._add_progress_listener(listener)
+ + def _add_progress_listener(self, listener): + # INTERNAL + self._progress_listeners.append(listener) + + def _add_qprogress_listener(self, qlistener): + self._qprogress_listeners.append(qlistener) + +
[docs] def remove_progress_listener(self, listener): + """ Remove a progress listener from the model. + + :param listener: + """ + try: + self._progress_listeners.remove(listener) + except ValueError: + # ignore errors + if self.is_docplex_debug(): + raise + else: + pass
+ +
[docs] def iter_progress_listeners(self): + """ Returns an iterator on the progress listeners attached to the model. + + :return: an iterator. + + """ + return iter(self._progress_listeners)
+ + @property + def number_of_progress_listeners(self): + """ Returns the number of progress listeners attached to the model. + + :return: an integer + """ + return len(self._progress_listeners) + +
[docs] def clear_progress_listeners(self): + """ Remove all progress listeners from the model.""" + self._progress_listeners = []
+ + def _clear_qprogress_listeners(self): + self._qprogress_listeners = [] + + def _fire_start_solve_listeners(self): + for l in self._progress_listeners: + l.notify_start() + + def _fire_end_solve_listeners(self, has_solution, objective_value): + for l in self._progress_listeners: + l.notify_end(has_solution, objective_value) + + def prettyprint(self, out=None): + from docplex.mp.ppretty import ModelPrettyPrinter + ppr = ModelPrettyPrinter() + ppr.printModel(self, out=out) + + pprint = prettyprint + + def pprint_as_string(self): + from docplex.mp.ppretty import ModelPrettyPrinter + with StringIO() as oss: + ppr = ModelPrettyPrinter() + ppr.printModel(self, out=oss) + return oss.getvalue() + +
[docs] def clone(self, new_name=None, **clone_kwargs): + """ Makes a deep copy of the model, possibly with a new name. + Decision variables, constraints, and objective are copied. + + Args: + new_name (string): The new name to use. If None is provided, returns a "Copy of xxx" where xxx is the original model name. + + :returns: A new model. + + :rtype: :class:`docplex.mp.model.Model` + """ + return self.copy(new_name=new_name, **clone_kwargs)
+ + def copy(self, new_name=None, removed_cts=None, **new_kwargs): + # INTERNAL + actual_copy_name = new_name or "Copy of %s" % self.name + # copy kwargs + copy_kwargs = self._get_kwargs().copy() + copy_kwargs.update(**new_kwargs) + + copy_context = self.context.copy() + # pass copy of initial context + # plus override kwargs (e.g. log_output) + copy_model = Model(name=actual_copy_name, context=copy_context, **copy_kwargs) + + # clone variable containers + ctn_map = {} + for ctn in self.iter_var_containers(): + copied_ctn = ctn.copy(copy_model) + ctn_map[ctn] = copied_ctn + copy_model._add_var_container(copied_ctn) + + # clone variables + def make_memo(): + memo = {} + generated_vars = [] + + # clone PWL functions and add them to var_mapping + for pwl_func in self.iter_pwl_functions(): + copied_pwl_func = pwl_func.copy(copy_model, memo) + memo[pwl_func] = copied_pwl_func + # copy 'primary' variables + for v in self.iter_variables(): + if v.is_generated(): + generated_vars.append(v) + else: + copied_var = copy_model._var(v.vartype, v.lb, v.ub, v.name) + var_ctn = v.container + if var_ctn: + copied_ctn = ctn_map.get(var_ctn) + assert copied_ctn is not None + copied_var.container = copied_ctn + memo[v] = copied_var + + for gv in generated_vars: + gvoo = gv.origin + try: + gvo, gvx = gvoo + except TypeError: + gvo = gvoo + gvx = 0 + assert gvo is not None + gvk = id(gvo) + cloned_origin = memo.get(gvk) + if cloned_origin is None: + cloned_origin = gvo.copy(copy_model, memo) + cloned_origin.resolve() + memo[gvk] = cloned_origin + cloned_gv = cloned_origin.get_artefact(gvx) + assert cloned_gv + + memo[gv] = cloned_gv + return memo + + memo = make_memo() + + + + # copy constraints + setof_removed_cts = set(removed_cts) if removed_cts else {} + linear_cts = [] + for ct in self.iter_constraints(): + if not ct.is_generated() and ct not in setof_removed_cts: + if isinstance(ct, PwlConstraint): + continue + if ct.is_linear(): + linear_cts.append(ct.copy(copy_model, memo)) + elif ct.is_logical: + if linear_cts: + # add stored linear cts + copy_model.add_constraints(linear_cts) + linear_cts = [] + copy_model.add(ct.copy(copy_model, memo)) + + if linear_cts: + copy_model.add_constraints(linear_cts) + + # clone objective + copy_model.set_objective_sense(self.objective_sense) + if self.has_multi_objective(): + multi_objective = self._multi_objective + exprs = multi_objective.exprs + nb_exprs = len(exprs) + copied_exprs = [expr.copy(copy_model, memo) for expr in exprs] + copy_model.set_multi_objective(self.objective_sense, + exprs=copied_exprs, + priorities=multi_objective.priorities, + weights=multi_objective.weights, + abstols=MultiObjective.as_optional_sequence(multi_objective.abstols, + nb_exprs), + reltols=MultiObjective.as_optional_sequence(multi_objective.reltols, + nb_exprs), + names=multi_objective.names) + + else: + copy_model.set_objective(self.objective_sense, self.objective_expr.copy(copy_model, memo)) + + # clone kpis + for kpi in self.iter_kpis(): + copy_model.add_kpi(kpi.copy(copy_model, memo)) + + # clone sos + for sos in self.iter_sos(): + if not sos.is_generated(): + copy_model._create_engine_sos(sos.copy(copy_model, memo)) + + # parameters + for p in self.parameters.iter_params(): + if p.is_nondefault(): + # copy value to new parames + newp = copy_model.get_parameter_from_id (p.cpx_id) + if newp: + newp.set(p.value) + + return copy_model + +
[docs] def end(self): + """ Terminates a model instance. + + Since this method destroys the objects associated with the model, you must not use the model + after you call this member function. + This method must be called when you don't need a CPLEX engine anymore to free resources, + unless the docplex.Model was created with the `with` keyword. + + """ + self._clear_internal(terminate=True)
+ + @property + def parameters(self): + """ This property returns the root parameter group of the model. + + The root parameter group models the parameter hierarchy. + It is the way to access any CPLEX parameter and get or set its value. + + Examples: + + .. code-block:: python + + model.parameters.mip.tolerances.mipgap + + Returns the parameter itself, an instance of the `Parameter` class. + + To get the value of the parameter, use the `get()` method, as in: + + .. code-block:: python + + model.parameters.mip.tolerances.mipgap.get() + >>> 0.0001 + + To change the value of the parameter, use a standard Python assignment: + + .. code-block:: python + + model.parameters.mip.tolerances.mipgap = 0.05 + model.parameters.mip.tolerances.mipgap.get() + >>> 0.05 + + Assignment is equivalent to the `set()` method: + + .. code-block:: python + + model.parameters.mip.tolerances.mipgap.set(0.02) + model.parameters.mip.tolerances.mipgap.get() + >>> 0.02 + + Returns: + The root parameter group, an instance of the `ParameterGroup` class. + + """ + context_params = self.context.cplex_parameters + if not self._synced_params: + self._sync_params(context_params) + self._synced_params = True + return context_params + +
[docs] def get_parameter_from_id(self, parameter_cpx_id): + """ Finds a parameter from a CPLEX id code. + + Args: + parameter_cpx_id: A CPLEX parameter id (positive integer, for example, 2009 is mipgap). + + :returns: An instance of :class:`docplex.mp.params.parameters.Parameter` if found, else None. + """ + assert parameter_cpx_id >= 0 + for p in self.parameters.generate_params(): + if p.cpx_id == parameter_cpx_id: + return p + return None
+ + def get_engine_parameter_value(self, param): + return self.__engine.get_parameter(param) + + def apply_parameters(self): + self._apply_parameters_to_engine(self.parameters) + + def apply_one_parameter(self, param): + # internal + self.__engine.set_parameter(param, param.value) + + def _apply_parameters_to_engine(self, parameters_to_use): + # internal + if parameters_to_use is not None: + self_engine = self.__engine + for param in parameters_to_use: + self_engine.set_parameter(param, param.value) + + def _get_cplex_engine(self, caller): + # INTERNAL + self_engine = self.get_engine() + if self_engine.name != 'cplex_local': + self.fatal("{1} is only for Cplex, engine is '{0}'", self_engine.name, str(caller)) + else: + return self_engine + + def set_hidden_parameter(self, parameter_id, param_value): + self_engine = self._get_cplex_engine(caller="Model.set_hidden_parameter") + self_engine.set_parameter_from_id(parameter_id, param_value) + + def get_hidden_parameter(self, parameter_id): + self_engine = self._get_cplex_engine(caller="Model.get_hidden_parameter") + return self_engine.get_parameter_from_id(parameter_id) + + + # with protocol + def __enter__(self): + return self + + def __exit__(self, atype, avalue, atraceback): + # terminate the model upon exiting a 'with' block. + self.end() + + def __iadd__(self, e): + # implements the "+=" dialect a la PulP + self.add(e) + return self + + def _resync(self): + # INTERNAL + self._lfactory.resync_whole_model() + + def resync_engine(self): + # INTERNAL: resync after pickle + self.__engine.resync() + + def sync_cplex_engine(self): + if self.has_cplex(): + eng = self.get_engine() + eng.sync_cplex() + else: + self.warning('Model has no cplex, sync operation ignored.') + +
[docs] def add_sos1(self, dvars, name=None): + ''' Adds an SOS of type 1 to the model. + + Args: + dvars: The variables in the special ordered set. + This method only accepts ordered sequences of variables or iterators. + Unordered iterables (e.g. dictionaries or sets) are not accepted. + + name: An optional name. + + Returns: + The newly added SOS. + ''' + return self.add_sos(dvars, sos_arg=SOSType.SOS1, name=name)
+ +
[docs] def add_sos2(self, dvars, name=None): + ''' Adds an SOS of type 2 to the model. + + Args: + dvars: The variables in the specially ordered set. + This method only accepts ordered sequences of variables or iterators. + Unordered iterables (e.g. dictionaries or sets) are not accepted. + name: An optional name. + + Returns: + The newly added SOS. + ''' + return self.add_sos(dvars, sos_arg=SOSType.SOS2, name=name)
+ +
[docs] def add_sos(self, dvars, sos_arg, weights=None, name=None): + ''' Adds an SOS to the model. + + Args: + sos_arg: The SOS type. Valid values are numerical (1 and 2) or enumerated (`SOSType.SOS1` and + `SOSType.SOS2`). + dvars: The variables in the special ordered set. + This method only accepts ordered sequences of variables or iterators, + e.g. lists, numpy arrays, pandas Series. + Unordered iterables (e.g. dictionaries or sets) are not accepted. + weights: optional weights. Accepts None (no weights) or a list of numbers, with the same size as + number of variables. + name: An optional name. + + Returns: + The newly added SOS. + ''' + sos_type = SOSType.parse(sos_arg) + msg = 'Model.add_%s() expects an ordered sequence (or iterator) of variables' % sos_type.lower() + self._checker.check_ordered_sequence(arg=dvars, caller=msg) + var_seq = self._checker.typecheck_var_seq(dvars, caller="Model.add_sos") + + var_list = list(var_seq) # we need len here. + nb_vars = len(var_list) + if nb_vars < sos_type.size: + self.fatal("A {0:s} variable set must contain at least {1:d} variables, got: {2:d}", + sos_type.name, sos_type.size, nb_vars) + elif nb_vars == sos_type.size: + self.warning("{0:s} variable is trivial, contains {1} variable(s): all variables set to 1", + sos_type.name, sos_type.size) + lweights = StaticTypeChecker.typecheck_optional_num_seq(self, weights, accept_none=True, expected_size=nb_vars, + caller='Model.add_sos') + return self._add_sos(dvars, sos_type, weights=lweights, name=name)
+ + def _add_sos(self, dvars, sos_type, weights=None, name=None): + # INTERNAL + new_sos = self._lfactory.new_sos(dvars, sos_type=sos_type, weights=weights, name=name) + sos_index = self.__engine.create_sos(new_sos) + self._register_sos(new_sos, sos_index) + new_sos._set_index(sos_index) + return new_sos + + def _create_engine_sos(self, new_sos): + # internal + sos_index = self.__engine.create_sos(new_sos) + self._register_sos(new_sos, sos_index) + new_sos._set_index(sos_index) + + def _register_sos(self, new_sos, sos_index): + self._sos_scope.notify_obj_index(new_sos, sos_index) + + def _get_sos_by_index(self, sos_idx): + return self._sos_scope.get_object_by_index(sos_idx) + +
[docs] def iter_sos(self): + ''' Iterates over all SOS sets in the model. + + Returns: + An iterator object. + ''' + return self._sos_scope.iter_objects()
+ + @property + def number_of_sos(self): + ''' This property returns the total number of SOS sets in the model. + + ''' + return self._sos_scope.size + +
[docs] def clear_sos(self): + ''' Clears all SOS sets in the model. + ''' + self._sos_scope.clear() + self.__engine.clear_all_sos()
+ + def _generate_sos(self, sos_type): + # INTERNAL + for sos_set in self.iter_sos(): + if sos_set.sos_type == sos_type: + yield sos_set + +
[docs] def iter_sos1(self): + ''' Iterates over all SOS1 sets in the model. + + Returns: + An iterator object. + ''' + return self._generate_sos(SOSType.SOS1)
+ +
[docs] def iter_sos2(self): + ''' Iterates over all SOS2 sets in the model. + + Returns: + An iterator object. + ''' + return self._generate_sos(SOSType.SOS2)
+ + @property + def number_of_sos1(self): + ''' This property returns the total number of SOS1 sets in the model. + + ''' + return sum(1 for _ in self.iter_sos1()) + + @property + def number_of_sos2(self): + ''' This property returns the total number of SOS2 sets in the model. + + ''' + return sum(1 for _ in self.iter_sos2()) + +
[docs] def piecewise(self, preslope, breaksxy, postslope, name=None): + """ Adds a piecewise linear function (PWL) to the model, using breakpoints to specify the function. + + Args: + preslope: Before the first segment of the PWL function there is a half-line; its slope is specified by + this argument. + breaksxy: A list `(x[i], y[i])` of coordinate pairs defining segments of the PWL function. + postslope: After the last segment of the the PWL function there is a half-line; its slope is specified by + this argument. + name: An optional name. + + Example:: + + # Creates a piecewise linear function whose value if '0' if the `x_value` is `0`, with a slope + # of -1 for negative values and +1 for positive value + model = Model('my model') + model.piecewise(-1, [(0, 0)], 1) + + # Note that a PWL function may be discontinuous. Here is an example of a step function: + model.piecewise(0, [(0, 0), (0, 1)], 0) + + Returns: + The newly added piecewise linear function. + """ + + if breaksxy is None: + self._checker.fatal("argument 'breaksxy' must be defined") + + StaticTypeChecker.typecheck_num_nan_inf(self, preslope, caller='Model.piecewise.preslope') + StaticTypeChecker.typecheck_num_nan_inf(self, postslope, caller='Model.piecewise.postslope') + PwlFunction.check_list_pair_breaksxy(self._checker, breaksxy) + return self._piecewise(PwlFunction._PwlAsBreaks(preslope, breaksxy, postslope), name)
+ +
[docs] def piecewise_as_slopes(self, slopebreaksx, lastslope, anchor=(0, 0), name=None): + """ Adds a piecewise linear function (PWL) to the model, using a list of slopes and x-coordinates. + + Args: + slopebreaksx: A list of tuple pairs `(slope[i], breakx[i])` of slopes and x-coordinates defining the slope of + the piecewise function between the previous breakpoint (or minus infinity if there is none) + and the breakpoint with x-coordinate `breakx[i]`. + For representing a discontinuity, two consecutive pairs with the same value for `breakx[i]` + are used. The value of `slope[i]` in the second pair is the discontinuity gap. + lastslope: The slope after the last specified breakpoint. + anchor: The coordinates of the 'anchor point'. The purpose of the anchor point is to ground the piecewise + linear function specified by the list of slopes and breakpoints. + name: An optional name. + Example:: + + # Creates a piecewise linear function whose value if '0' if the `x_value` is `0`, with a slope + # of -1 for negative values and +1 for positive value + model = Model('my model') + model.piecewise_as_slopes([(-1, 0)], 1, (0, 0)) + + # Here is the definition of a step function to illustrate the case of a discontinuous PWL function: + model.piecewise_as_slopes([(0, 0), (0, 1)], 0, (0, 0)) + + Returns: + The newly added piecewise linear function. + """ + StaticTypeChecker.typecheck_num_nan_inf(self, lastslope, caller="Model.piecewise_as_slopes.lastslope") + StaticTypeChecker.check_number_pair(self, anchor, caller="Model.piecewise_as_slopes.anchor") + PwlFunction.check_list_pair_slope_breakx(self, slopebreaksx, anchor) + return self._piecewise(PwlFunction._PwlAsSlopes(slopebreaksx, lastslope, anchor), name)
+ + def add_piecewise_constraint(self, y, pwlf, x, name=None): + checker = self._checker + checker.typecheck_continuous_var(x) + checker.typecheck_continuous_var(y) + checker.typecheck_pwl_function(pwlf) + if x is y: + self.fatal('Piecewise-linear constraint requires two different variables, only one wa passed: {0}', x) + + pwl_expr = self._add_pwl_expr(pwlf, arg=x, yvar=y, resolve=False) # will be resolved later + pwl_ct = self._lfactory.new_pwl_constraint(pwl_expr, name) + return self._add_pwl_constraint_internal(pwl_ct) + + def _piecewise(self, pwl_def, name=None): + pwl_func = self._lfactory.new_piecewise(pwl_def, name) + self.__allpwlfuncs.append(pwl_func) + return pwl_func + + def _add_pwl_expr(self, pwl_func, arg, yvar=None, resolve=True): + pwl_expr = self._lfactory.new_pwl_expr(pwl_func, arg, y_var=yvar, resolve=resolve) + return pwl_expr + + def _add_pwl_constraint_internal(self, pwlct): + ct_engine_index = self.__engine.create_pwl_constraint(pwlct) + self._register_one_pwl_constraint(pwlct, ct_engine_index) + return pwlct + + def _register_one_pwl_constraint(self, new_pwl_ct, ct_index): + self.__notify_new_model_object( + "pwl", new_pwl_ct, ct_index, mobj_name=None, name_dir=None, idx_scope=self._pwl_scope, is_name_safe=True) + +
[docs] def iter_pwl_constraints(self): + """ Iterates over all PWL constraints in the model. + + Returns: + An iterator object. + """ + return self._pwl_scope.iter_objects()
+ + @property + def number_of_pwl_constraints(self): + """ This property returns the total number of PWL constraints in the model. + """ + return self._pwl_scope.size + + def _ensure_benders_annotations(self): + if self._benders_annotations is None: + self._benders_annotations = {} + return self._benders_annotations + + def set_benders_annotation(self, obj, group): + if group is None: + self_benders = self._benders_annotations + if self_benders is not None and obj in self_benders: + del self_benders[obj] + else: + self._checker.typecheck_int(group, accept_negative=False, caller='Model.set_benders_annotation') + self._ensure_benders_annotations()[obj] = group + + def remove_benders_annotation(self, obj): + self_benders = self._benders_annotations + if self_benders: + del self_benders[obj] + + def get_benders_annotation(self, obj): + self_benders = self._benders_annotations + return self_benders.get(obj) if self_benders is not None else None + + def iter_benders_annotations(self): + self_benders = self._benders_annotations + return self_benders.items() if self_benders is not None else iter([]) + + def clear_benders_annotations(self): + self._benders_annotations = None + + def get_annotations_by_scope(self): + # INTERNAL + from collections import defaultdict + annotated_by_scope = defaultdict(list) + for obj, group in self.iter_benders_annotations(): + annotated_by_scope[obj.cplex_scope].append((obj, group)) + return annotated_by_scope + + def has_benders_annotations(self): + self_benders = self._benders_annotations + return bool(self_benders) + + def get_annotation_stats(self): + from collections import Counter + annotated_by_scope = Counter() + for obj, group in self.iter_benders_annotations(): + annotated_by_scope[obj.cplex_scope] += 1 + return annotated_by_scope + + def register_callback(self, cb_type): + # Registers a callback with the model. + # + # Assumes the type has a `model` setter property. Use a subclass of `ModelCallbackMixin` mixin class + # as a parent class to ensure this. + # + # :param cb_type: a callback type; the type must be a subtype of some cplex callback type + # + # :return: an instance of the callback + # + cplex_cb = self.__engine.register_callback(cb_type) + if cplex_cb: + cplex_cb._model = self + return cplex_cb + + def _resolve_pwls(self): + # INTERNAL + self._objective_expr.resolve() + no_pwl_scopes = [self._linct_scope, self._logical_scope, self._quadct_scope] + for sc in no_pwl_scopes: + for x in sc.iter_objects(): + x.resolve() + # this call updates the dict so we must iterate on something else. + #pwls = [pw for pw in self._pwl_scope.iter_objects()] + pwls = list(self._pwl_scope.iter_objects()) + # BEWARE: need this temp list as resolve will modify the dict, so canno titerate on it. + for pw in pwls: + pw.resolve() + + def get_constraint_priority(self, ct): + # INTERNAL + return self._constraint_priority_dict.get(ct) # return None if not found + + def set_constraint_priority(self, ct, prio): + # INTERNAL + self._constraint_priority_dict[ct] = prio + + def _extend_constraint_section(self, collector, extra_cts, ctnames, caller): + # INTERNAL + checker = self._checker + new_cts = checker.typecheck_constraint_seq(extra_cts, check_linear=True, accept_range=False) + + if ctnames is not None: + checker.typecheck_iterable(ctnames) + for ct, ctn in izip2_filled(new_cts, ctnames): + checker.typecheck_string(ctn, accept_none=True) + if ctn: + self._register_ct_name(ct, ctn, checker) + + # extend + lncts = list(new_cts) + for nc in lncts: + checker.typecheck_ct_not_added(nc, do_raise=False, caller=caller) + + collector.extend(new_cts) + return new_cts + +
[docs] def add_lazy_constraints(self, lazy_cts, names=None): + """Adds lazy constraints to the problem. + + This method expects an iterable returning linear constraints (ranges are not accepted). + + :param lazy_cts: an iterable returning linear constraints (not ranges) + :param names: an optional iterable returning strings, used to set names for lazy constraints. + + *New in version 2.10* + """ + new_lazy_cts = self._extend_constraint_section(self._lazy_constraints, lazy_cts, names, caller='Model.add_lazy_constraints') + for nc in new_lazy_cts: + nc.notify_used_as_lazy_constraint() + self.__engine.add_lazy_constraints(new_lazy_cts)
+ +
[docs] def add_lazy_constraint(self, lazy_ct, name=None): + """Adds one lazy constraint to the problem. + + This method expects a linear constraint. + + :param lazy_ct: a linear constraints (ranges are not accepted) + :param name: an optional string, used to set the name of the lazy constraint. + + *New in version 2.10* + + """ + self.add_lazy_constraints((lazy_ct,), names=(name,))
+ +
[docs] def clear_lazy_constraints(self): + """ + Clears all lazy constraints from the model. + + *New in version 2.10* + """ + old_lazy_cts = self._lazy_constraints + for lz in old_lazy_cts: + lz.notify_unused_as_lazy_constraint() + self._lazy_constraints = [] + self.__engine.clear_lazy_constraints()
+ +
[docs] def iter_lazy_constraints(self): + """ Returns an iterator on the model's lazy constraints + + :return: an iterator on lazy constraints. + + *New in version 2.10* + """ + return iter(self._lazy_constraints)
+ + @property + def number_of_lazy_constraints(self): + """Returns the number of lazy constraints present in the model + """ + return len(self._lazy_constraints) + + def _is_lazy_constraint(self, lineart_ct): + # INTERNAL + return any(lc is lineart_ct for lc in self.iter_lazy_constraints()) + +
[docs] def add_user_cut_constraints(self, cut_cts, names=None): + """Adds user cut constraints to the problem. + + This method expects an iterable returning linear constraints (ranges are not accepted). + + :param cut_cts: an iterable returning linear constraints (not ranges) + :param names: an optional iterable returning strings, used to set names for user cut constraints. + + *New in version 2.10* + """ + new_user_cuts = self._extend_constraint_section(self._user_cuts, cut_cts, names, caller='Model.add_user_cut_constraints') + for nc in new_user_cuts: + nc.notify_used_as_user_cut() + self.__engine.add_user_cuts(new_user_cuts)
+ +
[docs] def add_user_cut_constraint(self, cut_ct, name=None): + """Adds one user cut constraint to the problem. + + This method expects a linear constraint. + + :param cut_ct: a linear constraints (ranges are not accepted) + :param name: an optional string, used to set the name for the cut constraint. + + *New in version 2.10* + """ + self.add_user_cut_constraints((cut_ct,), names=(name,))
+ +
[docs] def clear_user_cut_constraints(self): + """ + Clears all user cut constraints from the model. + + *New in version 2.10* + """ + old_user_cuts = self._user_cuts + for uc in old_user_cuts: + uc.notify_unused_as_user_cut() + + self._user_cuts = [] + self.__engine.clear_user_cuts()
+ +
[docs] def iter_user_cut_constraints(self): + """ Returns an iterator on the model's user cut constraints + + :return: an iterator on user cut constraints. + + *New in version 2.10* + """ + return iter(self._user_cuts)
+ + @property + def number_of_user_cut_constraints(self): + """Returns the number of user cut constraints present in the model + + *New in version 2.10* + + """ + return len(self._user_cuts) + + def _is_user_cut_constraint(self, lineart_ct): + # INTERNAL + return any(lc is lineart_ct for lc in self.iter_user_cut_constraints())
+ + +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/model_reader.html b/docs/2.24.232/mp/_modules/docplex/mp/model_reader.html new file mode 100644 index 0000000..ad25c0c --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/model_reader.html @@ -0,0 +1,864 @@ + + + + + + + + + docplex.mp.model_reader — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.model_reader

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+
+import os
+import time
+
+# docplex
+from docplex.mp.model import Model
+from docplex.mp.utils import DOcplexException, MockIterable
+from docplex.mp.environment import Environment
+
+from docplex.mp.params.cplex_params import get_params_from_cplex_version
+from docplex.mp.constants import ComparisonType
+from docplex.mp.constr import LinearConstraint
+
+from docplex.mp.cplex_adapter import CplexAdapter
+from docplex.mp.cplex_engine import CplexEngine
+
+from docplex.mp.quad import VarPair
+
+
+
[docs]class ModelReaderError(DOcplexException): + pass
+ + +class _CplexReaderFileContext(object): + def __init__(self, filename, read_method=None): + self._cplex = None + self._filename = filename + self._read_method = read_method or ["read"] + + def __enter__(self): + self.cpx_adapter = CplexAdapter() + cpx = self.cpx_adapter.cpx + # no output from CPLEX + cpx.set_results_stream(None) + cpx.set_log_stream(None) + cpx.set_warning_stream(None) + cpx.set_error_stream(None) + self_read_fn = cpx + for m in self._read_method: + self_read_fn = self_read_fn.__getattribute__(m) + + try: + self_read_fn(self._filename) + self._cplex = cpx + return self.cpx_adapter + + except self.cpx_adapter.CplexError as cpx_e: # pragma: no cover + # delete cplex instance + del cpx + raise ModelReaderError("*CPLEX error {0!s} reading file {1} - exiting".format(cpx_e, self._filename)) + + # noinspection PyUnusedLocal + def __exit__(self, exc_type, exc_val, exc_tb): + cpx = self._cplex + if cpx is not None: + del cpx + self._cplex = None + + +# def compute_full_dotf(mdl, coefs, constant=0): +# def coef_fn(dvx): +# return coefs[dvx] +# +# lfactory = mdl._lfactory +# terms_dict = lfactory._new_term_dict() +# for dv in mdl.iter_variables(): +# coef = coef_fn(dv.index) +# if coef: +# terms_dict[dv] = coef +# linear_expr = lfactory.linear_expr(terms_dict, constant=constant, safe=True) +# return linear_expr + + +def compute_full_dot(mdl, coefs, constant=0): + # compute a scalar product, with possible zeros as coefs + lfactory = mdl._lfactory + terms_dict = lfactory._new_term_dict() + for dv, k in zip(mdl.iter_variables(), coefs): + if k: + terms_dict[dv] = k + linear_expr = lfactory.linear_expr(terms_dict, constant=constant, safe=True) + return linear_expr + + +# noinspection PyArgumentList +
[docs]class ModelReader(object): + """ This class is used to read models from CPLEX files (e.g. SAV, LP, MPS) + This class requires CPLEX runtime, otherwise an exception is raised. + + Example: + Use class method ``read`` to read a model file. + + >>> m = ModelReader.read('mymodel.lp', ignore_names=True) + + reads a model while ignoring all names. + + Global function ``read_model`` is a synonym for ``ModelReader.read``, + and is the preferred way to read model files. + + """ + + @staticmethod + def _build_linear_expr_from_sparse_pair(lfactory, var_map, cpx_sparsepair): + terms = {var_map[ix]: k for ix, k in zip(cpx_sparsepair.ind, cpx_sparsepair.val)} + expr = lfactory.linear_expr(arg=terms, safe=True) + return expr + + _sense2comp_dict = {'L': ComparisonType.LE, 'E': ComparisonType.EQ, 'G': ComparisonType.GE} + + # noinspection PyDefaultArgument + @classmethod + def parse_sense(cls, cpx_sense, sense_dict=_sense2comp_dict): + return sense_dict.get(cpx_sense) + +
[docs] @classmethod + def read_prm(cls, filename): + """ Reads a CPLEX PRM file. + + Reads a CPLEX parameters file and returns a DOcplex parameter group + instance. This parameter object can be used in a solve(). + + Args: + filename: a path string + + Returns: + A `RootParameterGroup object`, if the read operation succeeds, else None. + """ + # TODO: Clean up - now creating an adapter raise importError if CPLEX not found + # if not Cplex: # pragma: no cover + # raise RuntimeError("ModelReader.read_prm() requires CPLEX runtime.") + with _CplexReaderFileContext(filename, read_method=["parameters", "read_file"]) as adapter: + cpx = adapter.cpx + if cpx: + # raw parameters + params = get_params_from_cplex_version(cpx.get_version()) + for param in params: + try: + cpx_value = cpx._env.parameters._get(param.cpx_id) + if cpx_value != param.default_value: + param.set(cpx_value) + + except adapter.CplexError: # pragma: no cover + pass + return params + else: # pragma: no cover + return None
+ + @staticmethod + def _safe_call_get_names(cpx_adapter, get_names_fn, fallback_names=None): + # cplex crashes when calling get_names on some files (e.g. SAV) + # in this case filter out error 1219 + # and return a fallback list with None or "" + try: + names = get_names_fn() + return names + # except TypeError: + # print("** type error ignored in call to {0}".format(get_names_fn.__name__)) + # return fallback_names or [] + + except cpx_adapter.CplexSolverError as cpxse: # pragma: no cover + errcode = cpxse.args[2] + # when all indicators have no names, cplex raises this error + # CPLEX Error 1219: No names exist. + if errcode == 1219: + return fallback_names or [] + else: + # this is something else + raise + + @classmethod + def _read_cplex(cls, filename, datacheck=None, silent=True): + cpx_adapter = CplexAdapter() + cpx = cpx_adapter.cpx + # no warnings + if silent: + cpx.set_results_stream(None) + cpx.set_log_stream(None) + cpx.set_warning_stream(None) + cpx.set_error_stream(None) # remove messages about names + try: + if datacheck is not None: + assert datacheck in {0,1,2} + cpx.parameters.read.datacheck.set(datacheck) + t0 = time.time() + cpx.read(filename) + t1 = time.time() - t0 + if Model.is_docplex_debug() and t1 >= 0.1: + print(f"-- cplex read time={t1:.1f}s") + return cpx_adapter + except cpx_adapter.CplexError as cpx_e: + raise ModelReaderError("*CPLEX error {0!s} reading file {1} - exiting".format(cpx_e, filename)) + + @classmethod + def _make_expr_from_varmap_coefs_dict(cls, lfactory, varmap, var_indices, coefs, offset=0): + terms_dict = {varmap.get(dvx): k for dvx, k in zip(var_indices, coefs)} + return lfactory.linear_expr(arg=terms_dict, constant=offset, safe=True) + + @classmethod + def _make_expr_from_var_coef_seq_dict(cls, lfactory, varmap, var_coefs, constant=0): + terms_dict = {varmap.get(dvx): k for dvx, k in var_coefs} + return lfactory.linear_expr(arg=terms_dict, constant=constant, safe=True) + + @classmethod + def _make_expr_from_varmap_coefs_nodict(cls, lfactory, varmap, var_indices, coefs, offset=0): + terms_dict = lfactory._new_term_dict() + for dvx, k in zip(var_indices, coefs): + dv = varmap.get(dvx) + if dv is not None: + terms_dict[dv] = k + return lfactory.linear_expr(arg=terms_dict, constant=offset, safe=True) + + @classmethod + def _make_expr_from_var_coef_seq_nodict(cls, lfactory, varmap, var_coefs, constant=0): + terms_dict = lfactory._new_term_dict() + for dvx, k in var_coefs: + dv = varmap.get(dvx) + if dv is not None: + terms_dict[dv] = k + return lfactory.linear_expr(arg=terms_dict, constant=constant, safe=True) + + @classmethod + def _make_expr_from_varmap_coefs(cls, lfactory, varmap, var_indices, coefs, offset=0): + if Environment.env_is_python36: + terms_dict = {varmap.get(dvx): k for dvx, k in zip(var_indices, coefs)} + else: + terms_dict = lfactory._new_term_dict() + for dvx, k in zip(var_indices, coefs): + dv = varmap.get(dvx) + if dv is not None: + terms_dict[dv] = k + return lfactory.linear_expr(arg=terms_dict, constant=offset, safe=True) + +
[docs] @classmethod + def read(cls, filename, model_name=None, verbose=False, model_class=None, **kwargs): + """ Reads a model from a CPLEX export file. + + Accepts all formats exported by CPLEX: LP, SAV, MPS. + + If an error occurs while reading the file, the message of the exception + is printed and the function returns None. + + Args: + filename: The file to read. + model_name: An optional name for the newly created model. If None, + the model name will be the path basename. + verbose: An optional flag to print informative messages, default is False. + model_class: An optional class type; must be a subclass of Model. + The returned model is built using this model_class and the keyword arguments kwargs, if any. + By default, the model class is :class:`docplex.mp.model.Model`. + kwargs: A dict of keyword-based arguments that are used when creating the modelpassed to the + model constructor. + + Example: + `m = read_model("c:/temp/foo.mps", model_name="docplex_foo", ignore_names=True)` + + reads a ,odel from file "c:/temp/foo.mps", sets its name to "docplex_foo", and discard all names. + + Returns: + An instance of Model, or None if an exception is raised. + + See Also: + :class:`docplex.mp.model.Model` + + """ + if not os.path.exists(filename): + raise IOError("* file not found: {0}".format(filename)) + + # extract basename + if model_name: + name_to_use = model_name + else: + basename = os.path.basename(filename) + if '.' not in filename: # pragma: no cover + raise RuntimeError('ModelReader.read_model(): path has no extension: {}'.format(filename)) + dotpos = basename.find(".") + if dotpos > 0: + name_to_use = basename[:dotpos] + else: # pragma: no cover + name_to_use = basename + + model_class = model_class or Model + + if 0 == os.stat(filename).st_size: + print("* file is empty: {0} - exiting".format(filename)) + return model_class(name=name_to_use, **kwargs) + + data_check = kwargs.pop('datacheck', None) + if verbose: + print("-> CPLEX starts reading file: {0}".format(filename)) + cpx_adapter = cls._read_cplex(filename, datacheck=data_check) + cpx = cpx_adapter.cpx + if verbose: + print("<- CPLEX finished reading file: {0}".format(filename)) + + if not cpx: # pragma: no cover + return None + + final_output_level = kwargs.get("output_level", "info") + debug_read = kwargs.get("debug", False) + final_agent = kwargs.get('agent', 'cplex') + use_new = kwargs.pop('use_new', True) + + + try: + # force no tck + if 'checker' in kwargs: + final_checker = kwargs['checker'] + else: + final_checker = 'default' + # build the model with no checker, then restore final_checker in the end. + kwargs['checker'] = 'off' + kwargs['agent'] = 'zero' + + ignore_names = kwargs.get('ignore_names', False) + # ------------- + if Environment.env_is_python36: + make_terms_fn = cls._make_expr_from_varmap_coefs_dict + make_terms_seq_fn = cls._make_expr_from_var_coef_seq_dict + else: + make_terms_fn = cls._make_expr_from_varmap_coefs_nodict + make_terms_seq_fn = cls._make_expr_from_var_coef_seq_nodict + + mdl = model_class(name=name_to_use, **kwargs) + if data_check is not None: + mdl.parameters.read.datacheck = data_check + mdl._provenance = filename + lfactory = mdl._lfactory + qfactory = mdl._qfactory + mdl.set_quiet() # output level set to ERROR + vartype_cont = mdl.continuous_vartype + vartype_map = {'B': mdl.binary_vartype, + 'I': mdl.integer_vartype, + 'C': mdl.continuous_vartype, + 'S': mdl.semicontinuous_vartype} + + def cpx_type_to_docplex_type(cpxt): + return vartype_map.get(cpxt, vartype_cont) + + # 1 upload variables + cpx_nb_vars = cpx.variables.get_num() + + if verbose: + print("-- uploading {0} variables...".format(cpx_nb_vars)) + + cpx_env_e = cpx._env._e + cpx_lp = cpx._lp + has_non_default_lb = cpx_adapter.has_non_default_lb + has_non_default_ub = cpx_adapter.has_non_default_ub + has_name = cpx_adapter.has_name + + cpx_var_names = [] + if not ignore_names: + if not has_name or has_name(cpx_env_e, cpx_lp, 0, cpx_nb_vars - 1): + cpx_var_names = cls._safe_call_get_names(cpx_adapter, cpx.variables.get_names) + + # check whether all varianbles have same type, it's worth it. + unique_type = None + docplex_vartypes = None + if cpx._is_MIP(): + cpx_types = cpx.variables.get_types() + first_cpxtype = cpx_types[0] + if all(cpxt == first_cpxtype for cpxt in cpx_types): + unique_type = cpx_type_to_docplex_type(first_cpxtype) + else: + # docplex_vartypes = [cpx_type_to_docplex_type(cpxt) for cpxt in cpx_types] + def kth_vartype(k): + return cpx_type_to_docplex_type(cpx_types[k]) + docplex_vartypes = MockIterable(size=cpx_nb_vars, fn_k=kth_vartype) + else: + unique_type = vartype_cont + + if use_new and has_non_default_lb and not has_non_default_lb(cpx_env_e, cpx_lp, 0, cpx_nb_vars - 1): + cpx_var_lbs = [] + else: + # either we have no fast predicate or we dont use it. + cpx_var_lbs = cpx.variables.get_lower_bounds() + # we have no predicate, so we might try to avoid building a large list?? + if not has_non_default_lb and not any(cpx_var_lbs): # all equal 0 + cpx_var_lbs = [] + + if use_new and has_non_default_ub and not has_non_default_ub(cpx_env_e, cpx_lp, 0, cpx_nb_vars - 1): + cpx_var_ubs = [] + else: + cpx_var_ubs = cpx.variables.get_upper_bounds() + + if not has_non_default_ub and all(cpxu >= 1e+20 for cpxu in cpx_var_ubs): + cpx_var_ubs = [] + + # if no names, None is fine. + model_varnames = cpx_var_names or None + + model_lbs = cpx_var_lbs + model_ubs = cpx_var_ubs + + # vars + if unique_type: + model_vars = lfactory.new_var_list(var_container=None, + key_seq=range(cpx_nb_vars), + vartype=unique_type, + lb=model_lbs, + ub=model_ubs, + name=model_varnames, + _safe_bounds=True, + _safe_names=True + ) + else: + assert docplex_vartypes + model_vars = lfactory.new_multitype_var_list(cpx_nb_vars, + docplex_vartypes, + model_lbs, + model_ubs, + model_varnames) + + # inverse map from indices to docplex vars + cpx_var_index_to_docplex = {v: model_vars[v] for v in range(cpx_nb_vars)} + + # 2. upload linear constraints and ranges (mixed in cplex) + cpx_linearcts = cpx.linear_constraints + nb_linear_cts = cpx_linearcts.get_num() + + all_rhs = cpx_linearcts.get_rhs() + all_senses = cpx_adapter.getsense(cpx_env_e, cpx_lp, 0, nb_linear_cts - 1) + if 'R' in all_senses: + all_range_values = cpx_linearcts.get_range_values() + else: + # do not query ranges if no R in senses... + all_range_values = [] + cpx_ctnames = [] if ignore_names else cls._safe_call_get_names(cpx_adapter, + cpx_linearcts.get_names) + + def make_constant_expr(k): + return lfactory.constant_expr(k, safe_number=True) + + if verbose: + print("-- uploading {0} linear constraints...".format(nb_linear_cts)) + + def upload_rows(row_iter): + cts_ = [] + for c, (indices, coefs) in enumerate(row_iter): + sense = all_senses[c] + rhs = all_rhs[c] + + ctname = cpx_ctnames[c] if cpx_ctnames else None + + expr = make_terms_fn(lfactory, cpx_var_index_to_docplex, indices, coefs) + + if sense == 'R': + # no use of querying range vars if no R constraints + range_val = all_range_values[c] + # rangeval can be negative !!! issue 52 + if range_val >= 0: + range_lb = rhs + range_ub = rhs + range_val + else: + range_ub = rhs + range_lb = rhs + range_val + + rgct = lfactory.new_range_constraint(lb=range_lb, ub=range_ub, expr=expr, name=ctname, + check_feasible=False) + cts_.append(rgct) + else: + op = cls.parse_sense(sense) + rhs_expr = make_constant_expr(rhs) + + ct = LinearConstraint(mdl, expr, op, rhs_expr, ctname) + cts_.append(ct) + + return cts_ + if nb_linear_cts: + fast_get_rows_tuple = cpx_adapter.fast_get_row_tuples + if fast_get_rows_tuple: + with fast_get_rows_tuple(cpx_env_e, cpx_lp) as rowgen: + deferred_cts = upload_rows(rowgen) + else: + all_rows = cpx_adapter.fast_get_rows(cpx) + deferred_cts = upload_rows(all_rows) + # add constraint as a block + lfactory._post_constraint_block(posted_cts=deferred_cts) + + # 3. upload Quadratic constraints + cpx_quadraticcts = cpx.quadratic_constraints + nb_quadratic_cts = cpx_quadraticcts.get_num() + if nb_quadratic_cts: + all_rhs = cpx_quadraticcts.get_rhs() + all_linear_nb_non_zeros = cpx_quadraticcts.get_linear_num_nonzeros() + all_linear_components = cpx_quadraticcts.get_linear_components() + all_quadratic_nb_non_zeros = cpx_quadraticcts.get_quad_num_nonzeros() + all_quadratic_components = cpx_quadraticcts.get_quadratic_components() + all_senses = cpx_quadraticcts.get_senses() + cpx_ctnames = [] if ignore_names else cls._safe_call_get_names(cpx_adapter, + cpx_quadraticcts.get_names) + + for c in range(nb_quadratic_cts): + rhs = all_rhs[c] + linear_nb_non_zeros = all_linear_nb_non_zeros[c] + linear_component = all_linear_components[c] + quadratic_nb_non_zeros = all_quadratic_nb_non_zeros[c] + quadratic_component = all_quadratic_components[c] + sense = all_senses[c] + ctname = cpx_ctnames[c] if cpx_ctnames else None + + if linear_nb_non_zeros > 0: + indices, coefs = linear_component.unpack() + # linexpr = mdl._aggregator._scal_prod((cpx_var_index_to_docplex[idx] for idx in indices), coefs) + linexpr = make_terms_fn(lfactory, cpx_var_index_to_docplex, indices, coefs) + else: + linexpr = None + + if quadratic_nb_non_zeros > 0: + qfactory = mdl._qfactory + ind1, ind2, coefs = quadratic_component.unpack() + quads = qfactory.term_dict_type() + for idx1, idx2, coef in zip(ind1, ind2, coefs): + quads[VarPair(cpx_var_index_to_docplex[idx1], cpx_var_index_to_docplex[idx2])] = coef + + else: # pragma: no cover + # should not happen, but who knows + quads = None + + quad_expr = mdl._aggregator._quad_factory.new_quad(quads=quads, linexpr=linexpr, safe=True) + op = ComparisonType.cplex_ctsense_to_python_op(sense) + ct = op(quad_expr, rhs) + mdl.add_constraint(ct, ctname) + + # 4. upload indicators + cpx_indicators = cpx.indicator_constraints + nb_indicators = cpx_indicators.get_num() + if nb_indicators: + all_ind_names = [] if ignore_names else cls._safe_call_get_names(cpx_adapter, + cpx_indicators.get_names) + + all_ind_bvars = cpx_indicators.get_indicator_variables() + all_ind_rhs = cpx_indicators.get_rhs() + all_ind_linearcts = cpx_indicators.get_linear_components() + all_ind_senses = cpx_indicators.get_senses() + all_ind_complemented = cpx_indicators.get_complemented() + all_ind_types = cpx_indicators.get_types() + ind_equiv_type = 3 + + for i in range(nb_indicators): + ind_bvar = all_ind_bvars[i] + ind_name = all_ind_names[i] if all_ind_names else None + ind_rhs = all_ind_rhs[i] + ind_linear = all_ind_linearcts[i] # SparsePair(ind, val) + ind_sense = cls.parse_sense(all_ind_senses[i]) + ind_complemented = all_ind_complemented[i] + ind_type = all_ind_types[i] + # 1 . check the bvar is ok + ind_bvar = cpx_var_index_to_docplex[ind_bvar] + # each var appears once + ind_linexpr = cls._build_linear_expr_from_sparse_pair(lfactory, cpx_var_index_to_docplex, + ind_linear) + ind_lct = lfactory._new_binary_constraint(ind_linexpr, ind_sense, ind_rhs) + if ind_type == ind_equiv_type: + logct = lfactory.new_equivalence_constraint( + ind_bvar, ind_lct, true_value=1 - ind_complemented, name=ind_name) + else: + logct = lfactory.new_indicator_constraint( + ind_bvar, ind_lct, true_value=1 - ind_complemented, name=ind_name) + mdl.add(logct) + + # 5. upload Piecewise linear constraints + try: + cpx_pwl = cpx.pwl_constraints + cpx_pwl_defs = cpx_pwl.get_definitions() + pwl_fallback_names = [""] * cpx_pwl.get_num() + cpx_pwl_names = pwl_fallback_names if ignore_names else cls._safe_call_get_names(cpx_adapter, + cpx_pwl.get_names, + pwl_fallback_names) + for (vary_idx, varx_idx, preslope, postslope, breakx, breaky), pwl_name in zip(cpx_pwl_defs, + cpx_pwl_names): + varx = cpx_var_index_to_docplex.get(varx_idx, None) + vary = cpx_var_index_to_docplex.get(vary_idx, None) + breakxy = [(brkx, brky) for brkx, brky in zip(breakx, breaky)] + pwl_func = mdl.piecewise(preslope, breakxy, postslope, name=pwl_name) + pwl_expr = mdl._lfactory.new_pwl_expr(pwl_func, varx, 0, resolve=False) + pwl_expr._f_var = vary + pwl_expr._ensure_resolved() + + except AttributeError: # pragma: no cover + pass # Do not check for PWLs if Cplex version does not support them + + # 6. upload objective + + # noinspection PyPep8 + try: + cpx_multiobj = cpx.multiobj + except AttributeError: # pragma: no cover + # pre-12.9 version + cpx_multiobj = None + + if cpx_multiobj is None or cpx_multiobj.get_num() <= 1: + cpx_obj = cpx.objective + cpx_sense = cpx_obj.get_sense() + fast_getobj = cpx_adapter.fast_getobj + if fast_getobj: + with fast_getobj(cpx_env_e, cpx_lp, 0, cpx_nb_vars-1) as var_coefs_tuple: + obj_expr = make_terms_seq_fn(lfactory, cpx_var_index_to_docplex, var_coefs_tuple) + else: + cpx_all_lin_obj_coeffs = cpx_obj.get_linear() + obj_expr = compute_full_dot(mdl, cpx_all_lin_obj_coeffs) + + if cpx_obj.get_num_quadratic_variables() > 0: + cpx_all_quad_cols_coeffs = cpx_obj.get_quadratic() + quads = qfactory.term_dict_type() + for v, col_coefs in zip(cpx_var_index_to_docplex, cpx_all_quad_cols_coeffs): + var1 = cpx_var_index_to_docplex[v] + indices, coefs = col_coefs.unpack() + for idx, coef in zip(indices, coefs): + vp = VarPair(var1, cpx_var_index_to_docplex[idx]) + quads[vp] = quads.get(vp, 0) + coef / 2 + + obj_expr += qfactory.new_quad(quads=quads, linexpr=None) + + obj_expr += cpx.objective.get_offset() + is_maximize = cpx_sense == cpx_adapter.cplex_module._internal._subinterfaces.ObjSense.maximize + + if is_maximize: + mdl.maximize(obj_expr) + else: + mdl.minimize(obj_expr) + else: + # we have multiple objective + + nb_multiobjs = cpx_multiobj.get_num() + exprs = [0] * nb_multiobjs + priorities = [1] * nb_multiobjs + weights = [1] * nb_multiobjs + abstols = [0] * nb_multiobjs + reltols = [0] * nb_multiobjs + if ignore_names: + names = ["Goal_{0}".format(g) for g in range(1, nb_multiobjs + 1)] + else: + names = cpx_multiobj.get_names() + + fast_multiobj = cpx_adapter.fast_multiobj_getobj is not None + if fast_multiobj: + def get_kth_multiobj(k): + weight = cpx_adapter.fast_multiobj_getweight(cpx_env_e, cpx_lp, k) + offset = cpx_adapter.fast_multiobj_getoffset(cpx_env_e, cpx_lp, k) + priority = cpx_adapter.fast_multiobj_getprio(cpx_env_e, cpx_lp, k) + abstol = cpx_adapter.fast_multiobj_getabstol(cpx_env_e, cpx_lp, k) + reltol = cpx_adapter.fast_multiobj_getreltol(cpx_env_e, cpx_lp, k) + with cpx_adapter.fast_multiobj_getobj(cpx_env_e, cpx_lp, k, 0, cpx_nb_vars-1) as objk_tuples: + obj_expr = make_terms_seq_fn(lfactory, cpx_var_index_to_docplex, objk_tuples, constant=offset) + return obj_expr, weight, priority, abstol, reltol + else: + def get_kth_multiobj(k): + (obj_coeffs, obj_offset, weight, prio, abstol, reltol) = cpx_multiobj.get_definition(k) + obj_expr = compute_full_dot(mdl, obj_coeffs, obj_offset) + return obj_expr, weight, prio, abstol, reltol + + for m in range(nb_multiobjs): + (obj_expr, weight, prio, abstol, reltol) = get_kth_multiobj(m) + exprs[m] = obj_expr + priorities[m] = prio + weights[m] = weight + abstols[m] = abstol + reltols[m] = reltol + sense = cpx_multiobj.get_sense() + mdl.set_multi_objective(sense, exprs, priorities, weights, abstols, reltols, names) + + # upload sos + cpx_sos = cpx.SOS + cpx_sos_num = cpx_sos.get_num() + if cpx_sos_num > 0: + cpx_sos_types = cpx_sos.get_types() + cpx_sos_indices = cpx_sos.get_sets() + cpx_sos_names = cpx_sos.get_names() + if not cpx_sos_names: + cpx_sos_names = [None] * cpx_sos_num + for sostype, sos_sparse, sos_name in zip(cpx_sos_types, cpx_sos_indices, cpx_sos_names): + sos_var_indices = sos_sparse.ind + sos_weights = sos_sparse.val + isostype = int(sostype) + sos_vars = [cpx_var_index_to_docplex[var_ix] for var_ix in sos_var_indices] + mdl.add_sos(dvars=sos_vars, sos_arg=isostype, name=sos_name, weights=sos_weights) + + # upload lazy constraints + cpx_linear_advanced = cpx.linear_constraints.advanced + cpx_lazyct_num = cpx_linear_advanced.get_num_lazy_constraints() + if cpx_lazyct_num: + print("WARNING: found {0} lazy constraints that cannot be uploaded to DOcplex".format(cpx_lazyct_num)) + + mdl.output_level = final_output_level + if final_checker: + # need to restore checker + mdl.set_checker(final_checker) + + except cpx_adapter.CplexError as cpx_e: # pragma: no cover + print("* CPLEX error: {0!s} reading file {1}".format(cpx_e, filename)) + mdl = None + if debug_read: + raise + + except ModelReaderError as mre: # pragma: no cover + print("! Model reader error: {0!s} while reading file {1}".format(mre, filename)) + mdl = None + if debug_read: + raise + + except DOcplexException as doe: # pragma: no cover + print("! Internal DOcplex error: {0!s} while reading file {1}".format(doe, filename)) + mdl = None + if debug_read: + raise + + # except Exception as any_e: # pragma: no cover + # print("Internal exception raised: {0} msg={1!s} while reading file '{2}'".format(type(any_e), any_e, filename)) + # mdl = None + # if debug_read: + # raise + + finally: + # clean up CPLEX instance... + # cpx.end() + pass + if mdl: + # reset engine, if necessary + if final_agent == 'zero': + pass # nothing to do + if final_agent == 'cplex': + mdl._set_engine(CplexEngine(mdl, _cplex=cpx_adapter)) + # mdl._check_scope_indices() + else: + # set a new non-cplex agent + mdl._set_new_engine_from_agent(final_agent) + mdl._set_solver_agent(final_agent) + + return mdl
+ +
[docs] @classmethod + def read_model(cls, filename, model_name=None, verbose=False, model_class=None, **kwargs): + """ This method is a synonym of `read` for compatibility. + + """ + import warnings + warnings.warn("ModelReader.read_model is deprecated, use class method ModelReader.read()", DeprecationWarning) + return cls.read(filename, model_name, verbose, model_class, **kwargs)
+ + +
[docs]def read_model(filename, model_name=None, verbose=False, **kwargs): + """ Reads a model from a CPLEX export file. + + Accepts all formats exported by CPLEX: LP, SAV, SAV.gz, MPS. + + If an error occurs while reading the file, the message of the exception + is printed and the function returns None. + + Args: + filename: The model file to read. + model_name: An optional name for the newly created model. If None, + the model name will be the path basename. + verbose: An optional flag to print informative messages, default is False. + kwargs: A dict of keyword-based arguments that are passed to the model contructor. + + Note: + This function requires CPLEX runtime, otherwise an exceotion is raised. + + Example: + `m = read_model("c:/temp/foo.mps", model_name="docplex_foo", solver_agent="local", output_level=100)` + + Returns: + An instance of Model, or None if an exception is raised. + + + + See Also: + :class:`docplex.mp.model.Model` + + """ + return ModelReader.read(filename, model_name=model_name, verbose=verbose, **kwargs)
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/model_stats.html b/docs/2.24.232/mp/_modules/docplex/mp/model_stats.html new file mode 100644 index 0000000..ca98fda --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/model_stats.html @@ -0,0 +1,334 @@ + + + + + + + + + docplex.mp.model_stats — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.model_stats

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+
+from io import StringIO
+
+
+
[docs]class ModelStatistics(object): + """ModelStatistics() + + This class gathers statistics from the model. + + Instances of this class are returned by the method :func:`docplex.mp.model.Model.get_statistics`. + + The class contains counters on the various types of variables and constraints + in the model. + + """ + + def __init__(self, nb_bvs, nb_ivs, nb_cvs, + nb_scvs, nb_sivs, + nb_le_cts, nb_ge_cts, nb_eq_cts, + nb_rng_cts, + nb_ind_cts, nb_equiv_cts, nb_quad_cts): + self._number_of_binary_variables = nb_bvs + self._number_of_integer_variables = nb_ivs + self._number_of_continuous_variables = nb_cvs + self._number_of_semicontinuous_variables = nb_scvs + self._number_of_semiinteger_variables = nb_sivs + self._number_of_le_constraints = nb_le_cts + self._number_of_ge_constraints = nb_ge_cts + self._number_of_eq_constraints = nb_eq_cts + self._number_of_range_constraints = nb_rng_cts + self._number_of_indicator_constraints = nb_ind_cts + self._number_of_equivalence_constraints = nb_equiv_cts + self._number_of_quadratic_constraints = nb_quad_cts + + def as_tuple(self): + return (self._number_of_binary_variables, + self._number_of_integer_variables, + self._number_of_continuous_variables, + self._number_of_semicontinuous_variables, + self._number_of_semiinteger_variables, + self._number_of_le_constraints, + self._number_of_ge_constraints, + self._number_of_eq_constraints, + self._number_of_range_constraints, + self._number_of_indicator_constraints, + self._number_of_equivalence_constraints, + self._number_of_quadratic_constraints) + + def equal_stats(self, other): + return isinstance(other, ModelStatistics) and (self.as_tuple() == other.as_tuple()) + + def __eq__(self, other): + return self.equal_stats(other) + + @property + def number_of_variables(self): + """ This property returns the total number of variables in the model. + + """ + return self._number_of_binary_variables \ + + self._number_of_integer_variables \ + + self._number_of_continuous_variables \ + + self._number_of_semicontinuous_variables + + @property + def number_of_binary_variables(self): + """ This property returns the number of binary decision variables in the model. + + """ + return self._number_of_binary_variables + + @property + def number_of_integer_variables(self): + """ This property returns the number of integer decision variables in the model. + + """ + return self._number_of_integer_variables + + @property + def number_of_continuous_variables(self): + """ This property returns the number of continuous decision variables in the model. + + """ + return self._number_of_continuous_variables + + @property + def number_of_semicontinuous_variables(self): + """ This property returns the number of semicontinuous decision variables in the model. + + """ + return self._number_of_semicontinuous_variables + + @property + def number_of_semiinteger_variables(self): + """ This property returns the number of semi-integer decision variables in the model. + + """ + return self._number_of_semiinteger_variables + + @property + def number_of_linear_constraints(self): + """ This property returns the total number of linear constraints in the model. + + This number comprises all relational constraints: <=, ==, and >= + and also range constraints. + + """ + return self._number_of_eq_constraints + \ + self._number_of_le_constraints + \ + self._number_of_ge_constraints + \ + self._number_of_range_constraints + + @property + def number_of_le_constraints(self): + """ This property returns the number of <= constraints + + """ + return self._number_of_le_constraints + + @property + def number_of_eq_constraints(self): + """ This property returns the number of == constraints + + """ + return self._number_of_eq_constraints + + @property + def number_of_ge_constraints(self): + """ This property returns the number of >= constraints + + """ + return self._number_of_ge_constraints + + @property + def number_of_range_constraints(self): + """ This property returns the number of range constraints. + + Range constraints are of the form L <= expression <= U. + + See Also: + :class:`docplex.mp.constr.RangeConstraint` + + """ + return self._number_of_range_constraints + + @property + def number_of_indicator_constraints(self): + """ This property returns the number of indicator constraints. + + See Also: + :class:`docplex.mp.constr.IndicatorConstraint` + + """ + return self._number_of_indicator_constraints + + @property + def number_of_equivalence_constraints(self): + """ This property returns the number of equivalence constraints. + + See Also: + :class:`docplex.mp.constr.EquivalenceConstraint` + + """ + return self._number_of_equivalence_constraints + + @property + def number_of_quadratic_constraints(self): + """ This property returns the number of quadratic constraints. + + See Also: + :class:`docplex.mp.constr.QuadraticConstraint` + + """ + return self._number_of_quadratic_constraints + + @property + def number_of_constraints(self): + return self.number_of_linear_constraints + \ + self.number_of_quadratic_constraints + \ + self.number_of_indicator_constraints + \ + self._number_of_equivalence_constraints + +
[docs] def print_information(self): + """ Prints model statistics in readable format. + + """ + print(' - number of variables: {0}'.format(self.number_of_variables)) + var_fmt = ' - binary={0}, integer={1}, continuous={2}' + if self._number_of_semicontinuous_variables: + var_fmt += ', semi-continuous={3}' + print(var_fmt.format(self.number_of_binary_variables, + self.number_of_integer_variables, + self.number_of_continuous_variables, + self._number_of_semicontinuous_variables + )) + + print(' - number of constraints: {0}'.format(self.number_of_constraints)) + ct_fmt = ' - linear={0}' + if self._number_of_indicator_constraints: + ct_fmt += ', indicator={1}' + if self._number_of_equivalence_constraints: + ct_fmt += ', equiv={2}' + if self._number_of_quadratic_constraints: + ct_fmt += ', quadratic={3}' + print(ct_fmt.format(self.number_of_linear_constraints, + self.number_of_indicator_constraints, + self.number_of_equivalence_constraints, + self.number_of_quadratic_constraints))
+ + def to_string(self): + oss = StringIO() + oss.write(" - number of variables: %d\n" % self.number_of_variables) + var_fmt = ' - binary={0}, integer={1}, continuous={2}' + if self._number_of_semicontinuous_variables: + var_fmt += ', semi-continuous={3}' + oss.write(var_fmt.format(self.number_of_binary_variables, + self.number_of_integer_variables, + self.number_of_continuous_variables, + self._number_of_semicontinuous_variables + )) + oss.write('\n') + nb_constraints = self.number_of_constraints + oss.write(' - number of constraints: {0}\n'.format(nb_constraints)) + if nb_constraints: + ct_fmt = ' - linear={0}' + if self._number_of_indicator_constraints: + ct_fmt += ', indicator={1}' + if self._number_of_equivalence_constraints: + ct_fmt += ', equiv={2}' + if self._number_of_quadratic_constraints: + ct_fmt += ', quadratic={3}' + oss.write(ct_fmt.format(self.number_of_linear_constraints, + self.number_of_indicator_constraints, + self.number_of_equivalence_constraints, + self.number_of_quadratic_constraints)) + return oss.getvalue() + + def __str__(self): + return self.to_string() + + def __repr__(self): # pragma: no cover + return "docplex.mp.Model.ModelStatistics()"
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/params/parameters.html b/docs/2.24.232/mp/_modules/docplex/mp/params/parameters.html new file mode 100644 index 0000000..3a673a0 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/params/parameters.html @@ -0,0 +1,1034 @@ + + + + + + + + + docplex.mp.params.parameters — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.params.parameters

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+
+from io import StringIO
+
+from docplex.mp.utils import is_int, is_string
+from docplex.mp.error_handler import docplex_fatal, DOcplexException
+
+
+
[docs]class ParameterGroup(object): + """ A group of parameters. + + Note: + This class is not meant to be instantiated by users. Models come + with a full hierarchy of parameters with groups as nodes. + + """ + + def __init__(self, name, parent_group=None): + self._name = name + self._parent = parent_group + self._params = [] + self._subgroups = [] + if parent_group: + parent_group._add_subgroup(self) + + def to_string(self, include_root=True): + return "group<%s>" % self.qualified_name(include_root=include_root) + + def __str__(self): + return self.to_string() + + def __repr__(self): + return "docplex.mp.params.ParameterGroup({0})".format(self.qualified_name()) + +
[docs] def clone(self): + """ + Returns: + A deep copy of the parameter group. + """ + from copy import deepcopy + return deepcopy(self)
+ + def copy(self): + return self.clone() + + @property + def name(self): + """ This property returns the name of the group. + + Note: + Parameter group names are always lowercase. + """ + return self._name + +
[docs] def iter_params(self): + """ Iterates over the group's own parameters. + + Returns: + A iterator over directparameters belonging to the group, not including + sub-groups. + """ + return iter(self._params)
+ + @property + def number_of_params(self): + """ This property returns the number of parameters in the group, not including subgroups. + """ + return len(self._params) + +
[docs] def total_number_of_params(self): + """ + Includes all parameters of subgroups recursively. + + Returns: + integer: The total number of parameters inside the group. + """ + subparams = sum(g.total_number_of_params() for g in self._subgroups) + return subparams + self.number_of_params
+ + def total_number_of_groups(self): + subgroups = sum(g.total_number_of_groups() for g in self._subgroups) + return subgroups + 1 + + @property + def number_of_subgroups(self): + """ This property returns the number of subgroups of the group, non-recursively. + + """ + return len(self._subgroups) + + def has_subgroups(self): + return len(self._subgroups) > 0 + + def iter_subgroups(self): + return iter(self._subgroups) + + @property + def parent_group(self): + """ This property returns the parent group (an instance of :class:`ParameterGroup`), or None for the root group. + """ + return self._parent + + def _add_param(self, param): + # internal + self._params.append(param) + + def _add_subgroup(self, subgroup): + # internal + self._subgroups.append(subgroup) + +
[docs] def is_root(self): + """ Checks whether the group is the root group, in other words, has no parent group. + + Returns: + Boolean: True if the group is the root group. + """ + return self._parent is None
+ + def root_group(self): + group = self + while not group.is_root(): + group = group.parent_group + return group + +
[docs] def qualified_name(self, sep=".", include_root=True): + """ Computes a string with all the parents of the parameters. + + Example: + `parameter mip.mip_cuts.Cover` returns "mip.mip_cuts.Covers". + + Args: + sep (string): The separator string. Default is ".". + include_root (flag): True if the root name is included. + + Returns: + string: A string representation of the parameter hierarchy. + """ + self_parent = self._parent + if not self_parent: + return self.name + if not include_root and self_parent.is_root(): + return self.name + else: + return "".join([self._parent.qualified_name(sep=sep, include_root=include_root), sep, self.name])
+ + def prettyprint(self, indent=0): + tab = indent * 4 * " " + print("{0}{1!s}={{".format(tab, self.qualified_name(include_root=False))) + for p in self.iter_params(): + print("{0} {1!s}".format(tab, p)) + for sg in self.iter_subgroups(): + assert isinstance(sg, ParameterGroup) + sg.prettyprint(indent + 1) + print("{0}}}".format(tab)) + + def _update_self_dict(self, extra_dict, do_check=True): + self_dict = self.__dict__ + if do_check: + # new entries should not already be present in self.dict + for k in extra_dict: + if k in self_dict: + # should not happen + print("!! update_self_dict: name collision with: %s" % k) # pragma: no cover + self_dict.update(extra_dict) + + def _restore_dict_recursive(self): + # internal use for deepcopy + param_dict = {p._name: p for p in self.iter_params()} + subgroup_dict = {sg._name: sg for sg in self.iter_subgroups()} + # update the __dict__ itself + self._update_self_dict(param_dict, do_check=False) + self._update_self_dict(subgroup_dict, do_check=False) + # recurse + for sg in self.iter_subgroups(): + sg._restore_dict_recursive() + + @staticmethod + def make(name, param_dict_fn, subgroup_fn, parent=None): + # INTERNAL + # factory method to create one group from: + # 1. a lambda function taking a group as argument and returning a dict of name: param instances + # 2. a dict of subgroup name: subgroup_make functions + # 3. a possibly-None parent group. If None, we are at root. + group = ParameterGroup(name, parent) if parent else RootParameterGroup(name, cplex_version=None) + group._initialize(param_dict_fn, subgroup_fn) + return group + + def _initialize(self, param_dict_fn, subgroup_fn): + param_dict = param_dict_fn(self) + self._update_self_dict(param_dict) + if subgroup_fn: + subgroup_fn_dict = subgroup_fn() + subgroup_dict = {group_name: group_fn(self) + for group_name, group_fn in subgroup_fn_dict.items()} + self._update_self_dict(subgroup_dict) + + def number_of_nondefaults(self): + return sum(1 for _ in self.generate_nondefault_params()) + + def has_nondefaults(self): + for _ in self.generate_nondefault_params(): + return True + else: + return False + +
[docs] def reset(self, recursive=False): + """ Resets all parameters in the group. + + Args: + recursive (Boolean): If True, also resets the subgroups. + """ + for p in self.iter_params(): + p.reset() + if recursive: + for g in self.iter_subgroups(): + g.reset(recursive=True)
+ + def reset_all(self): + self.reset(recursive=True) + +
[docs] def generate_params(self): + """ A generator function that traverses all parameters. + + The generator yields all parameters from the group + and also from its subgroups, recursively. + When called from the root parameter group, it returns all parameters. + + Returns: + A generator object. + + """ + return self._generate_and_filter_params(predicate=None)
+ + def _generate_and_filter_params(self, predicate): + """ A filtering generator function that traverses a group's parameters. + + This generator function traverses the group and its subgroup tree, + yielding only parameters that are accepted by th epredicate. + + Args: + predicate: A function that takes one parameter as asrgument. + The return value of this function will be interpreted as a boolean using + Python conversion rules. + + Returns: + A generator object. + """ + for p in self.iter_params(): + if predicate is None or predicate(p): + yield p + # now recurse + for sg in self.iter_subgroups(): + for nd in sg._generate_and_filter_params(predicate): + yield nd + +
[docs] def generate_nondefault_params(self): + """ A generator function that returns all non-default parameters. + + This generator function traverses the group and its subgroup tree, + yielding those parameters with a non-default value, one at a time. + A parameter is non-default when its value differs from the default. + + Returns: + A generator object. + """ + return self._generate_and_filter_params(predicate=lambda p: p.is_nondefault())
+ + def generate_all_subgroups(self): + # INTERNAL + for sg in self.iter_subgroups(): + yield sg + for ssg in sg.generate_all_subgroups(): + yield ssg + + def __setattr__(self, attr_name, value): + if attr_name.startswith("_"): + self.__dict__[attr_name] = value + elif hasattr(self, attr_name): + attr = getattr(self, attr_name) + if isinstance(attr, Parameter): + # attribute is set inside param, not necessarily in engine... + attr.set(value) + else: + docplex_fatal("No parameter with name {0} in {1}", attr_name, self.qualified_name()) + else: + docplex_fatal("No parameter with name {0} in {1}", attr_name, self.qualified_name()) + + @property + def cplex_version(self): + return self.root_group().cplex_version
+ + +
[docs]class Parameter(object): + """ Base class for all parameters. + + This class is not meant to be instantiated but subclassed. + + """ + __slots__ = ('_parent', '_name', '_cpx_name', '_id', '_description', '_default_value', + '_current_value', '_synchronous') + + # This global flag controls checking new values. + # If set to False, assigned values are not checked for min/max ranges + skip_range_check = False + + # noinspection PyProtectedMember + def __init__(self, group, short_name, cpx_name, param_key, description, default_value): + assert isinstance(group, ParameterGroup) + self._parent = group + self._name = short_name + self._cpx_name = cpx_name + self._id = param_key + self._description = description + self._default_value = default_value + # current = default at start... + self._current_value = default_value + self._synchronous = False + # link to parent group + group._add_param(self) + + def ctor_name(self): + return self.__class__.__name__ + + def is_synchronous(self): + return self._synchronous + + @property + def short_name(self): + return self._name + + @property + def name(self): + return self._name + + @property + def qualified_name(self): + """ Returns a hierarchical name string for the parameter. + + The qualified name reflects the location of the parameter in the parameter hierarchy. + The qualified name of a parameter is guaranteed to be unique. + + Examples: + `parameters.mip.tolerances.mipgap -> mip.tolerances.mipgap` + + Returns: + A unique name that reflects the parameter location in the hierarchy. + :rtype: + string + """ + return self.get_qualified_name(sep='.', include_root=True) + + def get_qualified_name(self, sep='.', include_root=True): + parent_qname = self._parent.qualified_name(sep=sep, include_root=include_root) + if parent_qname: + return "%s.%s" % (parent_qname, self.name) + else: + return self.name + + @property + def cpx_name(self): + """ Returns the CPLEX name of the parameter. This string is the CPLEX reference name which is used + in CPLEX Reference Manual. + + Examples: + `parameters.mip.tolerances.mipgap` has the name `CPX_PARAM_EPGAP` in CPLEX + + :rtype: + string + """ + return self._cpx_name + + @property + def cpx_id(self): + """ Returns the CPLEX integer code of the parameter. See the CPLEX Reference Manual for more + information. + + Returns: + An integer code. + """ + return self._id + + @property + def description(self): + """ Returns a string describing the parameter. + + :rtype: + string + """ + return self._description + + @property + def default_value(self): + """ Returns the default value of the parameter. This value can be numeric or a string, + depending on the parameter type. + + Examples: + `parameters.workdir` has default value "." + `parameters.optimalitytarget` has default value 0 + `parameters.mip.tolerances.mipgap` has default value of 0.0001 + + Returns: + The default value. + """ + return self._default_value + + def reset_default_value(self, new_default): + # INTERNAL: use with caution + self._default_value = new_default # pragma: no cover + self._current_value = new_default # pragma: no cover + + @property + def value(self): + return self._current_value + +
[docs] def accept_value(self, new_value): + """ Checks if `new_value` is an accepted value for the parameter. + + Args: + new_value: The candidate value. + + Returns: + Boolean: True if acceptable, else False. + + """ + raise NotImplementedError() # pragma: no cover
+ + def transform_value(self, raw_value): + # INTERNAL + return raw_value + + def _check_value(self, raw_value): + if raw_value == self.default_value: + return raw_value + elif not self.accept_value(raw_value): + docplex_fatal("Value {0!r} of type {2} is invalid for parameter '{1}'", + raw_value, self.get_qualified_name(include_root=False), type(raw_value)) + else: + return self.transform_value(raw_value) + + def __call__(self, *args): + if not args: + return self._current_value + elif len(args) == 1: + self.set(args[0]) + else: + docplex_fatal('Call parameter accepts either 0 or 1 argument') + +
[docs] def set(self, new_value): + """ Changes the value of the parameter to `new_value`. + + This method checks that the new value has the proper type and is valid. + Numeric parameters often specify a valid range with a minimum and a maximum value. + + If the value is valid, the current value of the parameter is changed, otherwise + an exception is raised. + + Args: + new_value: The new value for the parameter. + + Raises: + An exception if the value is not valid. + """ + accepted_value = self._check_value(new_value) + if accepted_value is not None: + self._current_value = accepted_value + if self._synchronous: + # print(" syncing {0!s} to {1}".format(self, accepted_value)) + self.root_group().apply(self) + + return accepted_value
+ +
[docs] def get(self): + """ Returns the current value of the parameter. + """ + return self._current_value
+ +
[docs] def reset(self): + """ Resets the parameter value to its default. + """ + self._current_value = self.default_value
+ +
[docs] def is_nondefault(self): + """ Checks if the current value of the parameter does not equal its default. + + Returns: + Boolean: True if the current value of the parameter differs from its default. + + """ + return self.get() != self._default_value
+ +
[docs] def is_default(self): + """ Checks if the current value of the parameter equals its default. + + Returns: + Boolean: True if the current value of the parameter equals its default. + """ + return self.get() == self.default_value
+ + @classmethod + def _is_in_range(cls, arg, range_min, range_max): + if not cls.skip_range_check: + if range_min is not None and arg < range_min: + return False + if range_max is not None and arg > range_max: + return False + return True + +
[docs] def to_string(self): + """ Converts the parameter to a string. + + This method is used in the `__str__` method to convert a parameter to a string. + + :rtype: + string + """ + return "{0}:{1:s}({2!s})".format(self._name, self.type_name, self._current_value)
+ + def __str__(self): + return self.to_string() + + def is_numeric(self): + # INTERNAL + return False # pragma: no cover + + @property + def type_name(self): + # INTERNAL + raise NotImplementedError # pragma: no cover + + def root_group(self): + return self._parent.root_group() + + def _repr_classname(self): + return "docplex.mp.params.{0}".format(self.__class__.__name__) + + def __repr__(self): + return "{0}({1},{2!s})".format(self._repr_classname(), self.qualified_name, self._current_value)
+ + +_BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True, + '0': False, 'no': False, 'false': False, 'off': False} + + +
[docs]class BoolParameter(Parameter): + __slots__ = () + + def __init__(self, group, short_name, cpx_name, param_key, description, default_value): + Parameter.__init__(self, group, short_name, cpx_name, param_key, description, default_value) + + def transform_value(self, new_value): + svalue = str(new_value).lower() + if new_value in {True, False}: + return new_value + elif new_value in {0, 1}: + return True if new_value else False + elif svalue in _BOOLEAN_STATES: + return _BOOLEAN_STATES[svalue] + else: + return None + +
[docs] def accept_value(self, value): + return value in {0, 1} or str(value).lower() in _BOOLEAN_STATES or value in {True, False}
+ + @property + def type_name(self): + return "bool"
+ + +
[docs]class StrParameter(Parameter): + __slots__ = () + + def __init__(self, group, short_name, cpx_name, param_key, description, default_value): + Parameter.__init__(self, group, short_name, cpx_name, param_key, description, default_value) + assert isinstance(default_value, str) + +
[docs] def accept_value(self, new_value): + return isinstance(new_value, str)
+ + @property + def type_name(self): + return "string" + +
[docs] def to_string(self): + """ Converts the parameter to a string. + + This method is used in the `__str__` method to convert a parameter to a string. + + :rtype: + string + """ + safe_value = self._current_value or '' + return "{0}:{1:s}('{2!s}')".format(self._name, self.type_name, safe_value)
+ +
[docs]class IntParameter(Parameter): + __slots__ = ('_min_value', '_max_value') + +
[docs] def accept_value(self, new_value): + return is_int(new_value) and self._is_in_range(new_value, self._min_value, self._max_value)
+ + def is_numeric(self): + return True # pragma: no cover + + def _get_min_value(self): + return self._min_value # pragma: no cover + + def _get_max_value(self): + return self._max_value # pragma: no cover + + def __init__(self, group, short_name, cpx_name, param_key, description, default_value, min_value=None, + max_value=None, sync=False): + Parameter.__init__(self, group, short_name, cpx_name, param_key, description, default_value) + self._min_value = int(min_value) + self._max_value = int(max_value) + self._synchronous = sync + + @property + def min_value(self): + return self._min_value + + @property + def max_value(self): + return self._max_value + + @property + def type_name(self): + return "int" + + def __repr__(self): + return "{0}({1},{2!s})".format(self._repr_classname(), self.qualified_name, self._current_value) + + def transform_value(self, new_value): + return int(new_value)
+ + +
[docs]class PositiveIntParameter(IntParameter): + __slots__ = () + + def __init__(self, group, short_name, cpx_name, param_key, description, default_value, max_value=None): + IntParameter.__init__(self, group, short_name, cpx_name, param_key, description, default_value, min_value=0, + max_value=max_value) + + @property + def type_name(self): + return "positive int"
+ + +
[docs]class NumParameter(Parameter): + """ A numeric parameter can take any floating-point value in the range of `[min,max]` values. + """ + __slots__ = ('_min_value', '_max_value') + + def __init__(self, group, short_name, cpx_name, param_key, description, default_value, min_value=None, + max_value=None): + Parameter.__init__(self, group, short_name, cpx_name, param_key, description, default_value) + self._min_value = min_value + self._max_value = max_value + + def is_numeric(self): + return True # pragma: no cover + + def _get_min_value(self): + return self._min_value # pragma: no cover + + def _get_max_value(self): + return self._max_value # pragma: no cover + + @property + def min_value(self): + return self._min_value + + @property + def max_value(self): + return self._max_value + +
[docs] def accept_value(self, new_value): + fvalue = float(new_value) + return self._is_in_range(fvalue, self._min_value, self._max_value)
+ + def transform_value(self, new_value): + return float(new_value) + + @property + def type_name(self): + return "number"
+ + +# a dictionary of formats for each type. +_param_prm_formats = {NumParameter: "%.14f", + IntParameter: "%d", + PositiveIntParameter: "%d", + BoolParameter: "%d", + StrParameter: "\"%s\"" # need quotes + } + + +
[docs]class RootParameterGroup(ParameterGroup): + """ The root parameter group (there should be only one instance at the root of the tree). + """ + + def __init__(self, name, cplex_version): + ParameterGroup.__init__(self, name) + self._cplex_version = cplex_version + self._models = [] + + def __deepcopy__(self, memodict={}): + """ + Returns: + A deep copy of the parameter group. + """ + import copy + saved_models = self._models + self._models = [] + new_root = RootParameterGroup(self.name, self._cplex_version) + memodict[id(self)] = new_root + for p in self.iter_params(): + new_root._add_param(copy.copy(p)) + for sg in self.iter_subgroups(): + new_root._add_subgroup(copy.deepcopy(sg, memodict)) + # add parameter and subgroup names to the inner __dict__ of the root + new_root._restore_dict_recursive() + # --- + for m in saved_models: + new_root.connect_model(m) + return new_root + + def connect_model(self, m): + self._models.append(m) + + def apply(self, param): + # apply one parameter to connected models + for m in self._models: + m.apply_one_parameter(param) + + @property + def cplex_version(self): + return self._cplex_version + +
[docs] def is_root(self): + return True
+ +
[docs] def export_prm(self, out, overload_params=None): + """ + Exports parameters to an output stream in PRM format. + + This method writes non-default parameters in CPLEX PRM syntax. + In addition to non-default parameters, some parameters can be forced to + be printed with a specific value by passing a dictionary with + Parameter objects as keys and values as arguments. + These values are used in the print operation, but will not be kept, + and the values of parameters will not be changed. + Passing `None` as `overload_params` will disable this functionality, and + only non-default parameters are printed. + + Args: + out: The output stream, typically a filename. + + overload_params: A dictionary of overloaded values for + certain parameters. This dictionary is of the form {param: value} + the printed PRM file will use overloaded values + for those parameters present in the dictionary. + + """ + cplex_version_string = self._cplex_version + out.write("# -- This content is generated by DOcplex\n") + out.write("CPLEX Parameter File Version %s\n" % cplex_version_string) + + param_generator = self.generate_params() + for param in param_generator: + if overload_params and param in overload_params: + param_value = overload_params[param] + else: + param_value = param.get() + + if param_value != param.default_value: + out.write("{0:<33}".format(param.cpx_name)) + out.write(_param_prm_formats[type(param)] % param_value) + out.write("\n") + + out.write("# --- end of generated prm data ---\n")
+ + def print_information(self, indent_level=0, print_all=False): + indent = ' ' * indent_level + param_generator = self.generate_params() + for param in param_generator: + if param.is_nondefault() or print_all: + param_value = param.get() + print("{0}{1} = {2!s}" + .format(indent, + param.qualified_name, + _param_prm_formats[type(param)] % param_value)) + + def export_prm_to_path(self, path, overload_params=None): + with open(path, mode='w') as out: + self.export_prm(out, overload_params=overload_params) + +
[docs] def print_info_to_stream(self, output, overload_params=None, print_defaults=False, indent_level=0): + """ Writes parameters to an output stream. + + This method writes non-default parameters in a human readable syntax. + In addition to non-default parameters, some parameters can be forced to + be printed with a specific value by passing a dictionary with + Parameter objects as keys and values as arguments. + These values are used in the print operation but not be kept, + and the values of parameters will not be changed. + Passing `None` as `overload_params` will disable this functionality, and + only non-default parameters are printed. + + Args: + output: The output stream. + overload_params: A dictionary of overloaded values for + certain parameters. This dictionary is of the form {param: value}. + """ + indent = " " * indent_level + param_generator = self.generate_params() + for param in param_generator: + if overload_params and param in overload_params: + param_value = overload_params[param] + elif print_defaults or param.is_nondefault(): + param_value = param.get() + else: + param_value = None + if param_value is not None: + output.write("{0}{1} = {2!s}\n" + .format(indent, + param.qualified_name, + _param_prm_formats[type(param)] % param_value))
+ +
[docs] def export_prm_to_string(self, overload_params=None): + """ Exports non-default parameters in PRM format to a string. + + The logic of overload is the same as in :func:`export_prm`. + A parameter is written if either it is a key on `overload_params`, + or it has a non-default value. + This allows merging non-default parameters with temporary parameter values. + + + Args: + overload_params: A dictionary of overloaded values, possibly None. + + Note: + This method has no side effects on the parameters. + + Returns: + string: A string in CPLEX PRM format. + """ + oss = StringIO() + self.export_prm(oss, overload_params) + return oss.getvalue()
+ +
[docs] def print_info_to_string(self, overload_params=None, print_defaults=False): + """ Writes parameters in readable format to a string. + + The logic of overload is the same as in :func:`export_prm`. + A parameter is written if either it is a key on `overload_params`, + or it has a non-default value. + This allows merging non-default params with temporary parameter values. + + Args: + overload_params: A dictionary of overloaded values, possibly None. + + Note: + This method has no side effects on the parameters. + + Returns: + A string. + """ + oss = StringIO() + self.print_info_to_stream(oss, print_defaults=print_defaults, overload_params=overload_params) + return oss.getvalue()
+ + @staticmethod + def make(name, param_dict_fn, subgroup_fn, cplex_version): + # INTERNAL + # factory method to create one group from: + # 1. a lambda function taking a group as argument and returning a dict of name: param instances + # 2. a dict of subgroup name: subgroup_make functions + # 3. a possibly-None parent group. If None, we are at root. + root_group = RootParameterGroup(name, cplex_version) + root_group._initialize(param_dict_fn, subgroup_fn) + return root_group + + def prettyprint(self, indent=0): + print("* CPLEX parameters version: {0}".format(self.cplex_version)) + ParameterGroup.prettyprint(self, indent) + + def __repr__(self): + return "docplex.mp.params.RootParameterGroup(%s)" % self.cplex_version + +
[docs] def qualified_name(self, sep='.', include_root=True): + return 'parameters' if include_root else ''
+ + def as_dict(self): + # INTERNAL: returns a dictionary of qualified name -> parameter + qdict = {p.qualified_name: p for p in self} + return qdict + + def __iter__(self): + for p in self.iter_params(): + yield p + # now recurse + for sg in self.iter_subgroups(): + for nd in sg._generate_and_filter_params(predicate=None): + yield nd + + def find_parameter(self, key): + if is_int(key): + pred = lambda p_: p_.cpx_id == key + elif is_string(key): + # eliminate initial '.' + pred = lambda p_: p_.get_qualified_name(include_root=False) == key + else: + docplex_fatal('Parameters.find() accepts either integer code or path-like name, got: {0!r}'.format(key)) + for p in self: + if pred(p): + return p + else: + return None + + def set_from_qualified_name(self, qname, pvalue): + qname_list = qname.split('.') + if not qname_list: + # empty qname + return + groups = qname_list[:-1] + pname = qname_list[-1] + group = self + for g in groups: + group = getattr(group, g) + if group is None: + raise ValueError("Bad parameter group name: {0}".format(g)) + res = None + try: + # extract parameter from group + target_param = getattr(group, pname) + if target_param: + target_param.set(pvalue) + res = pvalue + except AttributeError: + raise ValueError("Cannot find paramater {0} in group {1}".format(pname, group)) + except DOcplexException as dex: + raise + + return res
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/priority.html b/docs/2.24.232/mp/_modules/docplex/mp/priority.html new file mode 100644 index 0000000..92df887 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/priority.html @@ -0,0 +1,222 @@ + + + + + + + + + docplex.mp.priority — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.priority

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------
+from enum import Enum
+
+from docplex.mp.utils import is_number, is_string
+
+
+
[docs]class Priority(Enum): + """ + This enumerated class defines the priorities: VERY_LOW, LOW, MEDIUM, HIGH, VERY_HIGH, MANDATORY. + """ + + def __new__(cls, value, print_name): + obj = object.__new__(cls) + # predefined + obj._value_ = value + obj._print_name = print_name + return obj + + VERY_LOW = 100, 'Very Low' + LOW = 200, 'Low' + MEDIUM = 300, 'Medium' + HIGH = 400, 'High' + VERY_HIGH = 500, 'Very High' + MANDATORY = 999999999, 'Mandatory' + + def __repr__(self): + return 'Priority<{}>'.format(self.name) + + @property + def cplex_preference(self): + return self._get_geometric_preference_factor(base=10.0) + + def _get_geometric_preference_factor(self, base): + # INTERNAL: returns a CPLEX preference factor as a power of "base" + # MEDIUM priority is the balance point with a preference of 1. + assert is_number(base) + + if self.is_mandatory(): + return 1e+20 + else: + # noinspection PyTypeChecker + medium_index = Priority.MEDIUM.value / 100 + # pylint complains about no value member but is wrong! + diff = self.value / 100 - medium_index + factor = 1.0 + pdiff = diff if diff >= 0 else -diff + for _ in range(0, int(pdiff)): + factor *= base + return factor if diff >= 0 else 1.0 / factor + + def less_than(self, other): + assert isinstance(other, Priority) + return self.value < other.value + + def __lt__(self, other): + return self.less_than(other) + + def __gt__(self, other): + return other.less_than(self) + + def is_mandatory(self): + return self == Priority.MANDATORY + + @classmethod + def parse(cls, arg, logger, accept_none=True, do_raise=True): + ''' Converts its argument to a ``Priority`` object. + + Returns `default_priority` if the text is not a string, empty, or does not match. + + Args; + arg: The argument to convert. + + logger: An error logger + + accept_none: True if None is a possible value. Typically, + Constraint.set_priority accepts None as a way to + remove the constraint's own priority. + + do_raise: A Boolean flag indicating if an exception is to be raised if the value + is not recognized. + + Returns: + A Priority enumerated object. + ''' + if isinstance(arg, cls): + return arg + elif is_string(arg): + key = arg.lower() + # noinspection PyTypeChecker + for p in cls: + if key == p.name.lower() or key == str(p.value): + return p + if do_raise: + logger.fatal('String does not match priority type: {}', arg) + else: + logger.error('String does not match priority type: {}', arg) + return None + return None + elif accept_none and arg is None: + return None + else: + logger.fatal('Cannot convert to a priority: {0!s}'.format(arg))
+ + +class UserPriority(object): + + def __init__(self, pref, name=None): + assert pref >= 0 + self._preference = pref + self._name = name + + @property + def cplex_preference(self): + return self._preference + + # noinspection PyMethodMayBeStatic + def is_mandatory(self): + return False + + @property + def name(self): + return self._name or '_user_' + + @property + def value(self): + return self._preference + + def __str__(self): + name = self._name + sname = '%s: ' % name if name else '' + return 'UserPriority({0}{1})'.format(sname, self._preference) +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/progress.html b/docs/2.24.232/mp/_modules/docplex/mp/progress.html new file mode 100644 index 0000000..7cd4a5a --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/progress.html @@ -0,0 +1,755 @@ + + + + + + + + + docplex.mp.progress — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.progress

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2019
+# --------------------------------------------------------------------------
+'''This package contains classes to monitor the progress of a MIP solve.
+
+This abstract class defines protocol methods, which are called from within CPLEX's MIP search algorithm.
+
+Progress listeners are based on CPLEX's MIPInfoCallback, and work only with a
+local installation of CPLEX, not on Cplexcloud.
+
+At each node, all progress listeners attached to the model receive progress data
+through a `notify_progress` method, with an instance of :class:`~ProgressData`.
+This named tuple class contains information about solve time, objective value,
+best bound, gap and nodes, but no variable values.
+
+The base class for progress listeners is :class:`~ProgressListener`.
+DOcplex provides concrete sub-classes to handle basic cases, but
+you may also create your own sub-class of :class:`~ProgressListener` to suit your needs.
+Each instance of :class:`~ProgressListener` must be attached to a model to receive events during
+a MIP solve., using `Model.add_progress_listener`.
+
+An example of such a progress listener is :class:`TextProgressListener`,
+which prints a message to stdout in a format similar to CPLEX log, each time it receives a progress data.
+
+If you need to get the values of variables in an intermediate solution,
+you should sub-class from class :class:`~SolutionListener`.
+In this case, the :func:`~ProgressListener.notify_solution` method will be called
+each time CPLEX finds a valid intermediate solution, with an instance of :class:`docplex.mp.solution.SolveSolution`.
+
+Listeners are called from within CPLEX code, with its own frequency. It may occur that listeners are
+called with no real progress in either the objective or the best bound. To help you chose what kind
+of calls you are really interested in, DOcplex uses the enumerated class :class:`~ProgressClock'.
+The baseline clock is the CPLEX clock, but you can refine this clock: for example,
+`ProgressClock.Objective` filters all calls but those where the objective has improved.
+
+To summarize, listeners are called from within CPLEX's MIP search algorithm, when the clock
+you have chosen accepts the call. All listeners are created with a clock argument that controls which
+calls they accept.
+
+When a listener accepts a call, its method :func:`~ProgressListener.notify_progress` is called with the current progress data.
+See :class:`~TextProgressListener` as an example of a simple progress listener.
+
+In addition, if the listener derives from :class:`~SolutionListener`, its method :func:`~ProgressListener.notify_solution` is also called
+with the intermediate solution, passed as an instance of :class:`docplex.mp.solution.SolveSolution`.
+See :class:`~SolutionRecorder` as an example of solution listener
+
+'''
+from enum import Enum
+from collections import namedtuple
+
+
+from docplex.mp.utils import is_string, is_number
+from docplex.mp.error_handler import docplex_fatal
+
+_TProgressData_ = namedtuple('_TProgressData',
+                             ['id', 'has_incumbent',
+                              'current_objective', 'best_bound', 'mip_gap',
+                              'current_nb_iterations', 'current_nb_nodes', 'remaining_nb_nodes',
+                              'time', 'det_time'])
+
+
+# noinspection PyUnresolvedReferences
+
[docs]class ProgressData(_TProgressData_): + """ A named tuple class to hold progress data, as reeived from CPLEX. + + Attributes: + has_incumbent: a boolean, indicating whether an incumbent solution is available (or not), + at the moment the listener is called. + current_objective: contains the current objective, if an incumbent is available, else None. + best_bound: The current best bound as reported by the solver. + mip_gap: the gap between the best integer objective and the objective of the best node remaining; + available if an incumbent is available. + current_nb_nodes: the current number of nodes. + current_nb_iterations: the current number of iterations. + remaining_nb_nodes: the remaining number of nodes. + time: the elapsed time since solve started. + det_time: the deterministic time since solve started. + """ + + def __str__(self): # pragma: no cover + fmt = 'ProgressData({0}, {1}, obj={2}, bbound={3}, #nodes={4})'. \ + format(self.id, self.has_incumbent, self.current_objective, self.best_bound, self.current_nb_nodes) + return fmt
+ + +
[docs]class ProgressClock(Enum): + """ + This enumerated class controls the type of events a listener listens to. + + The possible values are (in order of decreasing call frequency). + + - All: the listener listens to all calls from CPLEX. + - BestBound: listen to changes in best bound, not necessarily with a solution present. + - Solutions: listen to all intermediate solutions, not necessarily impriving. + Nothing prevents being called several times with an identical solution. + + - Gap: listen to intermediate solutions, where either objective or best bound has improved. + - Objective: listen to intermediate solutions, where the solution objective has improved. + + To determine whether objective or best bound have improved (or not), see the numerical parameters + `absdiff` and `reldiff` in `:class:ProgressListener` and all its descendant classes. + + *New in version 2.10* + """ + # + # An enumeration of filtering levels for listeners. + # + All = 0 + Solutions = 1 + BestBound = 2 # best bound clock is indepedent from solution clock + Objective = 5 # b101 + Gap = 7 # the gap clock changes when there is a solution, and either bound or objective has changed + + @property + def listens_to_solution(self): + # returns true if the enum listens to solutions + return 1 == self.value & 1 + + @classmethod + def parse(cls, arg): + if isinstance(arg, ProgressClock): + return arg + else: + # int value + for fl in cls: + if arg == fl.value: + return fl + elif is_string(arg) and arg.lower() == fl.name.lower(): + return fl + else: + # pragma: no cover + raise ValueError('Expecting filter level, {0!r} was passed'.format(arg))
+ + +default_absdiff = 1e-1 +default_reldiff = 1e-2 + + +class _AbstractProgressListener(object): + + def __init__(self): + self._cb = None + self._current_progress_data = None + + def _get_model(self): + ccb = self._cb + return ccb._model if ccb else None + + def _set_current_progress_data(self, pdata): + # INTERNAL + self._current_progress_data = pdata + + def _disconnect(self): + # INTERNAL + self._cb = None + + def _connect_cb(self, cb): + # INTERNAL + self._cb = cb + + def abort(self): + ccb = self._cb + if ccb is not None: + ccb.abort() + else: + print('!!! callback is not connected - abort() ignored') + + def notify_start(self): + self._current_progress_data = None + + +
[docs]class ProgressListener(_AbstractProgressListener): + ''' The base class for progress listeners. + ''' + + def __init__(self, clock_arg=ProgressClock.All, absdiff=None, reldiff=None): + super().__init__() + clock = ProgressClock.parse(clock_arg) + self._clock = clock + self._absdiff = absdiff if absdiff is not None else default_absdiff + self._reldiff = reldiff if reldiff is not None else default_reldiff + self._filter = make_clock_filter(clock, absdiff, reldiff) + + @property + def clock(self): + """ Returns the clock of the listener. + + :return: + an instance of :class:`~ProgressClock` + + """ + return self._clock + + @property + def abs_diff(self): + return self._absdiff + + @property + def relative_diff(self): + return self._reldiff + + @property + def current_progress_data(self): + """ This property return the current progress data, if any. + + Returns the latest progress data, if at least one has been received. + + :return: an instance of :class:~ProgressData` or None. + """ + return self._current_progress_data + + def accept(self, pdata): + return self._filter.accept(pdata) + + def requires_solution(self): + return False + +
[docs] def notify_solution(self, s): + """ Redefine this method to handle an intermediate solution from the callback. + + Args: + s: solution + + Note: + if you need to access runtime information (time, gap, number of nodes), use + :func:`SolutionListener.current_progress_data`, which contains the latest + priogress information as a tuple. + """ + pass # pragma: no cover
+ +
[docs] def notify_start(self): + """ This methods is called on all attached listeners at the beginning of a solve(). + + When writing a custom listener, this method is used to restore + internal data which can be modified during solve. + Always call the superclass `notify_start` before adding specific code in a sub-class. + """ + super().notify_start() + self._filter.reset()
+ + def notify_jobid(self, jobid): + pass # pragma: no cover + +
[docs] def notify_end(self, status, objective): + """The method called when solve is finished on a model. + + The status is the solve status from the + solve() method + """ + pass # pragma: no cover
+ +
[docs] def notify_progress(self, progress_data): + """ This method is called from within the solve with a ProgressData instance. + + :param progress_data: an instance of :class:`ProgressData` containing data about the + current point in the search tree. + + """ + pass # pragma: no cover
+ +
[docs] def abort(self): + ''' Aborts the CPLEX search. + + This method tells CPLEX to stop the MIP search. + You may use this method in a custom progress listener to stop the search based on your own + criteria (for example, when improvements in gap is consistently below a minimum threshold). + + Note: + This method should be called from within a :func:`notify_progress` method call, otherwise it will have + no effect. + + ''' + super().abort()
+ + +# noinspection PyUnusedLocal,PyMethodMayBeStatic +class FilterAcceptAll(object): + + def accept(self, pdata): + return True + + def reset(self): + pass + + +# noinspection PyMethodMayBeStatic +class FilterAcceptAllSolutions(object): + + def accept(self, pdata): + return pdata.has_incumbent + + def reset(self): + pass + + +class Watcher(object): + + def __init__(self, name, absdiff, reldiff, update_fn): + assert absdiff >= 0 + assert reldiff >= 0 + + self.name = name + self._watched = None + self._old = None + self._absdiff = absdiff + self._reldiff = reldiff + self._update_fn = update_fn + + def reset(self): + self._watched = None + self._old = None + + def accept(self, progress_data): + accepted = False + + old = self._watched + new_watched = self._update_fn(progress_data) + # if new_watched is None, then it is not available (e.g. objective) + if new_watched is not None: + if old is None: + accepted = True + else: + # assert is a number + delta = abs(new_watched - old) + reldiff = self._reldiff + absdiff = self._absdiff + if 0 < absdiff <= abs(new_watched - old): + accepted = True + elif reldiff and delta / (1 + abs(old)) >= reldiff: + accepted = True + return accepted + + def sync(self, pdata): + cur = self._watched + self._watched = self._update_fn(pdata) + self._old = cur + + def __str__(self): # pragma: no cover + ws = '--' if self._watched is None else self._watched + return 'W_{0}[{1}]'.format(self.name, ws) + + +def clock_filter_accept_stop_here(watcher, pdata): + pass + + +class ClockFilter(object): + + def __init__(self, level, obj_absdiff, bbound_absdiff, obj_reldiff=0, bbound_reldiff=0, node_delta=-1): + watchers = [] + if obj_absdiff > 0 or obj_reldiff > 0: + def update_obj(pdata): + return pdata.current_objective if pdata.has_incumbent else None + + obj_watcher = Watcher(name='obj', absdiff=obj_absdiff, reldiff=obj_reldiff, update_fn=update_obj) + watchers.append(obj_watcher) + if bbound_absdiff > 0 or bbound_reldiff > 0: + def update_bbound(pdata): + return pdata.best_bound + + watchers.append(Watcher(name='gap_bound', absdiff=bbound_absdiff, reldiff=bbound_reldiff, + update_fn=update_bbound)) + if node_delta > 0: + used_node_delta = max(node_delta, 1) + + def update_nodes(pdata): + return pdata.current_nb_nodes + + watchers.append(Watcher(name='nodes', absdiff=used_node_delta, reldiff=0, update_fn=update_nodes)) + self._watchers = watchers + self._clock = level + self._listens_to_solution = level.listens_to_solution + + def accept(self, progress_data): + if not progress_data.has_incumbent and self._listens_to_solution: + return False + # the filter accepts data as soon as one of its cells accepts it. + poke = self.peek(progress_data) + if poke is None: + return False + else: + clock_filter_accept_stop_here(poke, progress_data) + # print("- accepting event #{0}, reason: {1}".format(progress_data.id, poke.name)) + for w in self._watchers: + w.sync(progress_data) + return True + + def peek(self, progress_data): + for w in self._watchers: + if w.accept(progress_data): + return w + else: + return None + + def reset(self): + for w in self._watchers: + w.reset() + + +# noinspection PyArgumentEqualDefault +def make_clock_filter(level, absdiff, reldiff, nodediff=0): + absdiff_ = 1e-1 if absdiff is None else absdiff + reldiff_ = 1e-2 if reldiff is None else reldiff + if level == ProgressClock.All: + return FilterAcceptAll() + elif level == ProgressClock.Solutions: + return FilterAcceptAllSolutions() + elif level == ProgressClock.Gap: + return ClockFilter(level, obj_absdiff=absdiff_, obj_reldiff=reldiff_, + bbound_absdiff=absdiff_, bbound_reldiff=reldiff_, node_delta=nodediff) + elif level == ProgressClock.Objective: + return ClockFilter(level, obj_absdiff=absdiff_, obj_reldiff=reldiff_, + bbound_absdiff=0, bbound_reldiff=0, node_delta=nodediff) + elif level == ProgressClock.BestBound: + return ClockFilter(level, obj_absdiff=0, obj_reldiff=0, + bbound_absdiff=absdiff_, bbound_reldiff=reldiff_) + + else: + # pragma: no cover + raise ValueError('unexpected level: {0!r}'.format(level)) + + +
[docs]class TextProgressListener(ProgressListener): + """ A simple implementation of Progress Listener, which prints messages to stdout, + in the manner of the CPLEX log. + + :param clock: an enumerated value of type `:class:ProgressClock` which defines the frequency of the listener. + :param absdiff: a float value which controls the minimum absolute change for objective or best bound values + :param reldiff: a float value which controls the minimum absolute change for objective or best bound values. + + Note: + The default behavior is to listen to an improvement in either the objective or best bound, + each being improved by either an absolute value change of at least `absdiff`, + or a relative change of at least `reldiff`. + + """ + + def __init__(self, clock=ProgressClock.Gap, gap_fmt=None, obj_fmt=None, + absdiff=None, reldiff=None): + ProgressListener.__init__(self, clock, absdiff, reldiff) + self._gap_fmt = gap_fmt or "{:.2%}" + self._obj_fmt = obj_fmt or "{:.4f}" + self._count = 0 + +
[docs] def notify_start(self): + super(TextProgressListener, self).notify_start() + self._count = 0
+ +
[docs] def notify_progress(self, progress_data): + self._count += 1 + pdata_has_incumbent = progress_data.has_incumbent + incumbent_symbol = '+' if pdata_has_incumbent else ' ' + # if pdata_has_incumbent: + # self._incumbent_count += 1 + current_obj = progress_data.current_objective + if pdata_has_incumbent: + objs = self._obj_fmt.format(current_obj) + else: + objs = "N/A" # pragma: no cover + best_bound = progress_data.best_bound + nb_nodes = progress_data.current_nb_nodes + remaining_nodes = progress_data.remaining_nb_nodes + if pdata_has_incumbent: + gap = self._gap_fmt.format(progress_data.mip_gap) + else: + gap = "N/A" # pragma: no cover + raw_time = progress_data.time + rounded_time = round(raw_time, 1) + + print("{0:>3}{7}: Node={4} Left={5} Best Integer={1}, Best Bound={2:.4f}, gap={3}, ItCnt={8} [{6}s]" + .format(self._count, objs, best_bound, gap, nb_nodes, remaining_nodes, rounded_time, + incumbent_symbol, progress_data.current_nb_iterations))
+ + +
[docs]class ProgressDataRecorder(ProgressListener): + """ A specialized class of ProgressListener, which collects all ProgressData it receives. + + """ + + def __init__(self, clock=ProgressClock.Gap, absdiff=None, reldiff=None): + super(ProgressDataRecorder, self).__init__(clock, absdiff, reldiff) + self._recorded = [] + +
[docs] def notify_start(self): + super(ProgressDataRecorder, self).notify_start() + # clear recorded data + self._recorded = []
+ +
[docs] def notify_progress(self, progress_data): + self._recorded.append(progress_data)
+ + @property + def number_of_records(self): + return len(self._recorded) + + @property + def iter_recorded(self): + """ Returns an iterator on stored progress data + + :return: an iterator. + """ + return iter(self._recorded) + + @property + def recorded(self): + """ Returns a copy of the recorded data. + + :return: + """ + return self._recorded[:]
+ + +
[docs]class SolutionListener(ProgressListener): + """ The base class for listeners that work on intermediate solutions. + + To define a custom behavior for a subclass of this class, you need to redefine `notify_solution`. + The current progress data is available from the `current_progress_data` property. + """ + + # noinspection PyMethodMayBeStatic + def check_solution_clock(self): + if not self.clock.listens_to_solution: + docplex_fatal('Solution listener requires a solution clock among (Solutions,Objective|Gap), {0} was passed', + self.clock) + + def __init__(self, clock=ProgressClock.Solutions, absdiff=None, reldiff=None): + super(SolutionListener, self).__init__(clock, absdiff, reldiff) + self.check_solution_clock() + + def requires_solution(self): + return True + +
[docs] def notify_solution(self, sol): + """ Generic method to be redefined by custom listeners to + handle intermediate solutions. This method is called by the + CPLEX search with an intermediate solution. + + :param sol: an instance of :class:`docplex.mp.solution.SolveSolution` + + Note: as this method is called at each node of the MIP search, it may happen + that several calls are made with an identical solution, that is, different object + instances, but sharing the same variable values. + """ + pass # pragma: no cover
+ +
[docs] def notify_start(self): + super(SolutionListener, self).notify_start()
+ + def accept(self, pdata): + return pdata.has_incumbent and super(SolutionListener, self).accept(pdata)
+ + +
[docs]class SolutionRecorder(SolutionListener): + """ A specialized implementation of :class:`SolutionListener`, + which stores --all-- intermediate solutions. + + As the listener might be called at different times with identical incumbent + values, thus the list of solutions list might well contain identical solutions. + """ + + def __init__(self, clock=ProgressClock.Gap, absdiff=None, reldiff=None): + super(SolutionRecorder, self).__init__(clock, absdiff, reldiff) + self._solutions = [] + +
[docs] def notify_start(self): + """ Redefinition of the generic notify_start() method to clear all data modified by solve(). + In this case, clears the list of solutions. + """ + super(SolutionListener, self).notify_start() + self._solutions = []
+ +
[docs] def notify_solution(self, sol): + """ Redefintion of the generic `notify_solution` method, called by CPLEX. + For this class, appends the intermediate solution to the list of stored solutions. + + """ + self._solutions.append(sol)
+ +
[docs] def iter_solutions(self): + """ Returns an iterator on the stored solutions""" + return iter(self._solutions)
+ + @property + def number_of_solutions(self): + """ Returns the number of stored solutions. """ + return len(self._solutions) + + @property + def current_solution(self): + # redefinition of generic method `notify_solution`, called by CPLEX. + sols = self._solutions + return sols[-1] if sols else None
+ + +
[docs]class FunctionalSolutionListener(SolutionListener): + """ A subclass of SolutionListener, which calls a function at each intermediate solution. + + No exception is caught. + """ + + def __init__(self, solution_fn, clock=ProgressClock.Gap, absdiff=None, reldiff=None): + SolutionListener.__init__(self, clock, absdiff, reldiff) + self._sol_fn = solution_fn + +
[docs] def notify_solution(self, sol): + self._sol_fn(sol)
+ + +
[docs]class KpiListener(SolutionListener): + """ A refinement of SolutionListener, which computes KPIs at each intermediate solution. + + Calls the `publish` method with a dicitonary of KPIs. Defaul tis to do nothing. + + This listener listens to the `Gap` clock. + + """ + + objective_kpi_name = '_current_objective' + time_kpi_name = '_current_time' + + def __init__(self, model, clock=ProgressClock.Gap, absdiff=None, reldiff=None): + super(KpiListener, self).__init__(clock, absdiff, reldiff) + self.model = model + +
[docs] def publish(self, kpi_dict): + """ This method is called at each improving solution, with a dictionay of name, values. + + :param kpi_dict: a dicitonary of names and KPi values, computed on the intermediate solution. + + """ + pass
+ +
[docs] def notify_solution(self, sol): + pdata = self.current_progress_data + + # 1. build a dict from formatted names to kpi values. + kpis_as_dict = {kp.name: kp.compute(sol) for kp in self.model.iter_kpis()} + # 2. add predefined keys for obj, time. + kpis_as_dict[self.objective_kpi_name] = sol.objective_value + kpis_as_dict[self.time_kpi_name] = pdata.time + self.publish(kpis_as_dict)
+ + +
[docs]class KpiPrinter(KpiListener): + + def __init__(self, model, clock=ProgressClock.Gap, absdiff=None, reldiff=None, + kpi_format='* ItCnt={3:d} KPI: {1:<{0}} = '): + super(KpiPrinter, self).__init__(model, clock, absdiff, reldiff) + self.kpi_format = kpi_format + +
[docs] def publish(self, kpi_dict): + try: + max_kpi_name_len = max(len(kn) for kn in kpi_dict) # max() raises ValueError on empty + except ValueError: + max_kpi_name_len = 0 + kpi_num_format = self.kpi_format + '{2:.3f}' + kpi_str_format = self.kpi_format + '{2!s}' + print('-' * (max_kpi_name_len + 15)) + itcnt = self.current_progress_data.current_nb_iterations + for kn, kv in kpi_dict.items(): + if is_number(kv): + k_format = kpi_num_format + else: + k_format = kpi_str_format + kps = k_format.format(max_kpi_name_len, kn, kv, itcnt) + print(kps)
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/publish.html b/docs/2.24.232/mp/_modules/docplex/mp/publish.html new file mode 100644 index 0000000..6f52099 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/publish.html @@ -0,0 +1,368 @@ + + + + + + + + + docplex.mp.publish — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.publish

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2019
+# --------------------------------------------------------------------------
+
+from docplex.mp.progress import SolutionListener, ProgressClock
+from docplex.mp.qprogress import QProgressListener
+
+from docplex.util.environment import get_environment
+
+try:
+    import pandas as pd
+except ImportError:
+    pd = None
+
+from docplex.util.csv_utils import write_csv, write_table_as_csv
+
+
+class _BarrierProgressPubisher(QProgressListener):
+
+    def __init__(self, publish_hook=None):
+        super().__init__()
+        self.publish_hook = publish_hook
+
+    def notify_progress(self, qprogress_data):
+        env = get_environment()
+        # 1. Start with empty table
+        name_values = {}
+
+        # new stats for https://github.ibm.com/IBMDecisionOptimization/dd-planning/issues/2491
+        if env.is_dods():
+            name_values['STAT.cplex.solve.iterationCount'] = qprogress_data.current_nb_iterations
+            name_values['STAT.cplex.solve.elapsedTime'] = qprogress_data.time
+            name_values['STAT.cplex.solve.primalObjective'] = qprogress_data.primal_objective_value
+            name_values['STAT.cplex.solve.dualObjective'] = qprogress_data.dual_objective_value
+            name_values['STAT.cplex.solve.primalInfeasibility'] = qprogress_data.primal_infeas
+            name_values['STAT.cplex.solve.dualInfeasibility'] = qprogress_data.dual_infeas
+
+        # publish ..or perish...
+        if self.publish_hook is not None:
+            self.publish_hook(name_values)
+
+
+class _KpiRecorder(SolutionListener):
+
+    def __init__(self, model, clock=ProgressClock.Gap,
+                 publish_hook=None,
+                 kpi_publish_format=None, absdiff=None, reldiff=None):
+        super(_KpiRecorder, self).__init__(clock, absdiff, reldiff)
+        self.model = model
+        self._context = model.context
+        self.publish_hook = publish_hook
+        self.kpi_publish_format = kpi_publish_format or 'KPI.%s'
+        self.publish_name_fn = lambda kn: self.kpi_publish_format % kn
+
+        # stored dictionaries of kpi values (name: value)
+        self._kpi_dicts = []
+
+    def notify_start(self):
+        super(_KpiRecorder, self).notify_start()
+        self._kpi_dicts = []
+
+    @property
+    def nb_reported(self):
+        return len(self._kpi_dicts)
+
+    def notify_solution(self, sol):
+        env = get_environment()
+        pdata = self.current_progress_data
+        context = self._context
+
+        # 1. Start with empty table
+        name_values = {}
+        # 2. add predefined keys for obj, time.
+        name_values['PROGRESS_CURRENT_OBJECTIVE'] = sol.objective_value
+
+        # 3. store it (why???)
+        self._kpi_dicts.append(name_values)
+
+        # new stats for https://github.ibm.com/IBMDecisionOptimization/dd-planning/issues/2491
+        if env.is_dods():
+            name_values['PROGRESS_GAP'] = pdata.mip_gap
+            name_values['PROGRESS_BEST_OBJECTIVE'] = pdata.best_bound
+            name_values['STAT.cplex.solve.explored'] = pdata.current_nb_nodes
+            name_values['STAT.cplex.solve.opened'] = pdata.remaining_nb_nodes
+            name_values['STAT.cplex.solve.iterationCount'] = pdata.current_nb_iterations
+            name_values['STAT.cplex.solve.elapsedTime'] = pdata.time
+
+        # add KPIs
+        publish_name_fn = self.publish_name_fn
+        name_values.update({publish_name_fn(kp.name): kp.compute(sol) for kp in self.model.iter_kpis()})
+        name_values[publish_name_fn('_time')] = pdata.time
+
+        # usually publish kpis in environment...
+        if self.publish_hook is not None:
+            self.publish_hook(name_values)
+
+        # save kpis.csv table
+        if auto_publishing_kpis_table_names(context) is not None:
+            write_kpis_table(env=env,
+                             context=context,
+                             model=self.model,
+                             solution=sol)
+
+    def iter_kpis(self):
+        return iter(self._kpi_dicts)
+
+    def __as_df__(self, **kwargs):
+        try:
+            from pandas import DataFrame
+        except ImportError:
+            raise RuntimeError("convert as DataFrame: This feature requires pandas")
+
+        df = DataFrame(self._kpi_dicts, **kwargs)
+        return df
+
+
+def get_auto_publish_names(context, prop_name, default_name):
+    # comparing auto_publish to boolean values because it can be a non-boolean
+    autopubs = context.solver.auto_publish
+    if autopubs == None:
+        return None
+    if autopubs is True:
+        return [default_name]
+    elif autopubs is False:
+        return None
+    elif prop_name in autopubs:
+        name = autopubs[prop_name]
+    else:
+        name = None
+
+    if isinstance(name, str):
+        # only one string value: make this the name of the table
+        # in a list with one object
+        name = [name]
+    elif name is True:
+        # if true, then use default name:
+        name = [default_name]
+    elif name is False:
+        # Need to compare explicitely to False
+        name = None
+    else:
+        # otherwise the kpi_table_name can be a collection-like of names,
+        # just return it
+        pass
+    return name
+
+
+def auto_publishing_result_output_names(context):
+    # Return the list of result output names for saving
+    return get_auto_publish_names(context, 'result_output', 'solution.json')
+
+
+def auto_publishing_kpis_table_names(context):
+    # Return the list of kpi table names for saving
+    return get_auto_publish_names(context, 'kpis_output', 'kpis.csv')
+
+
+def get_kpis_name_field(context):
+    autopubs = context.solver.auto_publish
+    if autopubs is True:
+        field = 'Name'
+    elif autopubs is False:
+        field = None
+    else:
+        field = context.solver.auto_publish.kpis_output_field_name
+    return field
+
+
+def get_kpis_value_field(context):
+    autopubs = context.solver.auto_publish
+    if autopubs is True:
+        field = 'Value'
+    elif autopubs is False:
+        field = None
+    else:
+        field = context.solver.auto_publish.kpis_output_field_value
+    return field
+
+
+
[docs]class PublishResultAsDf(object): + '''Mixin for classes publishing a result as data frame + ''' + + @staticmethod + def value_if_defined(obj, attr_name, default=None): + value = getattr(obj, attr_name) if hasattr(obj, attr_name) else None + return value if value is not None else default + +
[docs] def write_output_table(self, df, context, + output_property_name=None, + output_name=None): + '''Publishes the output `df`. + + The `context` is used to control the output name: + + - If context.solver.auto_publish is true, the `df` is written using + output_name. + - If context.solver.auto_publish is false, This method does nothing. + - If context.solver.auto_publish.output_property_name is true, + then `df` is written using output_name. + - If context.solver.auto_publish.output_propert_name is None or + False, this method does nothing. + - If context.solver.auto_publish.output_propert_name is a string, + it is used as a name to publish the df + + Example: + + A solver can be defined as publishing a result as data frame:: + + class SomeSolver(PublishResultAsDf) + def __init__(self, output_customizer): + # output something if context.solver.autopublish.somesolver_output is set + self.output_table_property_name = 'somesolver_output' + # output filename unless specified by somesolver_output: + self.default_output_table_name = 'somesolver.csv' + # customizer if users wants one + self.output_table_customizer = output_customizer + # uses pandas.DataFrame if possible, otherwise will use namedtuples + self.output_table_using_df = True + + def solve(self): + # do something here and return a result as a df + result = pandas.DataFrame(columns=['A','B','C']) + return result + + Example usage:: + + solver = SomeSolver() + results = solver.solve() + solver.write_output_table(results) + + ''' + + prop = self.value_if_defined(self, 'output_table_property_name') + prop = output_property_name if output_property_name else prop + default_name = self.value_if_defined(self, 'default_output_table_name') + default_name = output_name if output_name else default_name + names = get_auto_publish_names(context, prop, default_name) + use_df = self.value_if_defined(self, 'output_table_using_df', True) + if names: + env = get_environment() + customizer = self.value_if_defined(self, 'output_table_customizer', lambda x: x) + for name in names: + r = customizer(df) + if pd and use_df: + env.write_df(r, name) + else: + # assume r is a namedtuple + write_csv(env, r, r[0]._fields, name)
+ + def is_publishing_output_table(self, context): + prop = self.value_if_defined(self, 'output_table_property_name') + default_name = self.value_if_defined(self, 'default_output_table_name') + names = get_auto_publish_names(context, prop, default_name) + return names
+ + +def write_kpis_table(env, context, model, solution): + names = auto_publishing_kpis_table_names(context) + kpis_table = [] + for k in model.iter_kpis(): + kpis_table.append([k.name, k.compute(solution)]) + if kpis_table: + # do not create the kpi tables if there are no kpis to be written + field_names = [get_kpis_name_field(context), + get_kpis_value_field(context)] + for name in names: + write_table_as_csv(env, kpis_table, name, field_names) + + +def write_solution(env, solution, name): + with env.get_output_stream(name) as output: + output.write(solution.export_as_json_string().encode('utf-8')) + + +def write_result_output(env, context, model, solution): + names = auto_publishing_result_output_names(context) + for name in names: + write_solution(env, solution, name) +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/pwl.html b/docs/2.24.232/mp/_modules/docplex/mp/pwl.html new file mode 100644 index 0000000..cbff64a --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/pwl.html @@ -0,0 +1,965 @@ + + + + + + + + + docplex.mp.pwl — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.pwl

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+
+from docplex.mp.utils import is_iterable
+from docplex.mp.basic import ModelingObjectBase
+from docplex.mp.utils import DOcplexException, is_number
+
+from docplex.mp.sttck import StaticTypeChecker
+
+import copy
+
+
+
[docs]class PwlFunction(ModelingObjectBase): + """ + This class models piecewise linear (PWL) functions. This class is not intended to be instantiated: + piecewise linear functions are defined by invoking :func:`docplex.mp.model.Model.piecewise`, + or :func:`docplex.mp.model.Model.piecewise_as_slopes`. + + Piecewise-linear functions are important in many applications. + They are often specified either: + + * by giving a set of slopes, a set of breakpoints at which the slopes change, and the value of the functions at + a given point, or + * by giving an ordered list of (x,y) points that are linearly connected, along with the slope before the first + point and the slope after the last point. + + Note that a piecewise-linear function may be discontinuous. + """ + + @staticmethod + def check_number(logger, arg, caller=None): + StaticTypeChecker.typecheck_num_nan_inf(logger, arg, caller) + + + @staticmethod + def check_list_pair_breaksxy(logger, arg): + if not is_iterable(arg): + logger.fatal("argument 'breaksxy' expects iterable, {0!r} was passed".format(arg)) + if isinstance(arg, tuple): + # Encapsulate tuple argument into a list: this allows defining a PWL with a tuple if there is only + # one element in its definition + arg = [arg] + if len(arg) == 0: + logger.fatal("argument 'breaksxy' must be a non-empty list of (x, y) tuples.") + prev_pair = None + pprev_pair = None + for pair in arg: + if isinstance(pair, tuple): + if len(pair) != 2: + logger.fatal("invalid tuple in 'breaksxy': {0!s}. Each tuple must have 2 items.".format(pair)) + PwlFunction.check_number(logger, pair[0]) + PwlFunction.check_number(logger, pair[1]) + else: + logger.fatal("invalid item in 'breaksxy': {0!s}. Each item must be a (x, y) tuple.".format(pair)) + if prev_pair is not None: + if pair[0] < prev_pair[0]: + logger.fatal("X coordinate in: {0!s} cannot be smaller than previous break abscisse: {1!s}.". + format(pair, prev_pair)) + if pprev_pair is not None and pair[0] == prev_pair[0] and prev_pair[0] == pprev_pair[0]: + logger.fatal( + "invalid break: {0!s}. There cannot be more than 2 consecutive breaks with same abscisse.". + format(pair)) + pprev_pair = prev_pair + prev_pair = pair + + @staticmethod + def check_number_pair(logger, arg): + if arg is None: + logger.fatal("argument 'anchor' must be defined") + if isinstance(arg, tuple): + if len(arg) != 2: + logger.fatal("invalid tuple for 'anchor': {0!s}. Anchor argument must have 2 items.".format(arg)) + PwlFunction.check_number(logger, arg[0]) + PwlFunction.check_number(logger, arg[1]) + else: + logger.fatal("invalid value for 'anchor': {0!s}. Anchor argument must be a (x, y) tuple.".format(arg)) + + @staticmethod + def check_list_pair_slope_breakx(logger, arg, anchor): + if arg is None: + logger.fatal("argument 'slopebreaksx' must be defined") + if not is_iterable(arg): + logger.fatal("not an iterable: {0!s}".format(arg)) + if len(arg) == 0: + return + if isinstance(arg, tuple): + # Encapsulate tuple argument into a list: this allows defining a PWL with a tuple if there is only + # one element in its definition + arg = [arg] + prev_pair = None + pprev_pair = None + for pair in arg: + if isinstance(pair, tuple): + if len(pair) != 2: + logger.fatal("invalid tuple in 'slopebreaksx': {0!s}. Each tuple must have 2 items.". + format(pair)) + PwlFunction.check_number(logger, pair[0]) + PwlFunction.check_number(logger, pair[1]) + else: + logger.fatal("invalid item in 'slopebreaksx': {0!s}. Each item must be a (x, y) tuple.".format(pair)) + if prev_pair is not None: + if pair[1] < prev_pair[1]: + logger.fatal("X coordinate in: {0!s} cannot be smaller than previous break abscisse: {1!s}.". + format(pair, prev_pair)) + if pprev_pair is not None and pair[1] == prev_pair[1] and prev_pair[1] == pprev_pair[1]: + logger.fatal( + "invalid break: {0!s}. There cannot be more than 2 consecutive breaks with same abscisse.". + format(pair)) + if pair[1] == prev_pair[1] and anchor[0] == pair[1]: + logger.fatal("anchor {0!s} cannot be defined at discontinuity point: {1!s}". + format(anchor, pair)) + pprev_pair = prev_pair + prev_pair = pair + + class _PwlAsBreaks: + """ + When using this class, the piecewise linear function is specified by: + - Breakpoints defined as a list of coordinate pairs `(x[i], y[i])` defining the segments of the PWL function. + - Before the first segment of the PWL function there is a half-line; its slope is specified by `preslope`. + - After the last segment of the the PWL function there is a half-line; its slope is specified by `postslope`. + Two consecutive breakpoints may have the same x-coordinate; in such cases there is a discontinuity in the + PWL function. Three consecutive breakpoints may not have the same x-coordinate. + """ + + def __init__(self, preslope, breaksxy, postslope): + self._preslope = preslope + self._breaksxy = self._reformulate_breaksxy(breaksxy) + self._postslope = postslope + + @property + def preslope(self): + return self._preslope + + @property + def breaksxy(self): + return self._breaksxy + + @property + def postslope(self): + return self._postslope + + def deepcopy(self): + breaksxy_copy = copy.deepcopy(self.breaksxy) + return PwlFunction._PwlAsBreaks(self.preslope, breaksxy_copy, self.postslope) + + @staticmethod + def _reformulate_breaksxy(breaksxy): + if isinstance(breaksxy, tuple): + return [] if len(breaksxy) == 0 else [breaksxy] + return breaksxy + + @staticmethod + def _remove_useless_intermediate_breaks(preslope, breaksxy, postslope): + result_breaksxy = [] + current_slope = preslope + prev_break = None + for br in breaksxy: + if prev_break is None: + pass + else: + if br[0] == prev_break[0]: + # Check discontinuity + if br[1] != prev_break[1]: + result_breaksxy.append(prev_break) + result_breaksxy.append(br) + current_slope = None + else: + slope = (br[1] - prev_break[1]) / (br[0] - prev_break[0]) + if current_slope is not None and current_slope != slope: + # Add prev_break in list + result_breaksxy.append(prev_break) + current_slope = slope + prev_break = br + # Handle last break + if not result_breaksxy: + # Set result breaks = first break + result_breaksxy = breaksxy[0] + elif current_slope is not None and current_slope != postslope: + result_breaksxy.append(prev_break) + return preslope, result_breaksxy, postslope + + def _get_break_at_index(self, index): + if len(self.breaksxy) <= index: + return None, None, index + break_1 = self.breaksxy[index] + if len(self.breaksxy) > (index + 1): + break_2 = self.breaksxy[index + 1] + if break_1[0] == break_2[0]: + # Discontinuity + return break_1, break_2, index + 1 + return break_1, None, index + + def _get_y_value(self, x_coord, prev_break_index=-1): + """ + :param x_coord: + :param prev_break_index: this parameter is mandatory if a breakxy tuple does exist before x_coord. Otherwise + an exception is raised. + :return: + """ + if prev_break_index < 0: + break_1, break_2, last_ind = self._get_break_at_index(0) + if break_1[0] < x_coord: + raise DOcplexException("Invalid arguments passed to PwlAsBreaks._get_y_value()") + if break_1[0] == x_coord: + y_coord_1 = break_1[1] + y_coord_2 = None if break_2 is None else break_2[1] + return y_coord_1, y_coord_2, last_ind + y_coord_1 = break_1[1] - self.preslope * (break_1[0] - x_coord) + return y_coord_1, None, -1 + break_1, break_2, last_ind = self._get_break_at_index(prev_break_index) + next_break_1, next_break_2, next_last_ind = self._get_break_at_index(last_ind + 1) + if next_break_1 is None: + # x-coord is after last break + last_break = break_1 if break_2 is None else break_2 + y_coord_1 = last_break[1] + self.postslope * (x_coord - last_break[0]) + return y_coord_1, None, last_ind + else: + if x_coord == break_1[0]: + # Here, one must have: x_coord > break_1[0] + raise DOcplexException("Invalid arguments passed to PwlAsBreaks._get_y_value()") + if x_coord == next_break_1[0]: + y_coord_1 = next_break_1[1] + y_coord_2 = None if next_break_2 is None else next_break_2[1] + return y_coord_1, y_coord_2, next_last_ind + y_coord_prev = break_1[1] if break_2 is None else break_2[1] + y_coord_next = next_break_1[1] + slope = (y_coord_next - y_coord_prev) / (next_break_1[0] - break_1[0]) + y_coord_1 = y_coord_prev + slope * (x_coord - break_1[0]) + return y_coord_1, None, last_ind + + def evaluate(self, x_val): + """ Evaluates the breaks-based PWL function at the point whose x-coordinate is `x_val`. + + Args: + x_val: The x value for which we want to compute the value of the function. + + Returns: + The value of the PWL function at point `x_val` + A DOcplexException exception is raised when evaluating at a discontinuity of the PWL function. + """ + prev_break_index, index = -1, 0 + while index < len(self.breaksxy): + break_1, break_2, index = self._get_break_at_index(index) + if break_1 is None: + raise DOcplexException("Invalid PWL definition: no break point is defined") + if break_1[0] < x_val: + prev_break_index = index + else: + if break_1[0] == x_val and break_2 is not None: + raise DOcplexException("Cannot evaluate PWL at a discontinuity") + break + index += 1 + y_val, _, _ = self._get_y_value(x_val, prev_break_index) + return y_val + + def _get_all_breaks(self, all_x_coord): + all_breaks = [] + prev_break_ind = -1 + for x_coord in all_x_coord: + y_coord_1, y_coord_2, prev_break_ind = self._get_y_value(x_coord, prev_break_ind) + all_breaks.append((x_coord, y_coord_1) if y_coord_2 is None else + [(x_coord, y_coord_1), (x_coord, y_coord_2)]) + return all_breaks + + def get_nb_intervals(self): + nb_discontinuities = 0 + prev_br = None + for br in iter(self.breaksxy): + if prev_br is not None and prev_br[0] == br[0]: + nb_discontinuities += 1 + prev_br = br + return len(self.breaksxy) - nb_discontinuities - 1 + + def __add__(self, arg): + if isinstance(arg, PwlFunction._PwlAsBreaks): + all_x_coord = sorted({br[0] for br in self.breaksxy + arg.breaksxy}) + all_breaks_left = self._get_all_breaks(all_x_coord) + all_breaks_right = arg._get_all_breaks(all_x_coord) + result_breaksxy = [] + # Both lists have same size, with same x-coord for breaks ==> perform the addition on each break + for br_l, br_r in zip(all_breaks_left, all_breaks_right): + if isinstance(br_l, tuple) and isinstance(br_r, tuple): + result_breaksxy.append((br_l[0], br_l[1] + br_r[1])) + else: + if isinstance(br_l, tuple): + # br_r is a list containing 2 tuple pairs + result_breaksxy.append((br_l[0], br_l[1] + br_r[0][1])) + result_breaksxy.append((br_l[0], br_l[1] + br_r[1][1])) + elif isinstance(br_r, tuple): + # br_l is a list containing 2 tuple pairs + result_breaksxy.append((br_r[0], br_l[0][1] + br_r[1])) + result_breaksxy.append((br_r[0], br_l[1][1] + br_r[1])) + else: + # br_l and br_r are two lists, each containing 2 tuple pairs + result_breaksxy.append((br_l[0][0], br_l[0][1] + br_r[0][1])) + result_breaksxy.append((br_l[0][0], br_l[1][1] + br_r[1][1])) + result_preslope = self.preslope + arg.preslope + result_postslope = self.postslope + arg.postslope + return PwlFunction._PwlAsBreaks(*self._remove_useless_intermediate_breaks( + result_preslope, result_breaksxy, result_postslope)) + + elif is_number(arg): + return PwlFunction._PwlAsBreaks( + self.preslope, [(br[0], br[1] + arg) for br in self.breaksxy], self.postslope) + + else: + raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg)) + + def __sub__(self, arg): + if isinstance(arg, PwlFunction._PwlAsBreaks): + return self + arg * (-1) + elif is_number(arg): + return PwlFunction._PwlAsBreaks( + self.preslope, [(br[0], br[1] - arg) for br in self.breaksxy], self.postslope) + else: + raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg)) + + def __mul__(self, arg): + if is_number(arg): + return PwlFunction._PwlAsBreaks(*self._remove_useless_intermediate_breaks( + self.preslope * arg, [(br[0], br[1] * arg) for br in self.breaksxy], self.postslope * arg)) + else: + raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg)) + + def translate(self, arg): + if is_number(arg): + return PwlFunction._PwlAsBreaks( + self.preslope, [(br[0] + arg, br[1]) for br in self.breaksxy], self.postslope) + else: + raise DOcplexException("Invalid type for argument: {0!s}.".format(arg)) + + def __str__(self): + return self.to_string() + + def to_string(self): + return '({0}, {1}, {2})'.format(self.preslope, self.breaksxy, self.postslope) + + def repr_string(self): + return 'preslope={0},breaksxy={1},postslope={2}'.format(self.preslope, self.breaksxy, self.postslope) + + class _PwlAsSlopes: + """ + When using this class, the piecewise linear function is specified by: + - a list of tuple pairs `(slope[i], breakx[i])` of slopes and x-coordinates defining the slope of the piecewise + function between the previous breakpoint (or minus infinity if there is none) and the breakpoint with + x-coordinate `breakx[i]`, + - the slope after the last specified breakpoint, and + - the coordinates of the 'anchor point'. The purpose of the anchor point is to ground the piecewise-linear + function specified by the list of slopes and breakpoints. + Note that: + - The `breakx[i]` values must be increasing. If two consecutive `breakx` values have the same value, a + discontinuity is defined and the value associated with the second argument is considered to be a "step". + - The list of tuple pairs `(slope[i], breakx[i])` may be empty. + - The default value for the anchor point is the origin (point with coordinates (0, 0)). + - If the piecewise linear function defines some discontinuities, the anchor must not reside at one of + these discontinuities, since the function would not be uniquely defined. + """ + + def __init__(self, slopebreaksx, lastslope, anchor=(0, 0)): + self._slopebreaksx = self._reformulate_slopebreaksx(slopebreaksx) + self._lastslope = lastslope + self._anchor = anchor + + @property + def slopebreaksx(self): + return self._slopebreaksx + + @property + def lastslope(self): + return self._lastslope + + @property + def anchor(self): + return self._anchor + + def deepcopy(self): + slopebreaksx_copy = copy.deepcopy(self.slopebreaksx) + anchor_copy = copy.deepcopy(self.anchor) + return PwlFunction._PwlAsSlopes(slopebreaksx_copy, self.lastslope, anchor_copy) + + @staticmethod + def _reformulate_slopebreaksx(slopebreaksx): + if isinstance(slopebreaksx, tuple): + return [] if len(slopebreaksx) == 0 else [slopebreaksx] + return slopebreaksx + + @staticmethod + def _compute_breaksxy_after(slope_breaks, anchor): + breaks_xy = [] + start_x, start_y = anchor[0], anchor[1] + for (slope, break_x) in slope_breaks: + delta_x = break_x - start_x + start_x = break_x + if delta_x > 0: + start_y = start_y + slope * delta_x + else: + # Discontinuity: slope is considered to be a "step" + start_y += slope + breaks_xy.append((start_x, start_y)) + return breaks_xy + + @staticmethod + def _compute_breaksxy_before(start_slope, slope_breaks, anchor): + breaks_xy = [] + start_x, start_y = anchor[0], anchor[1] + last_slope = start_slope + for (slope, break_x) in slope_breaks: + delta_x = break_x - start_x + start_x = break_x + if delta_x < 0 or anchor[0] == break_x: + start_y = start_y + last_slope * delta_x + else: + # Discontinuity: slope is considered to be a "step" + start_y -= last_slope + last_slope = slope + breaks_xy.append((start_x, start_y)) + return breaks_xy, last_slope + + def convert_to_pwl_as_breaks(self): + breaks_before = [(s, b) for (s, b) in self.slopebreaksx if b <= self.anchor[0]] + breaks_after = [(s, b) for (s, b) in self.slopebreaksx if b > self.anchor[0]] + # Compute y value at each break point + anchor_slope = breaks_after[0][0] if len(breaks_after) > 0 else self.lastslope + breaks_before.reverse() + breaks_xy_before, preslope = self._compute_breaksxy_before(anchor_slope, breaks_before, self.anchor) + breaks_xy_before.reverse() + breaks_xy_after = self._compute_breaksxy_after(breaks_after, self.anchor) + # Now, we can build the PWL as breaks + breaksxy = breaks_xy_before + breaks_xy_after + if len(breaksxy) > 0: + return PwlFunction._PwlAsBreaks(preslope, breaksxy, self.lastslope) + else: + # No breakpoint is defined + return PwlFunction._PwlAsBreaks(self.lastslope, [self.anchor], self.lastslope) + + def _get_safe_xy_anchor(self): + """ + Return an anchor point that is on or after (if last break corresponds to a discontinuity) the largest + x-coord corresponding to a break or the anchor. + :return: + """ + breaks_after = [(s, b) for (s, b) in self.slopebreaksx if b > self.anchor[0]] + breaks_xy_after = self._compute_breaksxy_after(breaks_after, self.anchor) + if len(breaks_xy_after) > 0: + # Check if last break corresponds to a discontinuity + if len(breaks_xy_after) > 1 and breaks_xy_after[-2][0] == breaks_xy_after[-1][0]: + # Returns point with x-coord = last_x_coord + 1 + return breaks_xy_after[-1][0] + 1, breaks_xy_after[-1][1] + self.lastslope + return breaks_xy_after[-1] + return self.anchor + + @staticmethod + def _remove_useless_intermediate_slopes(slopebreaksx, lastslope, anchor): + result_slopebreaksx = [] + prev_sbr = None + for sbr in slopebreaksx: + if prev_sbr is not None: + if sbr[0] != prev_sbr[0]: + result_slopebreaksx.append(prev_sbr) + prev_sbr = sbr + if prev_sbr is not None and prev_sbr[0] != lastslope: + result_slopebreaksx.append(prev_sbr) + return result_slopebreaksx, lastslope, anchor + + def _get_all_slopebreaks(self, all_x_coord): + all_slopebreaks = [] + iter_slopebreakx = iter(self.slopebreaksx) + current_slopebreakx = next(iter_slopebreakx, None) + for x_coord in all_x_coord: + if current_slopebreakx is None: + all_slopebreaks.append((self.lastslope, x_coord)) + else: + while current_slopebreakx is not None and x_coord > current_slopebreakx[1]: + prev_slopebreakx = current_slopebreakx + current_slopebreakx = next(iter_slopebreakx, None) + if current_slopebreakx is not None and current_slopebreakx[1] == prev_slopebreakx[1]: + # Case of a discontinuity ==> update last item in result list to a list containing 2 tuples + all_slopebreaks[-1] = [(prev_slopebreakx[0], prev_slopebreakx[1]), + (current_slopebreakx[0], current_slopebreakx[1])] + if current_slopebreakx is None: + all_slopebreaks.append((self.lastslope, x_coord)) + else: + all_slopebreaks.append((current_slopebreakx[0], x_coord)) + # Handle case where last break is a discontinuity + prev_slopebreakx = current_slopebreakx + current_slopebreakx = next(iter_slopebreakx, None) + if current_slopebreakx is not None: + # Case of a discontinuity ==> update last item in result list to a list containing 2 tuples + all_slopebreaks[-1] = [(prev_slopebreakx[0], prev_slopebreakx[1]), + (current_slopebreakx[0], current_slopebreakx[1])] + return all_slopebreaks + + def __add__(self, arg): + if isinstance(arg, PwlFunction._PwlAsSlopes): + all_x_coord = sorted({sbr[1] for sbr in self.slopebreaksx + arg.slopebreaksx}) + all_slopebreaks_left = self._get_all_slopebreaks(all_x_coord) + all_slopebreaks_right = arg._get_all_slopebreaks(all_x_coord) + result_slopebreaksxy = [] + # Both lists have same size, with same x-coord for slopebreaks + # ==> perform the addition of slopes on each break + for sbr_l, sbr_r in zip(all_slopebreaks_left, all_slopebreaks_right): + if isinstance(sbr_l, tuple) and isinstance(sbr_r, tuple): + result_slopebreaksxy.append((sbr_l[0] + sbr_r[0], sbr_l[1])) + else: + if isinstance(sbr_l, tuple): + # sbr_r is a list containing 2 tuple pairs + result_slopebreaksxy.append((sbr_l[0] + sbr_r[0][0], sbr_l[1])) + result_slopebreaksxy.append((sbr_r[1][0], sbr_l[1])) + elif isinstance(sbr_r, tuple): + # sbr_l is a list containing 2 tuple pairs + result_slopebreaksxy.append((sbr_l[0][0] + sbr_r[0], sbr_r[1])) + result_slopebreaksxy.append((sbr_l[1][0], sbr_r[1])) + else: + # sbr_l and sbr_r are two lists, each containing 2 tuple pairs + result_slopebreaksxy.append((sbr_l[0][0] + sbr_r[0][0], sbr_l[0][1])) + result_slopebreaksxy.append((sbr_l[1][0] + sbr_r[1][0], sbr_l[0][1])) + result_lastslope = self.lastslope + arg.lastslope + + if self.anchor[0] == arg.anchor[0]: + result_anchor = (self.anchor[0], self.anchor[1] + arg.anchor[1]) + else: + # Compute a new anchor based on the last x-coord in the slopebreakx list + anchor point + anchor_l = self._get_safe_xy_anchor() + anchor_r = arg._get_safe_xy_anchor() + delta = anchor_r[0] - anchor_l[0] + if anchor_l[0] < anchor_r[0]: + result_anchor = (anchor_r[0], anchor_l[1] + anchor_r[1] + delta * self.lastslope) + else: + result_anchor = (anchor_l[0], anchor_l[1] + anchor_r[1] - delta * arg.lastslope) + + return PwlFunction._PwlAsSlopes(*self._remove_useless_intermediate_slopes( + result_slopebreaksxy, result_lastslope, result_anchor)) + + elif is_number(arg): + return PwlFunction._PwlAsSlopes(copy.deepcopy(self.slopebreaksx), + self.lastslope, (self.anchor[0], self.anchor[1] + arg)) + else: + raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg)) + + def __sub__(self, arg): + if isinstance(arg, PwlFunction._PwlAsSlopes): + return self + arg * (-1) + elif is_number(arg): + return PwlFunction._PwlAsSlopes(copy.deepcopy(self.slopebreaksx), + self.lastslope, (self.anchor[0], self.anchor[1] - arg)) + else: + raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg)) + + def __mul__(self, arg): + if is_number(arg): + return PwlFunction._PwlAsSlopes(*self._remove_useless_intermediate_slopes( + [(br[0] * arg, br[1]) for br in self.slopebreaksx], + self.lastslope * arg, (self.anchor[0], self.anchor[1] * arg))) + else: + raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg)) + + def translate(self, arg): + if is_number(arg): + return PwlFunction._PwlAsSlopes( + [(br[0], br[1] + arg) for br in self.slopebreaksx], self.lastslope, + (self.anchor[0] + arg, self.anchor[1])) + else: + raise DOcplexException("Invalid type for argument: {0!s}.".format(arg)) + + def __str__(self): + return self.to_string() + + def to_string(self): + return '{' + ''.join( + repr(slope) + ' -> ' + repr(break_x) + ';' for (slope, break_x) in self._slopebreaksx) + \ + repr(self._lastslope) + '}(' + repr(self.anchor[0]) + ', ' + repr(self.anchor[1]) + ')' + + # _name_generator = _AutomaticSymbolGenerator(pattern="pwl", offset=1) + + def __init__(self, model, pwl_def, name=None): + ModelingObjectBase.__init__(self, model, name=name) + self._pwl_def = pwl_def + self._pwl_def_as_breaks = None + self._set_pwl_definition(pwl_def) + + def _set_pwl_definition(self, pwl_def): + # INTERNAL + if isinstance(pwl_def, PwlFunction._PwlAsBreaks): + # Use the same data structure as input for internal representation (do not duplicate) + self._pwl_def_as_breaks = pwl_def + elif isinstance(pwl_def, PwlFunction._PwlAsSlopes): + pwl_def_as_breaks = pwl_def.convert_to_pwl_as_breaks() + self._set_pwl_as_breaks(pwl_def_as_breaks.preslope, pwl_def_as_breaks.breaksxy, + pwl_def_as_breaks.postslope) + else: + self.model._checker.fatal("Invalid definition for Piecewise Linear Function: {0!s}.".format(pwl_def)) + + def _set_pwl_as_breaks(self, preslope, breaksxy=None, postslope=None): + """Internal format to represent a piecewise linear function is based on the Cplex representation""" + self._pwl_def_as_breaks = self._PwlAsBreaks(preslope, breaksxy, postslope) + + def copy(self, target_model, _): + pwl_def_copy = self.pwl_def.deepcopy() + return target_model._piecewise(pwl_def_copy, self.name) + + @property + def pwl_def(self): + return self._pwl_def + + @property + def pwl_def_as_breaks(self): + return self._pwl_def_as_breaks + + # __call__ builds an expression equal to the piecewise linear value of its argument, based + # on the definition of the PWL function. + # + # Args: + # e: Accepts any object that can be transformed into an expression: + # decision variables, expressions, or numbers. + # + # Returns: + # An expression that can be used in arithmetic operators and constraints. + # + # Note: + # Building the expression generates one auxiliary decision variable. + def __call__(self, e): + self.model._checker.typecheck_operand(e, caller="Model.pwl", accept_numbers=True) + return self.model._add_pwl_expr(self, e) + + def __hash__(self): + return id(self) + + def __str__(self): + return self.pwl_def.__str__() + + def __repr__(self): + return 'docplex.mp.pwl.PwlFunction({0})'.format(self.pwl_def_as_breaks.repr_string()) + +
[docs] def clone(self): + """ Creates a copy of the PWL function on the same model. + + Returns: + The copy of the PWL function. + """ + return self.copy(self.model, None)
+ + def __add__(self, e): + return self.plus(e) + + def __radd__(self, e): + return self.plus(e) + + def __iadd__(self, e): + self.model.fatal('Cannot modify a PWL function') + # self.add(e) + # return self + + def plus(self, e): + cloned = self.clone() + return cloned.add(e) + +
[docs] def add(self, arg): + """ Adds an expression to self. + + Note: + This method does not create a new PWL function but modifies the `self` instance. + + Args: + arg: The expression to be added. Can be a PWL function or a number. + + Returns: + The modified self. + """ + if isinstance(arg, PwlFunction): + if (isinstance(self.pwl_def, PwlFunction._PwlAsBreaks) and + isinstance(arg.pwl_def, PwlFunction._PwlAsBreaks)) or \ + (isinstance(self.pwl_def, PwlFunction._PwlAsSlopes) and + isinstance(arg.pwl_def, PwlFunction._PwlAsSlopes)): + self._pwl_def = self.pwl_def + arg.pwl_def + self._set_pwl_definition(self._pwl_def) + else: + # Use Breaks representation + self._pwl_def = self.pwl_def_as_breaks + arg.pwl_def_as_breaks + self._set_pwl_definition(self._pwl_def) + elif is_number(arg): + self._pwl_def = self.pwl_def + arg + self._set_pwl_definition(self._pwl_def) + else: + raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg)) + return self
+ + def __sub__(self, e): + return self.minus(e) + + def __rsub__(self, e): + return self * (-1) + e + + def __isub__(self, e): + self.model.fatal('Cannot modify a PWL function') + + def minus(self, e): + cloned = self.clone() + return cloned.subtract(e) + +
[docs] def subtract(self, arg): + """ Subtracts an expression from this PWL function. + + Note: + This method does not create a new function but modifies the `self` instance. + + Args: + arg: The expression to be subtracted. Can be either a PWL function, or a number. + + Returns: + The modified self. + """ + if isinstance(arg, PwlFunction): + if (isinstance(self.pwl_def, PwlFunction._PwlAsBreaks) and + isinstance(arg.pwl_def, PwlFunction._PwlAsBreaks)) or \ + (isinstance(self.pwl_def, PwlFunction._PwlAsSlopes) and + isinstance(arg.pwl_def, PwlFunction._PwlAsSlopes)): + self._pwl_def = self.pwl_def - arg.pwl_def + self._set_pwl_definition(self._pwl_def) + else: + # Use Breaks representation + self._pwl_def = self.pwl_def_as_breaks - arg.pwl_def_as_breaks + self._set_pwl_definition(self._pwl_def) + elif is_number(arg): + self._pwl_def = self.pwl_def - arg + self._set_pwl_definition(self._pwl_def) + else: + raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg)) + return self
+ + def __mul__(self, e): + return self.times(e) + + def __rmul__(self, e): + return self.times(e) + + def __imul__(self, e): + self.model.fatal('Cannot modify a PWL function') + # return self.multiply(e) + + def __div__(self, e): + return self.quotient(e) + + def __truediv__(self, e): + # for py3 + # INTERNAL + return self.quotient(e) # pragma: no cover + + def __itruediv__(self, e): + # for py3 + # INTERNAL + self.model.fatal('Cannot modify a PWL function') # pragma: no cover + + def __idiv__(self, other): + self.model.fatal('Cannot modify a PWL function') + + def __rtruediv__(self, e): + # for py3 + self.fatal("PWL function {0!s} cannot be used as denominator of {1!s}", self, e) # pragma: no cover + + def __rdiv__(self, e): + self.fatal("PWL function {0!s} cannot be used as denominator of {1!s}", self, e) + + def quotient(self, e): + cloned = self.clone() + cloned.divide(e) + return cloned + +
[docs] def divide(self, arg): + """ Divides this PWL function by a number. + + Note: + This method does not create a new function but modifies the `self` instance. + + Args: + arg: The number that is used to divide `self`. + + Returns: + The modified `self`. + """ + self.model._typecheck_as_denominator(arg, numerator=self) + inverse = 1.0 / float(arg) + return self.multiply(inverse)
+ + def times(self, e): + cloned = self.clone() + return cloned.multiply(e) + +
[docs] def multiply(self, arg): + """ Multiplies this PWL function by a number. + + Note: + This method does not create a new function but modifies the `self` instance. + + Args: + arg: The number that is used to multiply `self`. + + Returns: + The modified `self`. + """ + if is_number(arg): + self._pwl_def = self.pwl_def * arg + self._set_pwl_definition(self._pwl_def) + else: + raise DOcplexException("Invalid type for right hand side operand: {0!s}.".format(arg)) + return self
+ +
[docs] def translate(self, arg): + """ Translate this PWL function by a number. + This method creates a new PWL function instance for which all breakpoints have been moved + along the horizontal axis by the amount specified by `arg`. + + Args: + arg: The number that is used to translate all breakpoints. + + Returns: + The translated PWL function. + """ + if is_number(arg): + return PwlFunction(self.model, self.pwl_def.translate(arg)) + else: + raise DOcplexException("Invalid type for argument: {0!s}.".format(arg))
+ +
[docs] def evaluate(self, x_val): + """ Evaluates the PWL function at the point whose x-coordinate is `x_val`. + + Args: + x_val: The x value for which we want to compute the value of the function. + + Returns: + The value of the PWL function at point `x_val`. + A DOcplexException exception is raised when evaluating at a discontinuity of the PWL function. + """ + return self._pwl_def_as_breaks.evaluate(x_val)
+ +
[docs] def plot(self, lx=None, rx=None, k=1, **kwargs): # pragma: no cover + """ + This method displays the piecewise linear function using the matplotlib package, if found. + + :param lx: The value to show the `preslope` (must be before the first breakpoint x value). + :param rx: The value to show the `postslope` (must be after the last breakpoint x value). + :param k: Scaling factor to calculate default values for `rx` and/or `lx` if these arguments are not provided, + based on mean interval length between the `x` values of breakpoints. + :param kwargs: additional arguments to be passed to matplotlib plot() function + """ + try: + import matplotlib.pyplot as plt + except ImportError: + raise DOcplexException('matplotlib is required for plot()') + bks = self.pwl_def_as_breaks.breaksxy + xs = [bk[0] for bk in bks] + ys = [bk[1] for bk in bks] + # compute mean delta_x + first_x = xs[0] + last_x = xs[-1] + nb_intervals = self._pwl_def_as_breaks.get_nb_intervals() + # k times the mean interval length is used for left/right extra points + kdx_m = k * (last_x - first_x) / float(nb_intervals) if nb_intervals > 0 else 1 + + if lx is None: + lx = first_x - kdx_m + ly = ys[0] - self.pwl_def_as_breaks.preslope * (first_x - lx) + xs.insert(0, lx) + ys.insert(0, ly) + + if rx is None or rx <= last_x: + rx = last_x + kdx_m + ry = ys[-1] + self.pwl_def_as_breaks.postslope * (rx - last_x) + xs.append(rx) + ys.append(ry) + + if plt: + plt.plot(xs, ys, **kwargs) + if self.name: + plt.title('pwl: {0}'.format(self.name)) + plt.show()
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/quad.html b/docs/2.24.232/mp/_modules/docplex/mp/quad.html new file mode 100644 index 0000000..7be1b50 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/quad.html @@ -0,0 +1,908 @@ + + + + + + + + + docplex.mp.quad — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.quad

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+from docplex.mp.constants import UpdateEvent
+from docplex.mp.basic import _SubscriptionMixin
+from docplex.mp.linear import Expr, ZeroExpr
+from docplex.mp.operand import LinearOperand
+from docplex.mp.utils import *
+from docplex.mp.xcounter import update_dict_from_item_value
+from docplex.mp.sttck import StaticTypeChecker
+
+
+def _compare_vars(v1, v2):
+    v1i = v1._index
+    v2i = v2._index
+    return (v1i > v2i) - (v2i > v1i)
+
+
+class VarPair(object):
+    __slots__ = ("first", "second", "_cached_hash")
+
+    def __init__(self, v1, v2=None):
+        if v2 is None:
+            self.first = v1
+            self.second = v1
+        else:
+            if _compare_vars(v1, v2) <= 0:
+                self.first = v1
+                self.second = v2
+            else:
+                self.first = v2
+                self.second = v1
+        self._cached_hash = self._hash_pair()
+
+    def is_square(self):
+        return self.first is self.second
+
+    def __eq__(self, other):
+        # INTERNAL: necessary for use as dict keys
+        # VarPair ensures variables are sorted by indices
+        return isinstance(other, VarPair) and (self.first is other.first) and (self.second is other.second)
+
+    def _hash_pair(self):
+        f = hash(self.first)
+        s = hash(self.second)
+        # cantor encoding. must cast to int() for py3
+        self_hash = int(((f + s) * (s + f + 1) / 2) + s)
+        if self_hash == -1:
+            # value -1 is reserved for errors
+            self_hash = -2  # pragma: no cover
+        return self_hash
+
+    def __hash__(self):
+        return self._cached_hash
+
+    def __repr__(self):
+        return "docplex.mp.quad.VarPair(first={0!s},second={1!s})".format(self.first, self.second)
+
+    def __str__(self):
+        return "VarPair({0!s}, {1!s})".format(self.first, self.second)
+
+    def __getitem__(self, item):
+        if 0 == item:
+            return self.first
+        elif 1 == item:
+            return self.second
+        else:
+            raise StopIteration
+
+    def index_tuple(self):
+        return self.first._index, self.second._index
+
+
+
[docs]class QuadExpr(_SubscriptionMixin, Expr): + """QuadExpr() + + This class models quadratic expressions. + This class is not intended to be instantiated. Quadratic expressions are built + either by using operators or by using :func:`docplex.mp.model.Model.quad_expr`. + + """ + def _new_term_dict(self): + return self._model._lfactory.term_dict_type() + + def copy(self, target_model, var_mapping): + copied_quads = self._quadterms.__class__() + for qv1, qv2, qk in self.iter_quad_triplets(): + new_v1 = var_mapping[qv1] + new_v2 = var_mapping[qv2] + copied_quads[VarPair(new_v1, new_v2)] = qk + + copied_linear = self._linexpr.copy(target_model, var_mapping) + return QuadExpr(model=target_model, + quads=copied_quads, + linexpr=copied_linear, + safe=True) + + def relaxed_copy(self, relaxed_model, var_map): + raise DocplexLinearRelaxationError(self, cause='quadratic') + +
[docs] def is_quad_expr(self): + return True
+ +
[docs] def has_quadratic_term(self): + """ Returns true if there is at least one quadratic term in the expression. + """ + return any(qk for _, qk in self.iter_quads())
+ + def square(self): + if self.has_quadratic_term(): + self.fatal("Cannot take the square of a quadratic term: {0!s}".format(self)) + else: + return self._linexpr.square() + + def _raw_solution_value(self, s=None): + # INTERNAL + quad_value = 0 + for qv0, qv1, qk in self.iter_quad_triplets(): + quad_value += qk * (qv0._raw_solution_value(s) * qv1._raw_solution_value(s)) + lin_value = self._linexpr._raw_solution_value(s) + return quad_value + lin_value + + __slots__ = ('_quadterms', '_linexpr', '_transient', '_subscribers') + + def __init__(self, model, quads=None, linexpr=None, safe=False): + Expr.__init__(self, model) + self._transient = False + self._subscribers = [] # used by subscription mixin + if quads is None: + self._quadterms = self._new_term_dict() + elif isinstance(quads, dict): + if safe: + self._quadterms = quads + else: + # check + safe_quads = self._new_term_dict() + for qvp, qk in quads.items(): + model._typecheck_num(qk) + if not isinstance(qvp, VarPair): + self.fatal("Expecting variable-pair, got: {0!r}", qvp) + else: + safe_quads[qvp] = qk + self._quadterms = safe_quads + + elif isinstance(quads, tuple): + try: + v1, v2, qk = quads + if not safe: + model._typecheck_var(v1) + model._typecheck_var(v2) + model._typecheck_num(qk, 'QuadExpr') + self._quadterms = model._lfactory._new_term_dict() + self._quadterms[VarPair(v1, v2)] = qk + + except ValueError: # pragma: no cover + self.fatal("QuadExpr accepts tuples of len: 3, got: {0!r}", quads) + + elif is_iterable(quads): + qterms = model._lfactory.term_dict_type() + for qv1, qv2, qk in quads: + qterms[VarPair(qv1, qv2)] = qk + self._quadterms = qterms + else: + self.fatal("unexpected argument for QuadExpr: {0!r}", quads) # pragma: no cover + + if quads is not None: + self._model._quad_count += 1 + + if linexpr is None: + self._linexpr = model._lfactory.linear_expr() + else: + self._linexpr = model._lfactory._to_linear_expr(linexpr) + + def clone_if_necessary(self): + # INTERNAL + if self._transient and not self._model._keep_all_exprs and not self.is_in_use(): + return self + else: + return self.clone() + + def keep(self): + self._transient = False + +
[docs] def clone(self): + """ Makes a copy of the quadratic expression and returns it. + + Returns: + A quadratic expression. + """ + cloned_linear = self._linexpr.clone() + new_quad = QuadExpr(self.model, quads=self._quadterms.copy(), + linexpr=cloned_linear, + safe=True) + return new_quad
+ + def is_discrete(self): + for qv0, qv1, qk in self.iter_quad_triplets(): + if not qv0.is_discrete or not qv1.is_discrete() or not is_int(qk): + return False + return self._linexpr.is_discrete() + + def _generate_quad_triplets(self): + # INTERNAL + # a generator that returns triplets (i.e. tuples of len 3) + # with the variable pair and the coefficient + for qvp, qk in self.iter_quads(): + yield qvp[0], qvp[1], qk + + def iter_quads(self): + return iter(self._quadterms.items()) + + def iter_sorted_quads(self): + if self._model.keep_ordering: + return self.iter_quads() + else: + return self._iter_sorted_quads() + + def _iter_sorted_quads(self): + quadterms = self._quadterms + for vp in sorted(quadterms.keys(), key=lambda vvp: vvp.index_tuple()): + yield vp, quadterms[vp] + + def iter_opposite_ordered_quads(self): + # INTERNAL + for qv, qk in self.iter_sorted_quads(): + yield qv, -qk + +
[docs] def iter_quad_triplets(self): + """ Iterates over quadratic terms. + + This iterator returns triplets of the form `v1,v2,k`, where `v1` and `v2` are decision + variables and `k` is a number. + + Returns: + An iterator object. + """ + return self._generate_quad_triplets()
+ +
[docs] def iter_terms(self): + """ Iterates over the linear terms in the quadratic expression. + + Equivalent to self.linear_part.iter_terms() + + Returns: + An iterator over the (variable, coefficient) pairs in the linear part of the expression. + + Example: + Calling this method on (x^2 +2x+1) will return one pair (x, 2). + + See Also: + :func:`docplex.mp.linear.LinearExpr.iter_terms` + """ + return self._linexpr.iter_terms()
+ + @property + def number_of_quadratic_terms(self): + """ This property returns the number of quadratic terms. + + Counts both the square and product terms. + + Examples: + + .. code-block:: python + + q1 = x**2 + q1.number_of_quadratic_terms + >>> 1 + q2 = (x+y+1)**2 + q2.number_of_quadratic_terms + >>> 3 + """ + return len(self._quadterms) + + @property + def size(self): + return self.number_of_quadratic_terms + self._linexpr.size + +
[docs] def is_separable(self): + """ Checks if all quadratic terms are separable. + + Returns: + True if all quadratic terms are separable. + """ + return all(qv.is_square() for qv, _ in self.iter_quads())
+ + def compute_separable_convexity(self, sense=1): + # INTERNAL + # returns 1 if separable, convex + # returns -1 if separable non convex + # return s0 if non separable + + justifier = None + for qv, qk in self.iter_quads(): + if not qv.is_square(): + return 0, None # non separable: fast exit + elif qk * sense < 0: + if justifier is None: + justifier = (qk, qv[0]) # separable, non convex, kept + else: + return justifier or (1, None) # (1, None) is for separable, convex + +
[docs] def get_quadratic_coefficient(self, var1, var2=None): + ''' Returns the coefficient of a quadratic term in the expression. + + Returns the coefficient of the quadratic term `var1*var2` in the expression, if any. + If the product is not present in the expression, returns 0. + + Args: + var1: The first variable of the product (an instance of class Var) + var2: the second variable of the product. If passed None, returns the coefficient + of the square of `var1` in the expression. + + Example: + Assuming `x` and `y` are decision variables and `q` is the expression `2*x**2 + 3*x*y + 5*y**2`, then + + `q.get_quadratic_coefficient(x)` returns 2 + + `q.get_quadratic_coefficient(x, y)` returns 3 + + `q.get_quadratic_coefficient(y)` returns 5 + + Returns: + The coefficient of one quadratic product term in the expression. + ''' + self.model._typecheck_var(var1) + if var2 is None: + var2 = var1 + else: + self.model._typecheck_var(var2) + return self._get_quadratic_coefficient(var1, var2)
+ + def _get_quadratic_coefficient(self, var1, var2): + # INTERNAL, no checks + vp = VarPair(var1, var2 or var1) + return self._get_quadratic_coefficient_from_var_pair(vp) + + def _get_quadratic_coefficient_from_var_pair(self, vp): + # INTERNAL + return self._quadterms.get(vp, 0) + + def set_quadratic_coefficient(self, var1, var2, k): + self.model._typecheck_var(var1) + if var2 is None: + var2 = var1 + else: + self.model._typecheck_var(var2) + self._set_quadratic_coefficient(var1, var2, k) + + def _set_quadratic_coefficient_internal(self, var1, var2, k): + vp = VarPair(var1, var2 or var1) + self_quadterms = self._quadterms + if not k: + if vp in self_quadterms: + del self_quadterms[vp] + return True + else: + return False + else: + self_quadterms[vp] = k + return True + + def _set_quadratic_coefficient(self, var1, var2, k): + if self._set_quadratic_coefficient_internal(var1, var2, k): + self.notify_modified(event=UpdateEvent.QuadExprGlobal) + + def _set_quadratic_coefficients(self, var_coef_seq): + nb_changes = 0 + for (var1, var2), k in var_coef_seq: + if self._set_quadratic_coefficient_internal(var1, var2, k): + nb_changes += 1 + if nb_changes: + self.notify_modified(event=UpdateEvent.QuadExprGlobal) + + # --- + def equals(self, other): + if not isinstance(other, QuadExpr): + return False + if self.number_of_quadratic_terms != other.number_of_quadratic_terms: + return False + + for qvp, qk in self.iter_quads(): + if other._get_quadratic_coefficient_from_var_pair(qvp) != qk: + return False + return self._linexpr.equals(other._linexpr) + + def is_constant(self): + return not self.has_quadratic_term() and self._linexpr.is_constant() + + def get_constant(self): + return self._linexpr.constant + + def set_constant(self, num): + linexpr = self._linexpr + event = None + if num != linexpr.get_constant(): + linexpr._constant = num + event = UpdateEvent.ExprConstant + self.notify_modified(event) + + @property + def constant(self): + """This property is used to get or set the constant part of a quadratic expression + """ + return self.get_constant() + + @constant.setter + def constant(self, new_cst): + self.set_constant(new_cst) + + @property + def linear_part(self): + """ This property returns the linear part of a quadratic expression. + + For example, the linear part of x^2 +2x+1 is (2x+1) + + :return: an instance of :class:`docplex.mp.LinearExpr` + """ + return self.get_linear_part() + + def get_linear_part(self): + linexpr = self._linexpr + return linexpr if linexpr else 0 + + def iter_variables(self): + for qvp, _ in self.iter_quads(): + if qvp.is_square(): + yield qvp[0] + else: + yield qvp[0] + yield qvp[1] + linexpr = self._linexpr + for lv in linexpr.iter_variables(): + yield lv + + def _quad_variable_set(self): + # INTERNAL + setof_qvars = set() + for qvp, _ in self.iter_quads(): + setof_qvars.add(qvp[0]) + if not qvp.is_square(): + setof_qvars.add(qvp[1]) + return setof_qvars + + def iter_quad_variables(self): + # iterates on quadratic variables, + # returns each variable only once + # order is irrelevant. + setof_qvars = self._quad_variable_set() + return iter(setof_qvars) + + iter_quad_vars = iter_quad_variables + + def __contains__(self, dvar): + return self.contains_var(dvar) + +
[docs] def contains_var(self, dvar): + # required by tests... + for qv in self.iter_variables(): + if qv is dvar: + return True + else: + return False
+ + def __iter__(self): + # INTERNAL: this is necessary to prevent expr from being an iterable. + # as it follows getitem protocol, it can mistakenly be interpreted as an iterable + # but this would make sum loop forever. + raise TypeError # pragma: no cover + + def contains_quad(self, qv): + # INTERNAL + return qv in self._quadterms + + def __repr__(self): + return "docplex.mp.quad.QuadExpr(%s)" % self.repr_str() + + def to_stringio(self, oss, nb_digits, use_space, var_namer=lambda v: v.lp_name): + q = 0 + # noinspection PyPep8Naming + SP = u' ' + for qvp, qk in self.iter_sorted_quads(): + if not qk: + continue + qv1 = qvp.first + qv2 = qvp.second + # --- + # sign is printed if non-first OR negative + # at the end of this block coeff is positive + if q > 0 and use_space: + oss.write(SP) + if qk < 0 or q > 0: + oss.write(u'-' if qk < 0 else u'+') + if qk < 0: + qk = -qk + if use_space and q > 0: + oss.write(SP) + + # write coeff if <> 1 + varname1 = var_namer(qv1) + if 1 != qk: + self._num_to_stringio(oss, num=qk, ndigits=nb_digits) + if use_space: + oss.write(SP) + + oss.write(str(varname1)) + if qv1 is qv2: + oss.write(u"^2") + else: + if use_space: + oss.write(SP) + oss.write(u'*') + oss.write(SP) + else: + oss.write(u'*') + oss.write(str(var_namer(qv2))) + q += 1 + # problem for linexpr: force '+' ssi c>0 + linexpr = self._linexpr + lin_constant = linexpr.get_constant() + if linexpr: + first_lk = 0 + for lv, lk in linexpr.iter_terms(): + if lk: + first_lk = lk + break + if q > 0 and first_lk > 0: + if use_space: + oss.write(u' ') + oss.write(u"+") + + if first_lk: + if use_space: + oss.write(SP) + self._linexpr.to_stringio(oss, nb_digits, use_space, var_namer) + + elif lin_constant: + self._num_to_stringio(oss, lin_constant, nb_digits, print_sign=True, force_plus=q > 0, + use_space=use_space) + elif not q: + oss.write(u'0') + + def plus(self, other): + cloned = self.clone_if_necessary() + cloned.add(other) + return cloned + + def minus(self, other): + cloned = self.clone_if_necessary() + cloned.subtract(other) + return cloned + + def rminus(self, other): + # other - self + cloned = self.clone() + cloned.negate() + cloned.add(other) + return cloned + + def times(self, other): + if is_number(other) and 0 == other: + return self.zero_expr() + + elif isinstance(other, ZeroExpr): + return other + + elif self.is_constant(): + k = self.constant + if not k: + return self.zero_expr() + elif 1 == k: + return self._model._lfactory._to_linear_expr(other) + else: + return other * k + else: + cloned = self.clone() + cloned.multiply(other) + return cloned + + def __add__(self, other): + return self.plus(other) + + def __iadd__(self, other): + self.add(other) + return self + + def __radd__(self, other): + return self.plus(other) + + def __sub__(self, other): + return self.minus(other) + + def __rsub__(self, other): + # e - self + return self.rsubtract(other) + + def __isub__(self, other): + self.subtract(other) + return self + + def __mul__(self, other): + return self.times(other) + + def __rmul__(self, other): + return self.times(other) + + def __imul__(self, other): + # self is modified + return self.multiply(other) + + def __div__(self, e): + return self.quotient(e) + + def __idiv__(self, other): + self.divide(other, check=True) + return self + + def __truediv__(self, e): + return self.quotient(e) # pragma: no cover + + def __itruediv__(self, other): + # this is for Python 3.z + return self.divide(other) # pragma: no cover + + def __neg__(self): + cloned = self.clone() + cloned.negate() + return cloned + + def add(self, other): + # increment the QuadExpr with some other argument + if isinstance(other, QuadExpr): + event = self._add_quad(other) + else: + self._linexpr.add(other) + event = UpdateEvent.LinExprGlobal + self.notify_modified(event=event) + + def rsubtract(self, other): + # to compute (other - self) we copy self, negate the copy and add other + # result is always cloned even if other is zero (optimization possible here) + cloned = self.clone() + cloned.negate() + cloned.add(other) + return cloned + + def subtract(self, other): + if isinstance(other, QuadExpr): + self._subtract_quad(other) + event = UpdateEvent.QuadExprGlobal + else: + self._linexpr.subtract(other) + event = UpdateEvent.LinExprGlobal + self.notify_modified(event=event) + return self + + def negate(self): + # INTERNAL: negate sall coefficients, modify self + qterms = self._quadterms + for qvp, qk in qterms.items(): + qterms[qvp] = -qk + self._linexpr.negate() + self.notify_modified(event=UpdateEvent.QuadExprGlobal) + return self + + def multiply(self, other): + event = UpdateEvent.QuadExprGlobal + if is_number(other): + self._scale(other) + + elif self.is_constant(): + this_constant = self._linexpr.get_constant() + if 0 == this_constant: + # do nothing + event = None + else: + self._assign_scaled(other, this_constant) + + elif self.has_quadratic_term(): + if other.is_constant(): + return self.multiply(other.get_constant()) + else: + StaticTypeChecker.mul_quad_lin_error(self.model, self, other) + + else: + # self is actually a linear expression + if is_quad_expr(other): + if other.has_quadratic_term(): + StaticTypeChecker.mul_quad_lin_error(self.model, self, other) + else: + return self.multiply(other._linexpr) + else: + other_linexpr = other.to_linear_expr() + self_linexpr = self._linexpr + for v1, k1 in self_linexpr.iter_terms(): + for v2, k2 in other_linexpr.iter_terms(): + self._add_one_quad_term(VarPair(v1, v2), k1 * k2) + other_cst = other.get_constant() + self_cst = self_linexpr.get_constant() + if other_cst: + self_linexpr._scale(other_cst) + if self_cst: + for ov, ok in other.iter_terms(): + self_linexpr._add_term(ov, ok * self_cst) + self.notify_modified(event) + return self + + def quotient(self, e): + self.model._typecheck_as_denominator(e, self) + cloned = self.clone() + cloned.divide(e, check=False) + return cloned + + def divide(self, other, check=True): + if check: + self.model._typecheck_as_denominator(other, self) # only a nonzero number is allowed... + inverse = 1.0 / other + self._scale(inverse) + self.notify_modified(event=UpdateEvent.QuadExprGlobal) + return self + + def _scale(self, factor): + # INTERNAL: scales a quad expr from a numeric constant. + # no checks done! + # this method modifies self. + if 0 == factor: + self.clear() + elif 1 == factor: + # nothing to do + pass + else: + # scale quads + self_quadterms = self._quadterms + for qv, qk in self.iter_quads(): + self_quadterms[qv] = factor * qk + # scale linear part + if self._linexpr is not None: + self._linexpr._scale(factor) + + def _assign_scaled(self, other, factor): + # INTERNAL + if isinstance(other, LinearOperand): + scaled = self._model._lfactory._to_linear_expr(other, force_clone=True) + scaled *= factor + self._linexpr = scaled + elif isinstance(other, QuadExpr): + for qv, qk in other.iter_quads(): + self._add_one_quad_term(qv, qk * factor) + self._assign_scaled(other.linear_part, factor) + else: + pass + + def _add_one_quad_term(self, qv, qk): + qterms = self._quadterms + if qk or qv in qterms: + qterms[qv] = qterms.get(qv, 0) + qk + + def normalize(self): # pragma: no cover + # INTERNAL + quadterms = self._quadterms + if quadterms: + to_remove = [qvp for qvp, qk in self.iter_quads() if not qk] + for rvp in to_remove: + del quadterms[rvp] + self._linexpr.normalize() + + def clear(self): + self._quadterms.clear() + self._linexpr._clear() + + # quad-specific + def _add_quad(self, other_quad): + # add quad part + for oqv, oqk in other_quad.iter_quads(): + self._add_one_quad_term(oqv, oqk) + # add linear part + if other_quad._linexpr.is_zero(): + return UpdateEvent.QuadExprQuadCoef + else: + self._linexpr.add(other_quad._linexpr) + return UpdateEvent.QuadExprGlobal + + def _subtract_quad(self, other_quad): + # subtract quad + quadterms = self._quadterms + for oqv, oqk in other_quad.iter_quads(): + update_dict_from_item_value(quadterms, oqv, -oqk) + # subtract linear part + self._linexpr.subtract(other_quad._linexpr) + + def to_linear_expr(self, msg=None): # pragma: no cover + # used_msg = msg or "Quadratic expression [{0!s}] cannot be converted to a linear expression" + # self.fatal(used_msg, self) + raise DocplexQuadToLinearException(self) + + def is_normalized(self): # pragma: no cover + # INTERNAL + for _, qk in self.iter_quads(): + if not qk: + return False # pragma: no cover + else: + return True + + # --- relational operators + def __eq__(self, other): + return self._model._qfactory.new_eq_constraint(self, other) + + def __le__(self, other): + return self._model._qfactory.new_le_constraint(self, other) + + def __ge__(self, other): + return self._model._qfactory.new_ge_constraint(self, other) + + def __ne__(self, other): + self.model.fatal("Operator `!=` is not supported for quadratic expressions, {0!s} was passed", self) + + def notify_expr_modified(self, expr, event): + if expr is self._linexpr: + # something to do.. + self.notify_modified(event, )
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/relax_linear.html b/docs/2.24.232/mp/_modules/docplex/mp/relax_linear.html new file mode 100644 index 0000000..7b33fa6 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/relax_linear.html @@ -0,0 +1,280 @@ + + + + + + + + + docplex.mp.relax_linear — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.relax_linear

+# returns a relaxed model from a given model
+
+from docplex.mp.utils import DocplexLinearRelaxationError
+
+from collections import defaultdict
+
+
+
[docs]class LinearRelaxer(object): + """ This class returns a linear relaxation for a MIP model. + + """ + + def __init__(self): + self._unrelaxables = defaultdict(list) + + def iter_unrelaxables(self): + return self._unrelaxables.items() + + def main_cause(self): + unrelaxables = self._unrelaxables + max_urx = -1 + justifier = None + main_cause = None + for cause, urxs in unrelaxables.items(): + if len(urxs) > max_urx: + max_urx = len(urxs) + main_cause = cause + justifier = urxs[0] + return main_cause, justifier + + @staticmethod + def make_relaxed_model(mdl, return_partial=False, **kwargs): + lrx = LinearRelaxer() + return lrx.linear_relaxation(mdl, return_partial=return_partial, **kwargs) + +
[docs] def linear_relaxation(self, mdl, return_partial=False, **kwargs): + """ Returns a continuous relaxation of the model. + + Variable types are set to continuous (note that semi-xxx variables have their LB set to zero) + + Some constructs are not relaxable, for example, piecewise-linear expressions, SOS sets, + logical constraints... + When a model contains at least one of these non-relaxable constructs, a message is printed + and this method returns None. + + By default, model parameters are not copied. If you want to copy them, pass the + keyword argument `copy_parameters=True` + + :param mdl: the initial model + :param return_partial: if True, returns the partially relaxed model anyway, default is False, + if an unrelaxable item is encountered, None is returned + + :return: a new model with continuous relaxation, if possible, else None. + """ + + relax_name = kwargs.get('relaxed_name', None) + verbose = kwargs.get('verbose', True) + copy_parameters = kwargs.get('copy_parameters', False) + # relax sos by default + relax_sos = kwargs.get('relax_sos', True) + + model_name = mdl.name + + def info(msg): + if verbose: + print("* relaxation of model {0}: {1}".format(model_name, msg)) + + mdl_class = mdl.__class__ + unrelaxables = defaultdict(list) + + def process_unrelaxable(urx_, reason): + unrelaxables[reason or 'unknown'].append(urx_) + + relax_model_name = relax_name or "lp_%s" % mdl.name + relaxed_model = mdl_class(name=relax_model_name) + + # transfer kwargs + relaxed_model._parse_kwargs(mdl._get_kwargs()) + + # transfer variable containers + ctn_map = {} + for ctn in mdl.iter_var_containers(): + copied_ctn = ctn.copy_relaxed(relaxed_model) + ctn_map[ctn] = copied_ctn + + # transfer variables + var_mapping = {} + continuous = relaxed_model.continuous_vartype + for v in mdl.iter_variables(): + cpx_code = v.cplex_typecode + if not v.is_generated() or cpx_code == 'C': + # if v has type semixxx, set lB to 0 + if cpx_code in {'N', 'S'}: + rx_lb = 0 + else: + rx_lb = v.lb + copied_var = relaxed_model._var(continuous, rx_lb, v.ub, v.name) + var_ctn = v.container + if var_ctn: + copied_ctn = ctn_map.get(var_ctn) + assert copied_ctn is not None + copied_var.container = copied_ctn + var_mapping[v] = copied_var + + # transfer all non-logical cts + for ct in mdl.iter_constraints(): + if not ct.is_generated(): + if ct.is_logical(): + process_unrelaxable(ct, 'logical') + try: + copied_ct = ct.relaxed_copy(relaxed_model, var_mapping) + relaxed_model.add(copied_ct) + except DocplexLinearRelaxationError as xe: + process_unrelaxable(xe.object, xe.cause) + except KeyError as ke: + info('failed to relax constraint: {0}'.format(ct)) + process_unrelaxable(ct, 'key') + + # clone objective + relaxed_model.objective_sense = mdl.objective_sense + try: + relaxed_model.objective_expr = mdl.objective_expr.relaxed_copy(relaxed_model, var_mapping) + except DocplexLinearRelaxationError as xe: + process_unrelaxable(urx_=xe.object, reason=xe.cause) + except KeyError: + process_unrelaxable(urx_=mdl.objective_expr, reason='objective') + + # clone kpis + for kpi in mdl.iter_kpis(): + relaxed_model.add_kpi(kpi.copy(relaxed_model, var_mapping)) + + if mdl.context: + relaxed_model.context = mdl.context.copy() + + if copy_parameters: + # copy parameters is not the default behavior + # by default, the relaxed copy has a clean, default, parameter set. + # if verbose: + # info("copying initial model parameters to relaxed model") + nb_copied = 0 + for p1, p2 in zip(mdl.parameters.generate_params(), relaxed_model.parameters.generate_params()): + if p1.is_nondefault(): + p2.set(p1.get()) + nb_copied += 1 + if verbose: + info("copied {0} initial model parameters to relaxed model".format(nb_copied)) + + # + if relax_sos: + for sos in mdl.iter_sos(): + # list of mapped variables for original sos + sos_vars = [var_mapping[dv1] for dv1 in sos.iter_variables()] + sos_type = sos.sos_type + sos_ctname = f"relaxed_sos{sos_type.value}#{sos.index+1}" + relaxed_model.add(relaxed_model.sum_vars(sos_vars) <= sos_type.value, sos_ctname) + else: + for sos in mdl.iter_sos(): + unrelaxables['sos'].append(sos) + + self._unrelaxables = unrelaxables + if unrelaxables: + nb_unrelaxables = len(unrelaxables) + main_cause, justifier = self.main_cause() + + if verbose and not return_partial: + print("* model {0}: found {1} un-relaxable elements, main cause is {2} (e.g. {3})" + .format(mdl.name, nb_unrelaxables, main_cause, justifier)) + if verbose: + for cause, urxs in unrelaxables.items(): + print('* reason: {0}: {1} unrelaxables'.format(cause, len(urxs))) + for u, urx in enumerate(urxs): + if hasattr(urx, "is_generated") and urx.is_generated(): + s_gen = " [generated]" + else: + s_gen = "" + print('-- {0}: cannot be relaxed: {1!s}{2}'.format(u + 1, urx, s_gen)) + if return_partial: + if verbose: + print(f"-- returning partially relaxed model") + return relaxed_model + else: + return None + else: + # force cplex if any... + cpx = relaxed_model.get_cplex(do_raise=False) + if cpx: + # force type to LP + cpx.set_problem_type(0) # 0 is code for LP. + # sanity check... + assert not relaxed_model._contains_discrete_artefacts() + assert not relaxed_model._solved_as_mip() + # --- + return relaxed_model
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/relaxer.html b/docs/2.24.232/mp/_modules/docplex/mp/relaxer.html new file mode 100644 index 0000000..4a54dcc --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/relaxer.html @@ -0,0 +1,758 @@ + + + + + + + + + docplex.mp.relaxer — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.relaxer

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+
+from collections import defaultdict, namedtuple
+from docplex.mp.constants import RelaxationMode
+
+from docplex.mp.utils import is_function, apply_thread_limitations, _to_list
+from docplex.mp.priority import Priority
+from docplex.mp.constr import AbstractConstraint
+from docplex.mp.error_handler import docplex_fatal
+from docplex.mp.sdetails import SolveDetails
+
+from docplex.util import as_df
+from docplex.mp.publish import PublishResultAsDf
+
+try:
+    import pandas as pd
+except ImportError:  # pragma: no cover
+    pd = None
+
+TOutputTables = namedtuple('TOutputTables', ['Constraint', 'Priority', 'Amount'])
+
+
+def to_output_table(relaxer, use_pd=True):
+    if pd and use_pd:
+        return as_df(relaxer)
+    else:
+        result = []
+        prioritizer = relaxer.prioritizer
+        for ct, relaxed in relaxer.iter_relaxations():
+            result.append(TOutputTables(ct.name,
+                                        prioritizer.get_priority_internal(ct).name,
+                                        relaxed))
+        return result
+
+
+# noinspection PyAbstractClass
+
[docs]class Prioritizer(object): + ''' Abstract base class for prioritizers. + This class acts like a functor to be called on each model constraint. + ''' + + def __init__(self, override=False): + self._override = override + + def get_priority(self, ct): + raise NotImplementedError("base class") # pragma: no cover + + def get_priority_internal(self, ct): + ct_prio = ct.priority + if not self._override and ct_prio: + return ct_prio + else: + return self.get_priority(ct)
+ + +class IRelaxationListener(object): + # INTERNAL + # ''' Base class for relaxation listeners.''' + + def notify_start_relaxation(self, priority, relaxables): + ''' This method is called at each step of the relaxation loop.''' + pass # pragma: no cover + + def notify_failed_relaxation(self, priority, relaxables): + ''' This method is called when a relaxation attempt fails.''' + pass # pragma: no cover + + def notify_successful_relaxation(self, priority, relaxables, relaxed_obj_value, violations): + ''' This method is called when a relaxation succeeds.''' + pass # pragma: no cover + + +class VerboseRelaxationListener(IRelaxationListener): + # INTERNAL + # ''' A default implementation of the listener, which prints messages.''' + + def __init__(self): + IRelaxationListener.__init__(self) + self.relaxation_count = 0 + + def notify_start_relaxation(self, priority, relaxables): + self.relaxation_count += 1 + print("-> relaxation #{0} starts with priority: {1!s}, #relaxables={2:d}" + .format(self.relaxation_count, priority.name, len(relaxables))) + + def notify_failed_relaxation(self, priority, relaxables): + print("<- relaxation #{0} fails, priority: {1!s}, #relaxables={2:d}" + .format(self.relaxation_count, priority.name, len(relaxables))) + + def notify_successful_relaxation(self, priority, relaxables, obj, violations): + print("<- relaxation #{0} succeeds: priority: {1!s}, #relaxables={2:d}, obj={3}, #relaxations={4}". + format(self.relaxation_count, priority.name, len(relaxables), obj, len(violations))) + + +
[docs]class NamedPrioritizer(Prioritizer): + # INTERNAL + # """ Basic prioritizer that relaxes any constraint with a name. + # + # More precisely, the prioritizer logic works as follows: + # + # - If the constraint has a ``priority`` attribute, then it is assumed + # to hold a priority instance and use it. + # - If the constraint has a user-defined name, relax it with MEDIUM priority. + # - Otherwise, the constraint is not to be relaxed (that is, it is assigned MANDATORY priority). + # + # """ + + def __init__(self, priority=Priority.MEDIUM, override=False): + Prioritizer.__init__(self, override) + self._priority = priority + + def get_priority(self, ct): + return self._priority if ct.has_user_name() else Priority.MANDATORY
+ + +
[docs]class UniformPrioritizer(Prioritizer): + # INTERNAL + # """ Constraint prioritizer that relaxes all constraints. + # + # This prioritizer assigns MEDIUM priority to all constraints + # unless an explicit priority has been set by the user. + # + # """ + + def __init__(self, priority=Priority.MEDIUM, override=False): + Prioritizer.__init__(self, override) + self._priority = priority + + def get_priority(self, ct): + return self._priority
+ + +
[docs]class MatchNamePrioritizer(Prioritizer): + # INTERNAL + # """ Constraint prioritizer based on constraint names. + # + # This prioritizer analyzes constraint names for strings that match priority names. + # If a constraint contains a string which matches a priority name, + # then it is assigned this priority. + # + # If a constraint has a priority explicitly set by the ``priority`` attribute, + # this user priority is returned. + # + # Note: + # 1. Unnamed constraints are considered as non-matches. + # 2. String matching is not case sensitive. + # + # For example: a constraint named "ct_salary_low" will be considered as having the priority LOW. + # """ + + def __init__(self, + priority_for_unnamed=Priority.MANDATORY, + priority_for_non_matches=Priority.MANDATORY, + case_sensitive=False, + override=False): + Prioritizer.__init__(self, override) + assert isinstance(priority_for_unnamed, Priority) + assert isinstance(priority_for_non_matches, Priority) + + self.priority_for_unnamed_cts = priority_for_unnamed + self.priority_for_non_matching_cts = priority_for_non_matches + self.priority_by_symbol = {prio.name.lower(): prio for prio in Priority} + self._is_case_sensitive = bool(case_sensitive) + +
[docs] def get_priority(self, ct): + ''' Looks for known priority names inside constraint names. + ''' + + if not ct.has_user_name(): + return self.priority_for_unnamed_cts + else: + ctname = ct.name + ctname_to_match = ctname if self._is_case_sensitive else ctname.lower() + best_matched = 0 + best_matching_priority = self.priority_for_non_matching_cts + for (prio_symbol, prio) in self.priority_by_symbol.items(): + if ctname_to_match.find(prio_symbol) >= 0: + matched = len(prio_symbol) + # longer matches are preferred + # e.g. very_low and low both match in very_low_ctxxx + # but the prioritizer will return very_low as the match is longer. + if matched > best_matched: + best_matched = matched + best_matching_priority = prio + return best_matching_priority
+ + +
[docs]class MappingPrioritizer(Prioritizer): + # INTERNAL + # """ + # Constraint prioritizer based on a dictionary of constraints and priorities. + # + # Initialized from a dictionary and an optional default priority. + # + # Args: + # priority_mapping: A dictionary with constraints as keys and priorities as values. + # + # default_priority: An optional priority, used when a constraint is not explicitly mentioned + # in the mapping. The default value is MANDATORY, meaning that any constraint not mentioned + # in the mapping will not be relaxed. + # """ + + def __init__(self, priority_mapping, default_priority=Priority.MANDATORY, override=False): + Prioritizer.__init__(self, override) + # --- typecheck that this dict is a a {ct: prio} mapping. + if not isinstance(priority_mapping, dict): + raise TypeError + for k, v in priority_mapping.items(): + if not isinstance(k, AbstractConstraint): + raise TypeError + + if not hasattr(v, 'cplex_preference'): + raise TypeError + # --- + self._mapping = priority_mapping + self._default_priority = default_priority + + def get_priority(self, ct): + # return the dict's value for ct if nay, else its own priority or the default. + return self._mapping.get(ct, ct.priority or self._default_priority)
+ + +
[docs]class FunctionalPrioritizer(Prioritizer): + def __init__(self, fn, override=False): + Prioritizer.__init__(self, override) + self._prioritize_fn = fn + + def get_priority(self, ct): + return self._prioritize_fn(ct)
+ + +# internal named tuples +_TRelaxableGroup = namedtuple("_TRelaxableGroup", ["preference", "relaxables"]) +_TParamData = namedtuple('_TParamInfo', ['short_name', 'default_value', 'accessor']) + + +
[docs]class Relaxer(PublishResultAsDf, object): + ''' This class is an abstract algorithm, in the sense that it operates on interfaces. + + It takes a prioritizer, which an implementation of ``ConstraintPrioritizer``. + For convenience, predefined prioritizer types are accessible through names: + + - `all` relaxes all constraints using a MEDIUM priority; this is the default. + - `named` relaxes all constraints with a user name but not the others. + - `match` looks for priority names within constraint names; + unnamed constraints are not relaxed. + + + Note: + All predefined prioritizers apply various forms of logic, but, when a constraint has been assigned + a priority by the user, this priority is always used. For example, the `named` prioritizer relaxes + all named constraints with MEDIUM, but if an unnamed constraint was assigned a HIGH priority, + then HIGH will be used. + + See Also: + :class:`docplex.mp.priority.Priority` + + ''' + default_precision = 1e-5 + + _default_mode = RelaxationMode.OptSum + + def __init__(self, prioritizer='all', verbose=False, precision=default_precision, + override=False, **kwargs): + self.output_table_customizer = kwargs.get('output_processing') # wml tables, internal + self.output_table_property_name = 'relaxations_output' + self.default_output_table_name = 'relaxations.csv' + self.output_table_using_df = True # if pandas is available of course + + self._precision = precision + # --- + if hasattr(prioritizer, 'get_priority'): + self._prioritizer = prioritizer + elif prioritizer == 'match': + self._prioritizer = MatchNamePrioritizer(override=override) + elif isinstance(prioritizer, dict): + self._prioritizer = MappingPrioritizer(priority_mapping=prioritizer, override=override) + elif prioritizer == 'named': + self._prioritizer = NamedPrioritizer() + elif prioritizer is None or prioritizer == 'all': + self._prioritizer = UniformPrioritizer(override=override) + elif is_function(prioritizer): + self._prioritizer = FunctionalPrioritizer(prioritizer, override=override) + else: + print("Cannot deduce a prioritizer from: {0!r} - expecting \"name\"|\"default\"| dict", prioritizer) + raise TypeError + + self._cumulative = True + self._listeners = [] + + # result data + self._last_relaxation_status = False + self._last_relaxation_objective = -1e+75 + self._last_successful_relaxed_priority = Priority.MANDATORY + self._last_relaxation_details = SolveDetails.make_dummy() + self._relaxations = {} + self._verbose = verbose + self._verbose_listener = VerboseRelaxationListener() + + if self._verbose: + self.add_listener(self._verbose_listener) + + @property + def prioritizer(self): + return self._prioritizer + + def set_verbose(self, is_verbose): + if is_verbose != self._verbose: + self._verbose = is_verbose + self.set_verbose_listener_from_flag(is_verbose) + + def set_verbose_listener_from_flag(self, is_verbose): + if is_verbose: + self.add_listener(self._verbose_listener) + else: + self.remove_listener(self._verbose_listener) + + def get_verbose(self): + return self._verbose + + verbose = property(get_verbose, set_verbose) + + def _check_successful_relaxation(self): + if not self._last_relaxation_status: + docplex_fatal("No relaxed solution is present") + + def _reset(self): + # INTERNAL + self._last_relaxation_status = False + self._last_relaxation_objective = -1e+75 + self._last_successful_relaxed_priority = Priority.MANDATORY + self._relaxations = {} + + def _accept_violation(self, violation): + ''' The filter method which accepts or rejects a violation.''' + return 0 == self._precision or abs(violation) >= self._precision + + def add_listener(self, listener): + # INTERNAL + # """ Adds a relaxation listener. + # + # Args: + # listener: The new listener to add. If ``listener`` is not an + # instance of ``IRelaxationListener``, it is ignored. + # + # See Also: + # :class:`IRelaxationListener` + # """ + if isinstance(listener, IRelaxationListener): + self._listeners.append(listener) + + def remove_listener(self, listener): + # INTERNAL + # """ Removes a relaxation listener. + # + # Args: + # listener: The listener to remove. + # """ + if listener in self._listeners: + self._listeners.remove(listener) + + def clear_listeners(self): + # INTERNAL + # """ Removes all relaxation listeners. + # """ + self._listeners = [] + + _param_data = {} + +
[docs] def relax(self, mdl, relax_mode=None, **kwargs): + """ Runs the relaxation loop. + + Args: + mdl: The model to be relaxed. + relax_mode: the relaxation mode. Accept either None (in which case the default mode is + used, or an instance of ``RelaxationMode`` enumerated type, or a string + that can be translated to a relaxation mode. + kwargs: Accepts named arguments similar to ``solve``. + + Returns: + If the relaxation succeeds, the method returns a solution object, an instance of ``SolveSolution``; otherwise returns None. + + See Also: + :func:`docplex.mp.model.Model.solve`, + :class:`docplex.mp.solution.SolveSolution`, + :class:`docplex.mp.constants.RelaxationMode` + + """ + self._reset() + + # 1. build a dir {priority : cts} + priority_map = defaultdict(list) + nb_prioritized_cts = 0 + mdl_priorities = set() + mandatory_justifier = None + nb_mandatories = 0 + for ct in mdl.iter_constraints(): + prio = self._prioritizer.get_priority_internal(ct) + if prio.is_mandatory(): + nb_mandatories += 1 + if mandatory_justifier is None: + mandatory_justifier = ct + else: + priority_map[prio].append(ct) + nb_prioritized_cts += 1 + mdl_priorities.add(prio) + + sorted_priorities = sorted(list(mdl_priorities), key=lambda p: p.value) + + if 0 == nb_prioritized_cts: + mdl.error("Relaxation algorithm found no relaxable constraints - exiting") + return None + if nb_mandatories: + assert mandatory_justifier is not None + s_justifier = mandatory_justifier.to_readable_string() + mdl.warning('{0} constraint(s) will not be relaxed (e.g.: {1})', nb_mandatories, s_justifier) + + temp_relax_verbose = kwargs.pop('verbose', False) + if temp_relax_verbose != self._verbose: + # install/deinstall listener for this relaxation only + self.set_verbose_listener_from_flag(temp_relax_verbose) + + # relaxation loop + all_groups = [] + all_relaxable_cts = [] + is_cumulative = self._cumulative + # -- relaxation mode + if relax_mode is None: + used_relax_mode = self._default_mode + else: + used_relax_mode = RelaxationMode.parse(relax_mode) + if not mdl.is_optimized(): + used_relax_mode = RelaxationMode.get_no_optimization_mode(used_relax_mode) + + # save this for restore later + saved_context_log_output = mdl.context.solver.log_output + saved_log_output_stream = mdl.log_output + saved_context = mdl.context + + # take into account local argument overrides + relax_context = mdl.prepare_actual_context(**kwargs) + + transient_engine = False + relax_engine = mdl.get_engine() + + if temp_relax_verbose: + print("-- starting relaxation. mode: {0!s}, precision={1}".format(used_relax_mode.name, self._precision)) + + try: + # mdl.context has been saved in saved_context above + mdl.context = relax_context + mdl.set_log_output(mdl.context.solver.log_output) + + # engine parameters, if needed to + parameters = apply_thread_limitations(relax_context) + + mdl._apply_parameters_to_engine(parameters) + + relaxed_sol = None + for prio in sorted_priorities: + if prio in priority_map: + cts = priority_map[prio] + if not cts: + # this should not happen... + continue # pragma: no cover + + pref = prio.cplex_preference + # build a new group + relax_group = _TRelaxableGroup(pref, cts) + + # relaxing new batch of cts: + if not is_cumulative: # pragma: no cover + # if not cumulative reset the groupset + all_groups = [relax_group] + all_relaxable_cts = cts + else: + all_groups.append(relax_group) + all_relaxable_cts += cts + + # at this stage we have a sequence of groups + # a group is itself a sequence of two components + # - a preference factor + # - a sequence of constraints + for ls in self._listeners: + ls.notify_start_relaxation(prio, all_relaxable_cts) + + # ---- + # call the engine. + # --- + + try: + relaxed_sol = relax_engine.solve_relaxed(mdl, prio.name, all_groups, used_relax_mode) + finally: + self._last_relaxation_details = relax_engine.get_solve_details() + # --- + + if relaxed_sol is not None: + relax_obj = relaxed_sol.objective_value + self._last_successful_relaxed_priority = prio + self._last_relaxation_status = True + self._last_relaxation_objective = relaxed_sol.objective_value + + # filter irrelevant relaxations below some threshold + for ct in all_relaxable_cts: + raw_infeas = relaxed_sol.get_infeasibility(ct) + if self._accept_violation(raw_infeas): + self._relaxations[ct] = raw_infeas + if not self._relaxations: + mdl.warning( + "Relaxation of model `{0}` found one relaxed solution, but no relaxed constraints - check".format( + mdl.name)) + + for ls in self._listeners: + ls.notify_successful_relaxation(prio, all_relaxable_cts, relax_obj, self._relaxations) + # now get out + break + else: + # TODO: maybe issue a warning that relaxation has failed? + # relaxation has failed, notify the listeners + for ls in self._listeners: + ls.notify_failed_relaxation(prio, all_relaxable_cts) + + mdl.notify_solve_relaxed(relaxed_sol, relax_engine.get_solve_details()) + + # write relaxation table.write_output_table() handles everything related to + # whether the table should be published etc... + if self.is_publishing_output_table(mdl.context): + output_table = to_output_table(self, self.output_table_using_df) + self.write_output_table(output_table, mdl.context) + finally: + # --- restore context, log_output if set. + if saved_log_output_stream != mdl.log_output: + mdl.set_log_output_as_stream(saved_log_output_stream) + if saved_context_log_output != mdl.context.solver.log_output: + mdl.context.solver.log_output = saved_context_log_output + mdl.context = saved_context + if transient_engine: # pragma: no cover + del relax_engine + if temp_relax_verbose != self._verbose: + # realign listener with flag + self.set_verbose_listener_from_flag(self._verbose) + + return relaxed_sol
+ +
[docs] def iter_relaxations(self): + """ Iterates on relaxations. + + Relaxations are built as a dictionary with constraints as keys and numeric violations as values, + so this iterator returns ``(ct, violation)`` pairs. + """ + self._check_successful_relaxation() + return iter(self._relaxations.items())
+ +
[docs] def relaxations(self): + """ Returns a dictionary with all relaxed constraints. + + Returns: + A dictionary where the keys are the relaxed constraints, + and the values are the numerical slacks. + + """ + return self._relaxations.copy()
+ + def __as_df__(self): + ''' Returns a pandas.DataFrame with all relaxed constraint. + + Returns: + A pandas.DataFrame which columns are: + + - Constraint: the constraint name + - Priority: The priority of the constraint + - Amount: The amount of relaxation + ''' + if not pd: # pragma: no cover + raise NotImplementedError('Cannot convert results to DataFrame, pandas is not available') + columns = ['Constraint', 'Priority', 'Amount'] + results_list = [] + prioritizer = self.prioritizer + for ct, relaxed in self.iter_relaxations(): + results_list.append({'Constraint': ct.name, + 'Priority': prioritizer.get_priority_internal(ct).name, + 'Amount': relaxed}) + df = pd.DataFrame(results_list, columns=columns) + return df + + def get_total_relaxation(self): + self._check_successful_relaxation() + return sum(abs(v) for v in self._relaxations.values()) + + @property + def total_relaxation(self): + return self.get_total_relaxation() + + def print_information(self): + self._check_successful_relaxation() + print("* number of relaxations: {0}".format(len(self._relaxations))) + for rct, relaxation in self.iter_relaxations(): + arg = rct.name if rct.has_user_name() else str(rct) + print(" - relaxed: {0}, with relaxation: {1}".format(arg, relaxation)) + print("* total absolute relaxation: {0}".format(self.get_total_relaxation())) + + def as_dict(self): + rxd = {rct.name or str(rct): relaxed for rct, relaxed in self.iter_relaxations()} + return rxd + + @property + def relaxed_objective_value(self): + """ Returns the objective value of the relaxed solution. + + Raises: + DOCplexException + If the relaxation has not been successful. + """ + self._check_successful_relaxation() + return self._last_relaxation_objective + + @property + def number_of_relaxations(self): + """ This property returns the number of relaxations found. + """ + return len(self._relaxations) + +
[docs] def get_relaxation(self, ct): + """ Returns the infeasibility computed for this constraint. + + Args: + ct: A constraint. + + Returns: + The amount by which the constraint has been relaxed by the relaxer. + The method returns 0 if the constraint has not been relaxed. + """ + self._check_successful_relaxation() + return self._relaxations.get(ct, 0)
+ +
[docs] def is_relaxed(self, ct): + ''' Returns true if the constraint ``ct`` has been relaxed + + Args: + ct: The constraint to check. + + Returns: + True if the constraint has been relaxed, else False. + ''' + self._check_successful_relaxation() + return ct in self._relaxations
+ + @classmethod + def run_feasopt(cls, model, relaxables, relax_mode): + relaxable_list = _to_list(relaxables) + groups = [] + checker = model._checker + try: + for pref, ctseq in relaxable_list: + checker.typecheck_num(pref) + cts = _to_list(ctseq) + for ct in cts: + checker.typecheck_constraint(ct) + groups.append((pref, cts)) + except ValueError: + model.fatal("expecting container with (preference, constraints), got: {0!s}", relaxable_list) + + feasible = model.get_engine().solve_relaxed(mdl=model, relaxable_groups=groups, + prio_name='feasopt', + relax_mode=relax_mode) + return feasible
+ +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/sdetails.html b/docs/2.24.232/mp/_modules/docplex/mp/sdetails.html new file mode 100644 index 0000000..645c30a --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/sdetails.html @@ -0,0 +1,501 @@ + + + + + + + + + docplex.mp.sdetails — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.sdetails

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+from io import StringIO
+from docplex.mp.utils import is_almost_equal
+from docplex.mp.constants import int_probtype_to_string
+from math import isnan
+
+
+
[docs]class SolveDetails(object): + """ + The :class:`SolveDetails` class contains the details of a solve. + + This class should never be instantiated. You get an instance of this class from + the model by calling the property :data:`docplex.mp.model.Model.solve_details`. + """ + + _unknown_label = "*unknown*" + + NOT_A_NUM = float('nan') + + _NO_GAP = NOT_A_NUM # value used when no gap is available + _NO_BEST_BOUND = NOT_A_NUM + + def __init__(self, time=0, dettime=0, + status_code=-1, status_string=None, + problem_type=None, + ncolumns=0, nonzeros=0, + miprelgap=None, + best_bound=None, + n_iterations=0, + n_nodes_processed=0): + self._time = max(time, 0) + self._dettime = max(dettime, 0) + self._solve_status_code = status_code + self._solve_status = status_string or self._unknown_label + self._problem_type = problem_type or self._unknown_label # string + self._ncolumns = ncolumns + self._linear_nonzeros = nonzeros + # -- + self._miprelgap = self._NO_GAP if miprelgap is None else miprelgap + self._best_bound = self._NO_BEST_BOUND if best_bound is None else best_bound + # -- progress + self._n_iterations = n_iterations + self._n_nodes_processed = n_nodes_processed + + self._quality_metrics = {} + + def equals(self, other, relative=1e-3, absolute=1e-5, compare_times=True): + if not isinstance(other, SolveDetails): + return False + + if compare_times and not is_almost_equal(self.time, other.time, relative, absolute): + return False + elif self.status_code != other.status_code: + return False + elif self.problem_type != other.problem_type: + return False + elif self.columns != other.columns: + return False + elif self.nb_linear_nonzeros != other.nb_linear_nonzeros: + return False + elif not is_almost_equal(self.mip_relative_gap, other.mip_relative_gap, relative, absolute): + return False + + else: + return True + + def as_worker_dict(self): + # INTERNAL + # Converts the solve details to a dictionary for python worker... + # using "legacy' keys from drop-solve + worker_dict = {"MODEL_DETAIL_TYPE": self._problem_type, + "MODEL_DETAIL_NONZEROS": self._linear_nonzeros + } + if not isnan(self._miprelgap): + worker_dict["PROGRESS_GAP"] = self._miprelgap + if not isnan(self._best_bound): + worker_dict['PROGRESS_BEST_OBJECTIVE'] = self._best_bound + return worker_dict + + @staticmethod + def to_plain_str(arg_s): + return arg_s # in py3 do nothing. + + # --- + # list of fields to be retrieved from the details + # as tuples: (<detail_attribute_name>, <json_attribute_name>, <type_conversion_fn>, <default_value>) + # example: + # _time denotes solve time, a float value, default is 0, to be found in json["cplex.time"] + # --- + _json_fields = (("_time", "cplex.time", float, 0), + ("_solve_status_code", "cplex.status", int, -1), + ("_solve_status", "cplex.statusstring", lambda s: SolveDetails.to_plain_str(s), ""), + ("_problem_type", "cplex.problemtype", lambda p: int_probtype_to_string(p), ""), + ("_ncolumns", "cplex.columns", int, 0), + ("_linear_nonzeros", "MODEL_DETAIL_NON_ZEROS", int, 0), + ("_miprelgap", "cplex.miprelgap", float, _NO_GAP), + ('_best_bound', 'PROGRESS_BEST_OBJECTIVE', float, _NO_BEST_BOUND), + ("_md5", "cplex.model.md5", str, ""), + ('_n_iterations', 'cplex.itcount', int, 0), + ('_n_nodes_processed', 'cplex.nodes.processed', int, 0) + ) + + @staticmethod + def from_json(json_details, all_json_fields=_json_fields): + if not json_details: + return SolveDetails.make_dummy() + + # for k,v in json_details.items(): + # print("{0}: {1!s}".format(k, v)) + # print("# -------------------------") + details = SolveDetails() + for attr_name, field_name, field_conv_fn, field_default in all_json_fields: + field_val = json_details.get(field_name, field_default) + if field_conv_fn is not None: + field_val = field_conv_fn(field_val) # conversion + setattr(details, attr_name, field_val) + + return details + + @staticmethod + def make_dummy(): + dummy_details = SolveDetails(status_string="dummy") + return dummy_details + + @staticmethod + def make_fake_details(time, feasible): + if feasible: + status_code = 1 + status = "OPTIMAL" + else: + status_code = 3 + status = "infeasible" + details = SolveDetails(time=time, + status_code=status_code, status_string=status, + problem_type=None) + return details + + @property + def time(self): + """ This property returns the solve time in seconds. + + """ + return self._time + + @property + def status_code(self): + """ + This property returns the CPLEX status code as a number. + Possible values for the status code are described in the CPLEX documentation + at: + https://www.ibm.com/support/knowledgecenter/SSSA5P_20.1.0/ilog.odms.cplex.help/refcallablelibrary/macros/Solution_status_codes.html + + :return: an integer number (a CPLEX status code) + + """ + return self._solve_status_code + + # status string + # CPX_STAT_ABORT_DETTIME_LIM = 25 + # CPX_STAT_ABORT_DUAL_OBJ_LIM = 22 + # CPX_STAT_ABORT_IT_LIM = 10 + # CPX_STAT_ABORT_OBJ_LIM = 12 + # CPX_STAT_ABORT_PRIM_OBJ_LIM = 21 + # CPX_STAT_ABORT_TIME_LIM = 11 + # CPX_STAT_ABORT_USER = 13 + # CPX_STAT_CONFLICT_ABORT_CONTRADICTION = 32 + # CPX_STAT_CONFLICT_ABORT_DETTIME_LIM = 39 + # CPX_STAT_CONFLICT_ABORT_IT_LIM = 34 + # CPX_STAT_CONFLICT_ABORT_MEM_LIM = 37 + # CPX_STAT_CONFLICT_ABORT_NODE_LIM = 35 + # CPX_STAT_CONFLICT_ABORT_OBJ_LIM = 36 + # CPX_STAT_CONFLICT_ABORT_TIME_LIM = 33 + # CPX_STAT_CONFLICT_ABORT_USER = 38 + # CPX_STAT_CONFLICT_FEASIBLE = 30 + # CPX_STAT_CONFLICT_MINIMAL = 31 + # CPX_STAT_FEASIBLE = 23 + # CPX_STAT_FEASIBLE_RELAXED_INF = 16 + # CPX_STAT_FEASIBLE_RELAXED_QUAD = 18 + # CPX_STAT_FEASIBLE_RELAXED_SUM = 14 + # CPX_STAT_FIRSTORDER = 24 + # CPX_STAT_INFEASIBLE = 3 + # CPX_STAT_INForUNBD = 4 + # CPX_STAT_NUM_BEST = 6 + # CPX_STAT_OPTIMAL = 1 + # CPX_STAT_OPTIMAL_FACE_UNBOUNDED = 20 + # CPX_STAT_OPTIMAL_INFEAS = 5 + # CPX_STAT_OPTIMAL_RELAXED_INF = 17 + # CPX_STAT_OPTIMAL_RELAXED_QUAD = 19 + # CPX_STAT_OPTIMAL_RELAXED_SUM = 15 + # CPX_STAT_UNBOUNDED = 2 + + @property + def status(self): + """ This property returns the solve status as a string. + + This string is normally the value returned by the CPLEX callable library method + CPXXgetstatstring, + see https://www.ibm.com/support/knowledgecenter/SSSA5P_20.1.0/ilog.odms.cplex.help/refcallablelibrary/cpxapi/getstatstring.html + + Example: + * Returns "optimal" when the solution has been proven optimal. + * Returns "feasible" for a feasible, but not optimal, solution. + * Returns "MIP_time_limit_feasible" for a MIP solution obtained before a time limit. + + Note: + In certain cases, status may return a string that is not directly passed from CPLEX + + * If an exception occurs during the CPLEX solve phase, status contains the text of the exception. + * If solve fails because of a promotional version limitation, the following message is returned + "Promotional version. Problem size limits exceeded., CPLEX code=1016." + The corresponding status code is 1016. + + """ + return self._solve_status + + @property + def problem_type(self): + """ This property returns the problem type as a string. + + """ + return self._problem_type + + @property + def columns(self): + """ This property returns the number of columns. + """ + return self._ncolumns + + @property + def nb_linear_nonzeros(self): + """ This property returns the number of linear non-zeros in the matrix solved. + + """ + return self._linear_nonzeros + + @property + def mip_relative_gap(self): + """ This property returns the MIP relative gap. + + Note: + * This property returns NaN when the problem is not a MIP. + * This property returns 1e+20 for multi-objective MIP problems. + * The gap is returned as a floating-point value, not as a percentage. + """ + return self._miprelgap + + gap = mip_relative_gap + + @property + def best_bound(self): + """ This property returns the MIP best bound at the end of the solve. + + Note: + * This property returns NaN when the problem is not a MIP. + * This property returns 1e+20 for multi-objective MIP problems. + """ + return self._best_bound + + @property + def nb_iterations(self): + """ This property returns the number of iterations at the end of the solve. + + Note: + - The nature of the iterations depend on the algorithm usd to solve the model. + - For multi-objective models, this property returns a tuple of numbers, + one for each objective solved. + + """ + return self._n_iterations + + @property + def nb_nodes_processed(self): + """ This property returns the number of nodes processed at the end of solve. + + Note: for multi-objective problems, this property returns a tuple of numbers, + one for each objective solved. + + """ + return self._n_nodes_processed + + @property + def dettime(self): + """ This property returns the solve deterministic time in ticks. + + """ + return self._dettime + + deterministic_time = dettime + + def __repr__(self): + return "docplex.mp.SolveDetails(time={0:g},status={1!r})" \ + .format(self._time, self._solve_status) + + def print_information(self): + print("status = {0}".format(self._solve_status)) + print("time = {0:g} s.".format(self._time)) + print("problem = {0}".format(self._problem_type)) + print("columns = {0:d}".format(self._ncolumns)) + print("iterations={0:d}".format(self._n_iterations)) + # print("nonzeros= {0}".format(self._linear_nonzeros)) + if self._miprelgap >= 0: + print("gap = {0:g}%".format(100 * self._miprelgap)) + + def to_string(self): + oss = StringIO() + oss.write("status = {0}\n".format(self._solve_status)) + oss.write("time = {0:g} s.\n".format(self._time)) + oss.write("problem = {0}\n".format(self._problem_type)) + if self._miprelgap >= 0: + oss.write("gap = {0:g}%\n".format(100.0 * self._miprelgap)) + return oss.getvalue() + + def __str__(self): + return self.to_string() + + _limit_statuses = frozenset({10, 11, 12, 21, 22, 25}.union([i for i in range(33,38)]).union([39]).union([i for i in range(104,109)]).union({111, 112, 128, 131, 132, 306})) + +
[docs] def has_hit_limit(self): + """ + Checks if the solve details indicate that the solve has hit a limit. + + Returns: + Boolean: True if the solve details indicate that the solve has hit a limit. + + """ + return self._solve_status_code in self._limit_statuses
+ + def notify_hit_limit(self, logger): + if self.has_hit_limit(): + logger.info("solve: {0}".format(self.status)) + + @property + def quality_metrics(self): + return self._quality_metrics
+ +# cplex.miprelgap: 0 +# cplex.quality.int.CPX_MAX_QCSLACK: -1 +# MODEL_DETAIL_CONSTRAINTS: 78 +# cplex.quality.int.CPX_MAX_SCALED_PRIMAL_RESIDUAL: 66 +# cplex.quality.double.CPX_SUM_QCSLACK: 0 +# cplex.semiintegers: 0 +# cplex.solution.type: 3 +# cplex.statusstring: integer optimal solution +# cplex.status: 101 +# cplex.quality.double.CPX_SUM_PRIMAL_RESIDUAL: 2.10942374678779742680490016937255859375E-15 +# cplex.quality.int.CPX_MAX_INDSLACK_INFEAS: -1 +# cplex.numquad: 0 +# cplex.quality.double.CPX_MAX_QCSLACK_INFEAS: 0 +# cplex.quality.int.CPX_MAX_SLACK: 3 +# cplex.quality.double.CPX_MAX_SCALED_X: 5.99999999999999911182158029987476766109466552734375 +# cplex.quality.int.CPX_MAX_QCPRIMAL_RESIDUAL: -1 +# cplex.quality.double.CPX_SUM_SCALED_SLACK: 10.608333333333337833437326480634510517120361328125 +# MODEL_DETAIL_CONTINUOUS_VARS: 26 +# cplex.nodes.processed: 0 +# cplex.quality.double.CPX_MAX_SCALED_PRIMAL_INFEAS: 1.1102230246251565404236316680908203125E-15 +# cplex.model.md5: B019A9CEBA436F0A65EAD04B9378517E +# MODEL_DETAIL_TYPE: MILP +# cplex.quality.double.CPX_MAX_PRIMAL_RESIDUAL: 4.44089209850062616169452667236328125E-16 +# PROGRESS_CURRENT_OBJECTIVE: 144.266750000000001818989403545856475830078125 +# cplex.quality.double.CPX_MAX_X: 5.99999999999999911182158029987476766109466552734375 +# cplex.infeasible: false +# MODEL_DETAIL_NON_ZEROS: 189 +# MODEL_DETAIL_BOOLEAN_VARS: 40 +# MODEL_DETAIL_LINEAR_CONSTRAINTS: 78 +# cplex.quality.double.CPX_SUM_SCALED_PRIMAL_RESIDUAL: 2.10942374678779742680490016937255859375E-15 +# cplex.qpnonzeros: 0 +# cplex.quality.int.CPX_MAX_PRIMAL_INFEAS: -62 +# MODEL_DETAIL_INTEGER_VARS: 0 +# cplex.quality.double.CPX_MAX_INDSLACK_INFEAS: 0 +# cplex.quality.double.CPX_SUM_PRIMAL_INFEAS: 4.55191440096314181573688983917236328125E-15 +# cplex.quality.double.CPX_SUM_SCALED_PRIMAL_INFEAS: 4.55191440096314181573688983917236328125E-15 +# cplex.solution.dfeas: true +# cplex.quality.int.CPX_MAX_SCALED_PRIMAL_INFEAS: -62 +# cplex.quality.double.CPX_MAX_QCSLACK: 0 +# cplex.quality.double.CPX_SUM_X: 85.18333333333333712289459072053432464599609375 +# cplex.quality.double.CPX_MAX_QCPRIMAL_RESIDUAL: 0 +# cplex.quality.double.CPX_MAX_SLACK: 1.875 +# cplex.indicatorconstraints: 0 +# cplex.nodes.left: 0 +# cplex.quality.double.CPX_SUM_QCPRIMAL_RESIDUAL: 0 +# cplex.quality.double.CPX_MAX_SCALED_PRIMAL_RESIDUAL: 4.44089209850062616169452667236328125E-16 +# PROGRESS_GAP: 4.930898076512417 +# cplex.quality.double.CPX_SUM_QCSLACK_INFEAS: 0 +# cplex.quality.int.CPX_MAX_QCSLACK_INFEAS: -1 +# cplex.quality.double.CPX_SUM_INDSLACK_INFEAS: 0 +# cplex.quality.double.CPX_MAX_PRIMAL_INFEAS: 1.1102230246251565404236316680908203125E-15 +# cplex.quality.double.CPX_SUM_SCALED_X: 85.18333333333333712289459072053432464599609375 +# cplex.quality.int.CPX_MAX_X: 1 +# cplex.objective.sense: -1 +# cplex.quality.int.CPX_MAX_PRIMAL_RESIDUAL: 66 +# cplex.quality.double.CPX_MAX_INT_INFEAS: 7.7715611723760957829654216766357421875E-16 +# cplex.solution.method: 12 +# cplex.parameters.md5: B08B4BE748EC31D8D31294F0EBE7F26D +# cplex.time: 0.03053903579711914 +# cplex.quality.int.CPX_MAX_SCALED_X: 1 +# cplex.quality.double.CPX_SUM_INT_INFEAS: 1.5543122344752191565930843353271484375E-15 +# cplex.columns: 66 +# cplex.sosconstraints: 0 +# cplex.mipitcount: 5 +# cplex.quality.int.CPX_MAX_INT_INFEAS: 14 +# cplex.problemtype: 1 +# cplex.quality.double.CPX_MAX_SCALED_SLACK: 1.875 +# cplex.solution.pfeas: true +# cplex.semicontinuous: 0 +# cplex.quality.double.CPX_SUM_SLACK: 10.608333333333337833437326480634510517120361328125 +# MODEL_DETAIL_QUADRATIC_CONSTRAINTS: 0 +# cplex.mipabsgap: 0 +# cplex.quality.int.CPX_MAX_SCALED_SLACK: 3 +# cplex.dettime: 0.2523078918457031 +# PROGRESS_BEST_OBJECTIVE: 144.266750000000001818989403545856475830078125 +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/solution.html b/docs/2.24.232/mp/_modules/docplex/mp/solution.html new file mode 100644 index 0000000..e57ef2a --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/solution.html @@ -0,0 +1,1559 @@ + + + + + + + + + docplex.mp.solution — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.solution

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+
+import sys
+import copy
+
+try:
+    from cplex._internal._subinterfaces import CutType
+except:
+    CutType = list
+
+try:  # pragma: no cover
+    from itertools import zip_longest as izip_longest
+except ImportError:  # pragma: no cover
+    # noinspection PyUnresolvedReferences
+    from itertools import izip_longest
+
+from io import StringIO
+from docplex.mp.constants import CplexScope, BasisStatus, WriteLevel
+from docplex.mp.utils import is_iterable, is_number, is_string, is_indexable, str_maxed, normalize_basename
+from docplex.mp.utils import make_output_path2, _var_match_function
+from docplex.mp.dvar import Var
+from docplex.mp.error_handler import docplex_fatal, handle_error
+
+from docplex.mp.solmst import SolutionMSTPrinter
+from docplex.mp.soljson import SolutionJSONPrinter
+from docplex.mp.solsol import SolutionSolPrinter
+
+from collections import defaultdict
+
+
+# noinspection PyAttributeOutsideInit
+
[docs]class SolveSolution(object): + """ + The :class:`SolveSolution` class holds the result of a solve. + """ + + # a symbolic value for no objective ? + NO_OBJECTIVE_VALUE = -1e+75 + + @staticmethod + def _is_discrete_value(v): + return v == int(v) + + def __init__(self, model, var_value_map=None, obj=None, blended_obj_by_priority=None, name=None, solved_by=None, + keep_zeros=True): + """ SolveSolution(model, var_value_map, obj, name) + + Creates a new solution object, associated to a a model. + + Args: + model: The model to which the solution is associated. This model cannot be changed. + + obj: The value of the objective in the solution. A value of None means the objective is not defined at the + time the solution is created, and will be set later. + + blended_obj_by_priority: For multi-objective models: the value of sub-problems' objectives (each sub-problem + groups objectives having same priority). + + var_value_map: a Python dictionary containing associations of variables to values. + + name: a name for the solution. The default is None, in which case the solution is named after the + model name. + + :return: A solution object. + """ + assert model is not None + assert solved_by is None or is_string(solved_by) + assert obj is None or is_number(obj) or is_indexable(obj) + assert blended_obj_by_priority is None or is_indexable(blended_obj_by_priority) + + self._model = model + self._name = name + self._problem_objective_expr = model.objective_expr if model.has_objective() else None + self._objective = self.NO_OBJECTIVE_VALUE if obj is None else obj + self._blended_objective_by_priority = [self.NO_OBJECTIVE_VALUE] if blended_obj_by_priority is None else \ + blended_obj_by_priority + self._solved_by = solved_by + self._var_value_map = {} + + # attributes + self._sensitivity = {} + self._cuts = None + self._reduced_costs = None + self._dual_values = None + self._slack_values = None + self._infeasibilities = {} + self._basis_statuses = None + + self._solve_status = None + self._keep_zeros = keep_zeros + self._solve_details = None + + if var_value_map is not None: + self._store_var_value_map(var_value_map, keep_zeros=keep_zeros) + + @property + def _checker(self): + return self._model._checker + + @staticmethod + def make_engine_solution(model, var_value_map, obj, blended_obj_by_priority, solved_by, solve_details, + job_solve_status=None): + # INTERNAL + # noinspection PyArgumentEqualDefault + sol = SolveSolution(model, + var_value_map=None, + obj=obj, + blended_obj_by_priority=blended_obj_by_priority, + solved_by=solved_by, + keep_zeros=False) + if solve_details is not None: + sol._solve_details = copy.copy(solve_details) + + if model.round_solution: + # only for models which specify round_solution + roundfn = sol._model._round_function + for dvar, value in var_value_map.items(): + if value and dvar.is_discrete() and value != int(value): + var_value_map[dvar] = roundfn(value) + # do trust engines... + sol._var_value_map = var_value_map + + if job_solve_status is not None: + sol._set_solve_status(job_solve_status) + return sol + + def _get_var_by_name(self, varname): + return self._model.get_var_by_name(varname) + + def as_mip_start(self, write_level=WriteLevel.DiscreteVars, complete_vars=False, eps_zero=1e-6): + write_level_ = WriteLevel.parse(write_level) + filter_discrete = write_level_.filter_nondiscrete() + filter_zeros = write_level_.filter_zeros() + mdl = self.model + mipstart_dict = {} + + def generate_completed_var_values(): + for dv_ in mdl.generate_user_variables(): + yield dv_, self._get_var_value(dv_) + + if not complete_vars: + vv_iter = self.iter_var_values() + else: + vv_iter = generate_completed_var_values() + for dv, dvv in vv_iter: + if filter_discrete and not dv.is_discrete(): + continue + if filter_zeros and abs(dvv) <= eps_zero: + continue + mipstart_dict[dv] = dvv + + mipstart = SolveSolution(self.model, name=self.name, + var_value_map=None, + obj=self.objective_value, + solved_by=self.solved_by, + keep_zeros=True) + mipstart._var_value_map = mipstart_dict + return mipstart + +
[docs] def clear(self): + """ Clears all solve result data. + + All data related to the model are left unchanged. + """ + self._var_value_map = {} + self._objective = self.NO_OBJECTIVE_VALUE + self._reduced_costs = None + self._dual_values = None + self._slack_values = None + self._infeasibilities = {} + self._solve_status = None + self._cuts = None + self._sensitivity = {}
+ +
[docs] def is_empty(self): + """ + Checks whether the solution is empty. + + Returns: + Boolean: True if the solution is empty; in other words, the solution has no defined objective and no variable value. + """ + return not self.has_objective() and not self._var_value_map
+ + @property + def problem_name(self): + return self._model.name + + @property + def solved_by(self): + ''' + Returns a string indicating how the solution was produced. + + - If the solution was created by a program, this field returns None. + - If the solution originated from a local CPLEX solve, this method returns the string 'cplex_local'. + - If the solution originated from a DOcplexcloud solve, this method returns 'cplex_cloud'. + + Returns: + A string, or None. + + ''' + return self._solved_by + + def set_name(self, solution_name): + self._checker.typecheck_string(solution_name, accept_empty=False, accept_none=True, + caller='SolveSolution.set_name(): ') + self._name = solution_name + + @property + def name(self): + """ This property allows to get/set a name on the solution. + + In some cases , it might be interesting to build different solutions for the same model, + in this case, use the name property to distinguish them. + + """ + return self._name + + @name.setter + def name(self, sol_name): + self.set_name(sol_name) + + def _resolve_var(self, var_key, do_raise): + # INTERNAL: accepts either strings or variable objects + # returns a variable or None + if isinstance(var_key, Var): + return var_key + elif is_string(var_key): + var = self._get_var_by_name(var_key) + # var might be None here if the name is unknown + if var is not None: + return var + # var is None hereafter + elif do_raise: + self.model.fatal("No variable with named {0}", var_key) + else: + self.model.warning("No variable with named {0}", var_key) + return None + + else: # pragma: no cover + self.model.fatal("Expecting variable or name, got: {0!r}", var_key) + + def _typecheck_var_key_value(self, var_key, value, caller): + # INTERNAL + self._checker.typecheck_num(value, caller=caller) + if not is_string(var_key) and not isinstance(var_key, Var): + self.model.fatal("{0} expects either Var or string, got: {1!r}", caller, var_key) + +
[docs] def add_var_value(self, var_key, value): + """ Adds a new (variable, value) pair to this solution. + + Args: + var_key: A decision variable (:class:`docplex.mp.dvar.Var`) or a variable name (string). + value (number): The value of the variable in the solution. + """ + self._typecheck_var_key_value(var_key, value, caller="Solution.add_var_value") + self._set_var_key_value(var_key, value, keep_zero=self._keep_zeros)
+ + def __setitem__(self, var_key, value): + # always keep zero, no warnings, no checks + self._set_var_key_value(var_key, value, keep_zero=self._keep_zeros) + + def set_var_key_value(self, var_key, value, keep_zero): + # INTERNAL + self._typecheck_var_key_value(var_key, value, caller="Solution.add_var_value") + self._set_var_key_value(var_key, value, keep_zero) + + def _set_var_key_value(self, var_key, value, keep_zero): + # INTERNAL: no checks done. + dvar = self._resolve_var(var_key, do_raise=False) + if dvar is not None: + if value or keep_zero: + # either value is nonzero or we keep all, store. + self._set_var_value_internal(dvar, value) + elif self.contains(dvar): + # value is 0 and we dont keep zeros: zap the variable, if + del self._var_value_map[dvar] + + def _set_var_value_internal(self, var, value): + self._var_value_map[var] = value + + def _set_var_value(self, var, value): + # INTERNAL + self._set_var_value_internal(var, value) + +
[docs] def update(self, var_values_iterable): + """ + Updates the solution from a dictionary. Keys can be either strings, interpreted as variable names, + or variables; values are the new values for the variable. + + This method returns nothing, only performs a side effect on the solution object. + + :param var_values_iterable: a dictionary of keys, values. + + """ + keep_zeros = self._keep_zeros + for k, v in var_values_iterable.items(): + self._set_var_key_value(k, v, keep_zeros)
+ + @property + def model(self): + """ + This property returns the model associated with the solution. + """ + return self._model + + @property + def solve_details(self): + """ This property returns the solve_details associated with the solution,if any. + + Note: + This property returns an instance of solve details if the solution is the result + of a solve operation. If the solution has been created by API, this property returns None + + See Also: + :class:`docplex.mp.sdetails.SolveDetails` + + Returns: + an instance of SolveDetails, or None. + + """ + return self._solve_details + + # @property + # def error_handler(self): + # return self.__model.error_handler + +
[docs] def get_objective_value(self): + """ + Gets the objective value (or list of objectives value) as defined in the solution. + When the objective value has not been defined, a special value `NO_SOLUTION` is returned. + To check whether the objective has been set, use :func:`has_objective`. + + Returns: + float or list(float): The value of the objective (or list of values for multi-objective) as defined by + the solution. + """ + return self._objective
+ +
[docs] def set_objective_value(self, obj): + """ + Sets the objective value (or list of values for multi-objective) of the solution. + + Args: + obj (float or list(float)): The value of the objective (or list of values for multi-objective) in + the solution. + """ + self._objective = obj
+ +
[docs] def get_blended_objective_value_by_priority(self): + """ + Gets the blended objective value (or list of blended objectives value) by priority level as defined in + the solution. + When the objective value has not been defined, a special value `NO_SOLUTION` is returned. + To check whether the objective has been set, use :func:`has_objective`. + + Returns: + float or list(float): The value of the objective (or list of values for multi-objective) as defined by + the solution. + """ + return self._blended_objective_by_priority
+ + @property + def blended_objective_values(self): + return self._blended_objective_by_priority + +
[docs] def has_objective(self): + """ + Checks whether or not the objective has been set. + + Returns: + Boolean: True if the solution defines an objective value. + """ + return self._objective != self.NO_OBJECTIVE_VALUE
+ + @property + def objective_value(self): + """ This property is used to get the objective value of the solution. + In case of multi-objective this property returns the value for the first objective + + When the objective value has not been defined, a special value `NO_SOLUTION` is returned. + To check whether the objective has been set, use :func:`has_objective`. + + """ + try: + return self._objective[0] + except TypeError: + return self._objective + + @objective_value.setter + def objective_value(self, new_objvalue): + self.set_objective_value(new_objvalue) + + @property + def multi_objective_values(self): + """ This property is used to get the list of objective values of the solution. + In case of single objective this property returns the value for the objective as a singleton list + + When the objective value has not been defined, a special value `NO_SOLUTION` is returned. + To check whether the objective has been set, use :func:`has_objective`. + + """ + self_obj = self._objective + return self_obj if is_indexable(self_obj) else [self_obj] + + @property + def solve_status(self): + return self._solve_status + + def _set_solve_status(self, new_status): + # INTERNAL + self._solve_status = new_status + + def _store_var_value_map(self, key_value_map, keep_zeros=False): + # INTERNAL + for e, val in key_value_map.items(): + # need to check var_keys and values + self.set_var_key_value(var_key=e, value=val, keep_zero=keep_zeros) + + def store_infeasibilities(self, infeasibilities): + assert isinstance(infeasibilities, dict) + self._infeasibilities = infeasibilities + + @staticmethod + def _resolve_attribute_index_map(attr_idx_map, obj_mapper): + return {obj_mapper(idx): attr_val + for idx, attr_val in attr_idx_map.items() + if attr_val and obj_mapper(idx) is not None} + + @classmethod + def _resolve_attribute_list(cls, attr_list, obj_mapper): + # attr list is a list of length N and obj_mapper maps indices to objs + return {obj_mapper(idx): attr_val for idx, attr_val in enumerate(attr_list)} + + def store_attribute_lists(self, mdl, slacks): + def linct_mapper(idx): + return mdl.get_constraint_by_index(idx) + + resolved_linear_slacks = self._resolve_attribute_list(slacks, linct_mapper) + self._slack_values = defaultdict(dict) + self._slack_values[CplexScope.LINEAR_CT_SCOPE] = resolved_linear_slacks + +
[docs] def iter_var_values(self): + """Iterates over the (variable, value) pairs in the solution. + + Returns: + iterator: A dict-style iterator which returns a two-component tuple (variable, value) + for all variables mentioned in the solution. + """ + return self._var_value_map.items()
+ +
[docs] def iter_variables(self): + """Iterates over all variables mentioned in the solution. + + Returns: + iterator: An iterator object over all variables mentioned in the solution. + """ + return self._var_value_map.keys()
+ +
[docs] def contains(self, dvar): + """ + Checks whether or not a decision variable is mentioned in the solution. + + This predicate can also be used in the form `var in solution`, because the + :func:`__contains_` method has been redefined for this purpose. + + Args: + dvar (:class:`docplex.mp.dvar.Var`): The variable to check. + + Returns: + Boolean: True if the variable is mentioned in the solution. + """ + return dvar in self._var_value_map
+ + def __contains__(self, dvar): + return self.contains(dvar) + +
[docs] def get_value(self, arg): + """ + Gets the value of a variable or an expression in a solution. + If the variable is not mentioned in the solution, + the method returns 0 and does not raise an exception. + Note that this method can also be used as :func:`solution[arg]` + because the :func:`__getitem__` method has been overloaded. + + Args: + arg: A decision variable (:class:`docplex.mp.dvar.Var`), + a variable name (a string), or an expression. + + Returns: + float: The value of the variable in the solution. + """ + if is_string(arg): + var = self._get_var_by_name(arg) + if var is None: + self.model.fatal("No variable with name: {0}", arg) + else: + return self._get_var_value(var) + elif isinstance(arg, Var): + return self._get_var_value(arg) + else: + try: + v = arg._raw_solution_value(self) + return v + except AttributeError: + self._model.fatal("Expecting variable, variable name or expression, {0!r} was passed", arg)
+ + def get_var_value(self, dvar): + self._checker.typecheck_var(dvar) + return self._get_var_value(dvar) + + def _get_var_value(self, dvar): + # INTERNAL + return self._var_value_map.get(dvar, 0) + +
[docs] def get_value_list(self, dvars): + """ + Gets the value of a sequence of variables in a solution. + If a variable is not mentioned in the solution, + the method assumes a 0 value. + + Args: + dvars: an ordered sequence of decision variables. + + Returns: + list: A list of float values, in the same order as the variable sequence. + + """ + checker = self._checker + checker.check_ordered_sequence(arg=dvars, + caller='SolveSolution.get_values() expects ordered sequence of variables') + dvar_seq = checker.typecheck_var_seq(dvars) + return self._get_values(dvar_seq)
+ +
[docs] def get_values(self, var_seq): + """ Same as get_value_list + """ + return self.get_value_list(var_seq)
+ + def _get_values(self, dvars): + # internal: no checks are done. + self_value_map = self._var_value_map + return [self_value_map.get(dv, 0) for dv in dvars] + + def _get_all_values(self): + # internal: no checks are done. + self_value_map = self._var_value_map + m = self._model + return [self_value_map.get(dv, 0) for dv in m.iter_variables()] + + @staticmethod + def _accept_value(value, accept_zeros: bool, precision: float = 1e-6): + # INTERNAL + if not value: + # accepting zero values is controlled by the accept_zeros flag. + return accept_zeros + else: + return abs(value) >= precision + +
[docs] def get_value_dict(self, var_dict, keep_zeros=True, precision=1e-6): + """ Converts a dictionary of variables to a dictionary of solutions + + Assuming `var_dict` is a dictionary of variables + (for example, as returned by `Model.integer_var_dict()`, + returns a dictionary with the same keys and as values the solution values of the + variables. + + :param var_dict: a dictionary of decision variables. + :param keep_zeros: an optional flag to keep zero values (default is True) + :param precision: an optional precision, used to filter small non-zero values. + The default is 1e-6. + + :return: A dictionary from variable keys to solution values (floats). + """ + # assume var_dict is a key-> variable dictionary + + assert precision >= 0 + # if precision -> abs(dvv) >= prec else dvv + value_dict = {} + for key, dvar in var_dict.items(): + dvar_value = self._get_var_value(dvar) + if self._accept_value(dvar_value, keep_zeros, precision=precision): + value_dict[key] = dvar_value + return value_dict
+ +
[docs] def get_value_df(self, var_dict, value_column_name=None, key_column_names=None): + """ Returns values of a dicitonary of variables, as a pandas dataframe. + + If pandas is not present, returns a dicitonary of columns. + + :param var_dict: the dicitonary of variables, as created by Model.xx_var_dict + :param value_column_name: an optional string to name the value column. Default is 'value' + :param key_column_names: an optional list of strings to name th ekeys of the dicitonary. + If not present, keys are named 'k1', 'k2', ... + + :return: a pandas DataFrame, if pandas is present. + """ + keys = list(var_dict.keys()) + values = self.get_values((dv for dv in var_dict.values())) + if isinstance(keys[0], tuple): + keys = list(zip(*keys)) + knames = None + if key_column_names: + if len(key_column_names) == len(keys): + knames = key_column_names + if not knames: + knames = ['key_%d' % k for k in range(1, len(keys) + 1)] + kd = {kn: ks for kn, ks in zip(knames, keys)} + else: + kn = key_column_names or 'key' + kd = {kn: keys} + value_col_name = value_column_name or 'value' + kd[value_col_name] = values + try: + import pandas as pd + return pd.DataFrame(kd) + except ImportError: + self.model.warning("pandas module not found, returning a dict instead of DataFrame") + return kd
+ + # def __len__(self): + # return len(self.__var_value_map) + + @property + def number_of_var_values(self): + """ This property returns the number of variable/value pairs stored in this solution. + """ + return len(self._var_value_map) + + @property + def size(self): + """ This property returns the number of variable/value pairs stored in this solution. + """ + return len(self._var_value_map) + + def __getitem__(self, arg): + return self.get_value(arg) + +
[docs] def get_status(self, ct): + """ Returns the status of a linear constraint in the solution. + + Returns 1 if the constraint is satisfied, else returns 0. This is particularly useful when using + the status variable of constraints. + + :param ct: A linear constraint + :return: a number (1 or 0) + """ + self._checker.typecheck_linear_constraint(ct) + return self._get_status(ct)
+ + def _get_status(self, ct): + # INTERNAL + ct_status_var = ct._get_status_var() + if ct_status_var: + return self._var_value_map.get(ct_status_var, 0) + elif ct.is_added(): + # a posted constraint is true if there is a solution + return 1 + else: + return 1 if ct.is_satisfied(self) else 0 + + def find_unsatisfied_constraints(self, m, tolerance=1e-6): + unsats = [] + for ct in m.iter_constraints(): + if not ct.is_satisfied(self, tolerance): + unsats.append(ct) + return unsats + + def number_of_var_diffs(self, other_sol, precision=1e-6, match="auto"): + target_model = other_sol.model + var_match_fn = _var_match_function(self.model, target_model, match) + nb_diffs = 0 + for dv, dvv in self.iter_var_values(): + other_dv = var_match_fn(dv, target_model) + if other_dv: + other_dvv = other_sol[other_dv] + if abs(dvv - other_dvv) >= precision: + nb_diffs += 1 + return nb_diffs + + def restore(self, target_model, abs_tolerance=1e-6, rel_tolerance=1e-4, restore_all=False, match="auto"): + # restores the solution in its model, adding ranges. + find_matching_var = _var_match_function(self.model, target_model, match) + lfactory = target_model._lfactory + restore_ranges = [] + for dvar, val in self.iter_var_values(): + if not dvar.is_generated() or restore_all: + dvar2 = find_matching_var(dvar, target_model) + if dvar2 is not None: + rel_prec = abs(val) * rel_tolerance + used_prec = max(abs_tolerance, rel_prec) + rlb = max(dvar2.lb, val - used_prec) + rub = min(dvar2.ub, val + used_prec) + if rlb >= rub + 1e-6: + target_model.fatal("restore solution fails on empty domain, var: {)}, lb={1} > ub={2}", + dvar2, rlb, rub) + restore_ranges.append(lfactory.new_range_constraint(rlb, dvar2, rub)) + else: + print("could not find matching var for {0}".format(dvar)) + target_model.info("restored {0} variable values using range constraints".format(len(restore_ranges))) + return target_model.add(restore_ranges) + + def find_invalid_domain_variables(self, m, tolerance=1e-6): + invalid_domain_vars = [] + for dv in m.iter_variables(): + dvv = self.get_var_value(dv) + if not dv.accepts_value(dvv, tolerance=tolerance): + invalid_domain_vars.append(dv) + return invalid_domain_vars + +
[docs] def is_valid_solution(self, tolerance=1e-6, silent=True): + """ Returns True if the solution is feasible. + + This method checks that solution values for variables are compatible for their types + and bounds. It also checks that all constraints are satisfied, within the tolerance. + + :param tolerance: a float number used to check satisfaction; default is 1e-6. + :param silent: optional flag. If False, prints which variable (or constraint) + causes the solution to be invalid. default is True. + + :return: True if the solution is valid, within the tolerance value. + + *New in version 2.13* + """ + m = self.model + verbose = not silent + invalid_domain_vars = self.find_invalid_domain_variables(m, tolerance) + if verbose and invalid_domain_vars: + m.warning("invalid domain vars: {0}".format(len(invalid_domain_vars))) + for v, invd_var in enumerate(invalid_domain_vars, start=1): + dvv = self.get_var_value(invd_var) + m.warning("{0} - invalid value {1} for variable {2}({5}), [{3}, {4}]".format(v, dvv, invd_var.lp_name, + invd_var.lb, invd_var.ub, + invd_var.cplex_typecode)) + + unsat_cts = self.find_unsatisfied_constraints(m, tolerance) + if verbose and unsat_cts: + m.info("unsatisfied constraints[{0}]".format(len(unsat_cts))) + for u, uct in enumerate(unsat_cts, start=1): + if uct.is_logical(): + # TODO: compute a measure of violation on logical cts + s_violated = '' + else: + uctv = uct._compute_violation(self, tolerance) + s_violated = f', violated: {uctv:0.3g}' + s_uct = uct.to_readable_string() + ctx = uct.index + 1 + m.warning("{0} - unsatisfied constraint[#{1}]: {2}{3}".format(u, ctx, s_uct, s_violated)) + return not (invalid_domain_vars or unsat_cts)
+ + is_feasible_solution = is_valid_solution + + def equals(self, other, check_models=False, obj_precision=1e-3, var_precision=1e-6, assume_equal_indices=True): + from itertools import dropwhile + if check_models and (self.model is not other.model): + return False + + if is_iterable(self.objective_value) and is_iterable(other.objective_value): + if len(self.objective_value) == len(other.objective_value): + for self_obj_val, other_obj_val in zip(self.objective_value, other.objective_value): + if abs(self_obj_val - other_obj_val) >= obj_precision: + return False + else: # Different number of objectives + return False + elif not is_iterable(self.objective_value) and not is_iterable(other.objective_value): + if abs(self.objective_value - other.objective_value) >= obj_precision: + return False + else: # One solution is for multi-objective, and not the other + return False + + # noinspection PyPep8 + this_triplets = [(dv.index, dv.name, svalue) for dv, svalue in dropwhile(lambda dvv: not dvv[1], + self.iter_var_values())] + other_triplets = [(dv.index, dv.name, svalue) for dv, svalue in dropwhile(lambda dvv: not dvv[1], + other.iter_var_values())] + # noinspection PyArgumentList + res = True + for this_triple, other_triple in zip(this_triplets, other_triplets): + this_index, this_name, this_val = this_triple + other_index, other_name, other_val = other_triple + if (assume_equal_indices and (other_index != this_index)) \ + or this_name != other_name \ + or abs(this_val - other_val) >= var_precision: + res = False + break + return res + + def ensure_reduced_costs(self, model, engine): + if self._reduced_costs is None: + self._reduced_costs = engine.get_all_reduced_costs(model) + + def ensure_cuts(self, model, engine): + if self._cuts is None: + self._cuts = engine.get_all_cuts(model) + + def ensure_dual_values(self, model, engine): + if self._dual_values is None: + self._dual_values = engine.get_all_dual_values(model) + + def ensure_slack_values(self, model, engine): + if self._slack_values is None: + self._slack_values = engine.get_all_slack_values(model) + + def ensure_basis_statuses(self, model, engine): + if self._basis_statuses is None: + # returns a tuple of two lists + self._basis_statuses = engine.get_basis(model) + + def has_basis(self): + m = self.model + self.ensure_basis_statuses(m, m.get_engine()) + return self._has_basis() + + def _has_basis(self): + try: + return len(self._basis_statuses[0]) > 0 + except TypeError: + return False + +
[docs] def get_sensitivity(self, dvars): + """ Returns the sensitivity values for a variable iterable. + + Note: The model must be solved successfully before calling this method. + + :param dvars: a sequence of variables. + :return: a list of tuples, in the same order as the variable sequence. Each tuple contains 3 tuples: the lower lower_bounds, the upper_bounds and the objective + For example [((-1e+20, 2.5), (-3.0, 5.0), (-1e+20, -2.0), (0.0, 4.0)), ((-1e+20, 2.5), (-3.0, 5.0), (-1e+20, -2.0), (0.0, 4.0))] + """ + ret = [None for d in dvars] + idx = {d : i for i,d in enumerate(dvars)} + todo = [] + for d in dvars: + if d in self._sensitivity.keys(): + sensitivity = self._sensitivity[d] + i = idx[d] + ret[i] = sensitivity + else: + todo.append(d) + if len(todo) != 0: + m = self.model + values = m.get_engine().get_sensitivity(todo) + for d,v in zip(todo, values): + i = idx[d] + ret[i] = v + return ret
+ + +
[docs] def get_num_cuts(self, cut_type): + """ Returns the number of cuts for a specific type. + + :param cut_type: a cut type. + :return: the number of cuts associated to this type of cut. 0 if CPLEX is not present + """ + cut_type_instance = CutType() + if cut_type in cut_type_instance: + cuts = self.get_cuts() + name = cut_type_instance[cut_type] + return cuts[name] + return 0
+ + + +
[docs] def get_cuts(self): + """ Returns the number of cuts under the form of a dict(type -> number). + + :return: the number of cuts under the form of a dict(type -> number). Empty dict if CPLEX is not present. + """ + m = self.model + self.ensure_cuts(m, m.get_engine()) + return self._cuts
+ +
[docs] def get_reduced_costs(self, dvars): + """ Returns the reduced costs for a variable iterable. + + Note: the model must a pure LP: no integer or binary variable, no piecewise, no SOS. + The model must also be solved successfully before calling this method. + + :param dvars: a sequence of variables. + :return: a list of float numbers, in the same order as the variable sequence. + """ + m = self.model + self.ensure_reduced_costs(m, m.get_engine()) + rcs = self._reduced_costs + assert rcs is not None + return [rcs.get(dv, 0) for dv in dvars]
+ +
[docs] def get_dual_values(self, lcts): + """ Returns the dual values of a sequence of linear constraints. + + Note: the model must a pure LP: no integer or binary variable, no piecewise, no SOS. + The model must also be solved successfully before calling this method. + + :param lcts: a sequence of linear constraints. + + :return: a sequence of float numbers + """ + duals = self._dual_values + assert duals is not None + return [duals.get(lc, 0) for lc in lcts]
+ +
[docs] def get_slacks(self, cts): + """ Return the slack values for a sequence of constraints. + + Slack values are available for linear, quadratic and indicator constraints. + The model must be solved successfully before calling this method. + + :param cts: a sequence of constraints. + :return: a list of float values, in the same order as the constraints. + """ + all_slacks = self._slack_values + assert all_slacks is not None + # first get cplex_scope, then fetch the slack: two indirections + return [all_slacks[ct.cplex_scope].get(ct, 0) for ct in cts]
+ +
[docs] def slack_value(self, ct, error='raise'): + """ Return the slack value for a constraint. + + Slack values are available for linear, quadratic and indicator constraints. + The model must be solved successfully before calling this method. + + :param ct: a constraint. + :return: the float value. + """ + all_slacks = self._slack_values + slack = 0 + if all_slacks is None: + handle_error(logger=self.model, error=error, msg="Solution contains no slack data") + else: + slack = all_slacks[ct.cplex_scope].get(ct, 0) + return slack
+ + def get_var_basis_statuses(self, dvars): + assert self._basis_statuses is not None + all_var_basis_statuses = self._basis_statuses[0] + return [BasisStatus.parse(all_var_basis_statuses.get(dv, -1)) for dv in dvars] + + def get_linearct_basis_statuses(self, linear_cts): + assert self._basis_statuses is not None + all_linearct_basis_statuses = self._basis_statuses[1] + return [BasisStatus.parse(all_linearct_basis_statuses.get(lct, -1)) for lct in linear_cts] + + def get_infeasibility(self, ct): + return self._infeasibilities.get(ct, 0) + + def display_attributes(self): + pass + + def display(self, + print_zeros=True, + header_fmt="solution for: {0:s}", + objective_fmt="{0}: {1:.{prec}f}", + value_fmt="{varname:s} = {value:.{prec}f}", + iter_vars=None, + **kwargs): + print_generated = kwargs.get("print_generated", False) + problem_name = self.problem_name + if header_fmt and problem_name: + print(header_fmt.format(problem_name)) + if self._problem_objective_expr is not None and objective_fmt and self.has_objective(): + obj_prec = self.model.objective_expr.float_precision + print(objective_fmt.format('objective', self._objective, prec=obj_prec)) + if self.solve_status is not None: + print("status: %s(%d)" %(self.solve_status.name,self.solve_status.value)) + if self.solve_details is not None and len(self.solve_details.quality_metrics) != 0: + for k,v in self.solve_details.quality_metrics.items(): + if abs(v) > 1e-16: + if isinstance(v,int): + print("%s: %d" %(k,v)) + else: + print("%s: %f16" % (k, v)) + if iter_vars is None: + iter_vars = self.iter_variables() + print_counter = 0 + for dvar in iter_vars: + if print_generated or not dvar.is_generated(): + var_value = self._get_var_value(dvar) + if print_zeros or var_value: + print_counter += 1 + varname = dvar.lp_name + # if type(value_fmt) != type(varname): + # # infamous mix of str and unicode. Should happen only + # # in py2. Let's convert things + # if isinstance(value_fmt, str): + # # noinspection PyUnresolvedReferences + # value_fmt = value_fmt.decode('utf-8') + # else: + # value_fmt = value_fmt.encode('utf-8') + output = value_fmt.format(varname=varname, + value=var_value, + prec=dvar.float_precision, + counter=print_counter) + try: + print(output) + except UnicodeEncodeError: + encoding = 'ascii' + if hasattr(sys.stdout, 'encoding') and sys.stdout.encoding: + encoding = sys.stdout.encoding + print(output.encode(encoding, + errors='backslashreplace')) + + def to_string(self, print_zeros=True): + oss = StringIO() + self.to_stringio(oss, print_zeros=print_zeros) + return oss.getvalue() + + def to_stringio(self, oss, print_zeros=True): + problem_name = self.problem_name + if problem_name: + oss.write("solution for: %s\n" % problem_name) + if self._problem_objective_expr is not None and self.has_objective(): + oss.write("objective: %g\n" % self._objective) + if self.solve_status is not None: + oss.write("status: %s(%d)\n" %(self.solve_status.name,self.solve_status.value)) + if self.solve_details is not None and len(self.solve_details.quality_metrics) != 0: + for k,v in self.solve_details.quality_metrics.items(): + if abs(v) > 1e-16: + if isinstance(v,int): + oss.write("%s: %d\n" %(k,v)) + else: + oss.write("%s: %f16\n" % (k, v)) + + value_fmt = "{var:s}={value:.{prec}f}" + for dvar, val in self.iter_var_values(): + if not dvar.is_generated(): + var_value = self._get_var_value(dvar) + if print_zeros or var_value != 0: + oss.write(value_fmt.format(var=str(dvar), value=var_value, prec=dvar.float_precision)) + oss.write("\n") + + def __str__(self): + return self.to_string() + + def __repr__(self): + if self.has_objective(): + s_obj = "obj={0:g}".format(self.objective_value) + else: + s_obj = "obj=N/A" + s_values = ",".join(["{0!s}:{1:g}".format(var, val) for var, val in self.iter_var_values()]) + r = "docplex.mp.solution.SolveSolution({0},values={{{1}}})".format(s_obj, s_values) + return str_maxed(r, maxlen=72) + + def __iter__(self): + # INTERNAL: this is necessary to prevent solution from being an iterable. + # as it follows getitem protocol, it can mistakenly be interpreted as an iterable + raise TypeError + + def __as_df__(self, name_key='name', value_key='value'): + return self.as_df(name_key, value_key) + +
[docs] def as_df(self, name_key='name', value_key='value'): + """ Converts the solution to a pandas dataframe with two columns: variable name and values + + :param name_key: column name for variable names. Default is 'name' + :param value_key: cilumn name for values., Default is 'value'. + + :return: a pandas dataframe, if pandas is present. + + *New in version 2.15* + """ + assert name_key + assert value_key + assert name_key != value_key + try: + import pandas as pd + except ImportError: + raise ImportError('Cannot convert solution to pandas.DataFrame if pandas is not available') + + names = [] + values = [] + for dv, dvv in self.iter_var_values(): + names.append(dv.to_string()) + values.append(dvv) + name_value_dict = {name_key: names, value_key: values} + return pd.DataFrame(name_value_dict)
+ +
[docs] def print_mst(self, outs=None, **kwargs): + """ Writes the solution in MST format in an output stream (default is sys.out) + """ + if outs is None: + outs = sys.stdout + self.export(outs, format='mst', **kwargs)
+ + def _export_as_string(self, format_spec, **kwargs): + # INTERNAL + printer = self._new_printer(format_spec) + return printer.print_to_string(self, **kwargs) + + def export_as_mst_string(self, write_level=WriteLevel.Auto, **kwargs): + kwargs['write_level'] = WriteLevel.parse(write_level) + return self._export_as_string(format_spec='mst', **kwargs) + +
[docs] def export_as_mst(self, path=None, basename=None, write_level=WriteLevel.Auto, **kwargs): + """ Exports a solution to a file in CPLEX mst format. + + Args: + basename: Controls the basename with which the solution is printed. + Accepts None, a plain string, or a string format. + If None, the model's name is used. + If passed a plain string, the string is used in place of the model's name. + If passed a string format (either with %s or {0}), this format is used to format the + model name to produce the basename of the written file. + path: A path to write the file, expects a string path or None. + Can be a directory, in which case the basename + that was computed with the basename argument is appended to the directory to produce + the file. + If given a full path, the path is directly used to write the file, and + the basename argument is not used. + If passed None, the output directory will be ``tempfile.gettempdir()``. + write_level: an enumerated value which controls which variables are printed. + The default is WriteLevel.Auto, which prints the values of all discrete variables. + This parameter also accepts the number values of the corresponding CPLEX parameters + (1 for AllVars, 2 for DiscreteVars, 3 for NonZeroVars, 4 for NonZeroDiscreteVars) + + Returns: + The full path of the file, when successful, else None + + Examples: + Assuming the solution has the name "prob": + + ``sol.export_as_mst()`` will write file prob.mst in a temporary directory. + + ``sol.export_as_mst(write_level=WriteLevel.ALlvars)`` will write file prob.mst in a temporary directory, + and will print all variables in the problem. + + ``sol.export_as_mst(path="c:/temp/myprob1.mst")`` will write file "c:/temp/myprob1.mst". + + ``sol.export_as_mst(basename="my_%s_mipstart", path ="z:/home/")`` will write "z:/home/my_prob_mipstart.mst". + + Note: + The complete description of MST format is found here: + https://www.ibm.com/support/knowledgecenter/SSSA5P_20.1.0/ilog.odms.cplex.help/CPLEX/FileFormats/topics/MST.html + + + See Also: + :class:`docplex.mp.constants.WriteLevel` + """ + kwargs2 = kwargs.copy() + kwargs2['write_level'] = WriteLevel.parse(write_level) + return self._export(format_spec='mst', path=path, basename=basename, **kwargs2)
+ +
[docs] def export_as_sol(self, path=None, basename=None, **kwargs): + """ Exports a solution to a file in CPLEX SOL format. + + SOL format is valid for all types of solutions, LP or MIP, but cannot be used for warm starts. + + Arguments are identical to the method :func:`export_as_mst` + + Note: + The complete description of SOL format is found here: + https://www.ibm.com/support/knowledgecenter/SSSA5P_20.1.0/ilog.odms.cplex.help/CPLEX/FileFormats/topics/SOL.html + + See Also: + :func:`docplex.mp.model.SolveSolution.export_as_mst` + + """ + return self._export(format_spec='sol', path=path, basename=basename, **kwargs)
+ + def _export(self, format_spec, path=None, basename=None, **kwargs): + # INTERNAL + printer = self._new_printer(format_spec) + return self._static_export(exported=self, + basename=self.problem_name, + printer=printer, + path=path, + basename_fmt=basename, + **kwargs + ) + + @classmethod + def _static_export(cls, exported, basename, printer, path, basename_fmt, **kwargs): + sol_basename = normalize_basename(basename, force_lowercase=True) + mst_path = make_output_path2(actual_name=sol_basename, + extension=printer.extension(), + path=path, + basename_fmt=basename_fmt) + if mst_path: + printer.print_to_stream(exported, mst_path, **kwargs) + return mst_path + + # noinspection PyPep8 + @classmethod + def _new_printer(cls, format_spec): + printers = {'json': SolutionJSONPrinter, + 'xml': SolutionMSTPrinter, + 'mst': SolutionMSTPrinter, + 'sol': SolutionSolPrinter + } + printer_type = printers.get(format_spec.lower()) + if not printer_type: + raise ValueError("format key must be one of {}".format(printers.keys())) + return printer_type() + +
[docs] def export(self, file_or_filename, format="json", **kwargs): + """ Export this solution. + + Args: + file_or_filename: If ``file_or_filename`` is a string, this argument contains the filename to + write to. If this is a file object, this argument contains the file object to write to. + format: A string, the name of format to use. Possible values are: + - "json" + - "mst": the MST cplex format for MIP starts + - "xml": same as MST + kwargs: additional kwargs passed to the actual exporter + """ + printer = self._new_printer(format) + + if isinstance(file_or_filename, str): + fp = open(file_or_filename, "w") + close_fp = True + else: + fp = file_or_filename + close_fp = False + try: + printer.print_to_stream(self, fp, **kwargs) + finally: + if close_fp: + fp.close()
+ +
[docs] def export_as_json_string(self, **kwargs): + """ Returns the solution as a string in JSON format. + + :return: a string. + + *New in version 2.10* + """ + return self._export_as_string(format_spec='json', **kwargs)
+ + def export_as_sol_string(self, **kwargs): + return self._export_as_string(format_spec='sol', **kwargs) + +
[docs] def check_as_mip_start(self, strong_check=False): + """Checks that this solution is a valid MIP start. + + To be valid, it must have: + * at least one discrete variable (integer or binary), and + * the values for decision variables should be consistent with the type. + + Returns: + Boolean: True if this solution is a valid MIP start. + """ + count_values = 0 + count_errors = 0 + m = self.model + for dv, dvv in self.iter_var_values(): + if dv.is_discrete() and not dv.is_generated(): + count_values += 1 + if not dv.accepts_value(dvv): # pragma: no cover + count_errors += 1 + m.warning("Solution value {1} is outside the domain of variable {0!r}: {1}, type: {2!s}", + dv, dvv, dv.vartype.short_name) + if count_values == 0: + docplex_fatal("MIP start contains no discrete variable") # pragma: no cover + return not count_errors if strong_check else True
+ + def as_dict(self, keep_zeros=False): + var_value_dict = {} + # INTERNAL: return a dictionary of variable: variable_value + for dvar, dval in self.iter_var_values(): + if keep_zeros or dval: + var_value_dict[dvar] = dval + return var_value_dict + + def as_name_dict(self, keep_zeros=False, error='ignore'): + # return a dictionary of variable_name: variable_value + def var_name_or_lp_name(dvar_): + return dvar_.name or dvar_.lp_name + + return self._as_dict(var_name_or_lp_name, keep_zeros, error) + + def as_index_dict(self, keep_zeros=False, error='ignore'): + # return a dictionary of var index: variable_value + # invalid indices are ignored + def var_valid_index(dvar_): + var_idx = dvar_.index + return var_idx if var_idx >= 0 else None + + return self._as_dict(var_valid_index, keep_zeros, error) + + def _as_dict(self, var_to_key_fn, keep_zeros=False, error='ignore'): + # INTERNAL + key_value_dict = {} + for dvar, dval in self.iter_var_values(): + if keep_zeros or dval: + dvar_key = var_to_key_fn(dvar) + if dvar_key is not None: + key_value_dict[dvar_key] = dval + else: + msg = ("Invalid variable key in solution.as_dict, variable: {0}, transformer: {1}" + .format(dvar, var_to_key_fn.__name__)) + handle_error(logger=self.model, error=error, msg=msg) + return key_value_dict + +
[docs] def kpi_value_by_name(self, name, match_case=False): + ''' Returns the solution value of a KPI from its name. + + Args: + name (string): The string to be matched. + + match_case (boolean): If True, looks for a case-exact match, else + ignores case. Default is False. + + Returns: + The value of the KPI, evaluated in the solution. + + Note: + This method raises an error when the string does not match any KPI in the model. + + See: + :func: `docplex.mp.model.kpi_by_name` + ''' + kpi = self.model.kpi_by_name(name, try_match=True, match_case=match_case) + return kpi._raw_solution_value(self)
+ +
[docs] @classmethod + def from_file(cls, filename, mdl): + """ Builds solution(s) from a SOL file. + Assumes `filename` is in CPLEX SOL format, + reference: https://www.ibm.com/support/knowledgecenter/SSSA5P_20.1.0/ilog.odms.cplex.help/CPLEX/FileFormats/topics/SOL.html + + Returns: + a list of solution objects, read from the file, or None, if an error occured. + """ + from docplex.mp.sol_xml_reader import read_sol_file + sols = read_sol_file(filename, mdl) + return sols
+ + +
[docs]class SolutionPool(object): + """SolutionPool() + + Solutions pools as returned by `Model.populate()` + + This class is not to be instantiated by users, only used after returned by Model.populate. + + Instances of this class can be used like lists. They are fully iterable, + and accessible by index. + + See Also: + :func:`docplex.mp.model.Model.populate` + + """ + + def __init__(self, sols, num_replaced=0): + self._solutions = tuple(sols) + self._num_replaced = num_replaced + + def __iter__(self): + """ Returns an iterator on pool solutions. + """ + return iter(self._solutions) + + def __len__(self): + """ Returns the number of solutions in the pool. + + """ + return self.size + + @property + def size(self): + """ Returns the number of solutions in the pool. + + :return: + """ + return len(self._solutions) + + def __getitem__(self, item): + return self._solutions[item] + + def __str__(self): + return 'SolutionPool[{0}](mean={1:.3f})'.format(len(self), self.mean_objective_value) + + def __repr__(self): + return "docplex.mp.SolutionPool[{0}]".format(len(self)) + + @property + def num_replaced(self): + return self._num_replaced + + @property + def mean_objective_value(self): + """ This property returns the mean objective value in the pool. + + """ + return self.stats[1] + +
[docs] def describe_objectives(self): + """ Prints statistical information about poolobjective values. + + Relies on the `stats` property. + + """ + nb_solutions, obj_mean, obj_sd, obj_min, obj_med, obj_max = self.stats + print("count = {0}".format(nb_solutions)) + print("mean = {0}".format(obj_mean)) + print("std = {0}".format(obj_sd)) + print("min = {0}".format(obj_min)) + print("med = {0}".format(obj_med)) + print("max = {0}".format(obj_max))
+ + @property + def stats(self): + """ Returns statistics about pool objective values. + + :return: a tuple of floats containing (in this order: + - number of solutions (same as len() + - mean objective value + - standard deviation + - minimum objective value + - median objective value + - maximum objective value + + Note: + if pool is empty returns dummy values, only the first value (len of 0) is valid. + """ + from math import sqrt + nb_solutions = len(self) + obj_min = 1e+75 + obj_max = -1e+75 + if not nb_solutions: + # dummy values + return 0, 0, 0, obj_min, obj_min, obj_max + + objs = [] + obj_sum1 = 0 + obj_sum2 = 0 + for ps in self._solutions: + obj = ps.objective_value + objs.append(obj) + if obj < obj_min: + obj_min = obj + if obj > obj_max: + obj_max = obj + obj_sum1 += obj + obj_sum2 += obj * obj + obj_med = sorted(objs)[nb_solutions // 2] + + obj_mean = obj_sum1 / nb_solutions + variance = (obj_sum2 / nb_solutions) - (obj_mean ** 2) + obj_sd = sqrt(variance) + return nb_solutions, obj_mean, obj_sd, obj_min, obj_med, obj_max + +
[docs] def export_as_sol(self, path=None, basename=None, **kwargs): + """ Exports the solution pool as a SOL file. + + Args: + basename: Controls the basename with which the solution is printed. + Accepts None, a plain string, or a string format. + If None, the model name is used. + If passed a plain string, the string is used in place of the model's name. + If passed a string format (either with %s or {0}), this format is used to format the + model name to produce the basename of the written file. + path: A path to write the file, expects a string path or None. + Can be a directory, in which case the basename + that was computed with the basename argument is appended to the directory to produce + the file. + If given a full path, the path is directly used to write the file, and + the basename argument is not used. + If passed None, the output directory will be ``tempfile.gettempdir()``. + + :return: + The path to which the solutions from the pool are written, or None if an error occured. + """ + printer = SolutionSolPrinter() + return SolveSolution._static_export(exported=self._solutions, + basename="pool", + printer=printer, + basename_fmt=basename, + path=path, + **kwargs)
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/sosvarset.html b/docs/2.24.232/mp/_modules/docplex/mp/sosvarset.html new file mode 100644 index 0000000..706510b --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/sosvarset.html @@ -0,0 +1,230 @@ + + + + + + + + + docplex.mp.sosvarset — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.sosvarset

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+from docplex.mp.basic import IndexableObject, _AbstractBendersAnnotated
+from docplex.mp.constants import CplexScope
+
+
+
[docs]class SOSVariableSet(IndexableObject, _AbstractBendersAnnotated): + ''' This class models :index:`Special Ordered Sets` (SOS) of decision variables. + An SOS has a type (SOS1, SOS2) and an ordered list of variables. + + This class is not meant to be instantiated directly. + To create an SOS, use the :func:`docplex.mp.model.Model.add_sos`, :func:`docplex.mp.model.Model.add_sos1`, + and :func:`docplex.mp.model.Model.add_sos2` methods in Model. + ''' + + def __init__(self, model, variable_sequence, sos_type, weights=None, name=None): + IndexableObject.__init__(self, model, name) + self._sos_type = sos_type + self._variables = variable_sequence[:] # copy sequence + self._set_weights(weights) + + def _set_weights(self, new_weights): + if new_weights is None: + self._weights = None + else: + checker = self._model._checker + checked_weights = checker.typecheck_num_seq(new_weights) + weight_list = list(checked_weights) + nb_vars = len(self._variables) + if len(weight_list) != nb_vars: + self._model.fatal("Expecting {0} SOS weights, a list with size {1} was passed", + nb_vars, len(weight_list)) + self._weights = weight_list[:] + + @property + def cplex_scope(self): + return CplexScope.SOS_SCOPE + + @property + def sos_type(self): + ''' This property returns the type of the SOS variable set. + + :returns: An enumerated value of type :class:`docplex.mp.constants.SOSType`. + ''' + return self._sos_type + +
[docs] def iter_variables(self): + ''' Iterates over the variables in the SOS. + + Note that the sequence of variables cannot be empty. + + Returns: + An iterator. + ''' + return iter(self._variables)
+ + def __len__(self): + ''' This special method makes it possible to call the `len()` function on an SOS, + returning the number of variables in the SOS. + + Returns: + The number of variables in the SOS. + ''' + return len(self._variables) + + def __getitem__(self, item): + ''' This special method enables the [] operator on special ordered sets, + + + Args: + item: an integer from 0 to the number of variables -1 + + Returns: + The variable in the set at location <item> + ''' + return self._variables[item] + +
[docs] def to_string(self): + ''' Converts an SOS of variables to a string. + + This function is used by the `__str__()` method + + Returns: + A string representation of the SOS of variables. + ''' + vars_s = ', '.join(str(v) for v in self.iter_variables()) + name_s = '(\'%s\')' % self._name if self._name else '' + return '{0!s}{2}[{1:s}]'.format(self._sos_type.name, vars_s, name_s)
+ + @property + def weights(self): + self_weights = self._weights + return self_weights if self_weights is not None else list(range(1, len(self) + 1)) + + @weights.setter + def weights(self, new_weights): + self._set_weights(new_weights) + + def as_constraint(self): + mdl = self._model + lfactory = mdl._lfactory + lhs = mdl.sum_vars(self._variables) + rhs = lfactory.constant_expr(self.sos_type.value) + return lfactory.new_binary_constraint(lhs, "le", rhs, name=self.name) + + + def __str__(self): + ''' Redefine the standard __str__ method of Python objects to customize string conversion. + + Returns: + A string representation of the SOS of variables. + ''' + return self.to_string() + + def __repr__(self): + name_s = ', name=\'%s\'' % self._name if self._name else '' + vars_s = ', '.join(str(v) for v in self.iter_variables()) + repr_s = 'docplex.mp.SOSVariableSet(type={0}{1}{2})'.format(self.sos_type.value, vars_s, name_s) + return repr_s + + def copy(self, target_model, var_mapping): + copy_variables = [var_mapping[v] for v in self.iter_variables()] + return SOSVariableSet(model=target_model, + variable_sequence=copy_variables, + sos_type=self.sos_type, + weights=self._weights, + name=self.name) + + @property + def benders_annotation(self): + """ + This property is used to get or set the Benders annotation of a SOS variable set. + The value of the annotation must be a positive integer + + """ + return self.get_benders_annotation() + + @benders_annotation.setter + def benders_annotation(self, new_anno): + self.set_benders_annotation(new_anno)
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/vartype.html b/docs/2.24.232/mp/_modules/docplex/mp/vartype.html new file mode 100644 index 0000000..1470c88 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/vartype.html @@ -0,0 +1,421 @@ + + + + + + + + + docplex.mp.vartype — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.vartype

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2022
+# --------------------------------------------------------------------------
+
+from docplex.mp.error_handler import docplex_fatal
+
+
[docs]class VarType(object): + """VarType() + + This abstract class is the parent class for all types of decision variables. + + This class must never be instantiated. + Specialized sub-classes are defined for each type of decision variable. + + """ + def __init__(self, short_name, lb, ub, cplex_typecode): + self._short_name = short_name + self._lb = lb + self._ub = ub + self._cpx_typecode = cplex_typecode + + def is_semi_type(self): + return False + + @property + def cplex_typecode(self): + """ This property returns the CPLEX type code for this type. + Possible values are: + + - 'B' for binary type + - 'I' for integer type + - 'C' for continuous type + - 'S' for semicontinuous type + - 'N' for semiinteger type + + :return: a one-letter string. + """ + return self._cpx_typecode + + @property + def short_name(self): + """ This property returns a short name string for the type. + """ + return self._short_name + + @property + def default_lb(self): + """ This property returns the default lower bound for the type. + """ + return self._lb + + @property + def default_ub(self): + """ This property returns the default upper bound for the type. + """ + return self._ub + + def resolve_lb(self, candidate_lb, logger): + if candidate_lb is None: + resolved_lb = self._lb + else: + resolved_lb = self._compute_lb(candidate_lb, logger) + return resolved_lb + + def resolve_ub(self, candidate_ub, logger): + if candidate_ub is None: + resolved_ub = self._ub + else: + resolved_ub = self._compute_ub(candidate_ub, logger) + return resolved_ub + + def _compute_lb(self, candidate_lb, logger): # pragma: no cover + # INTERNAL + raise NotImplementedError + + def _compute_ub(self, candidate_ub, logger): # pragma: no cover + # INTERNAL + raise NotImplementedError + +
[docs] def is_discrete(self): + """ Checks if this is a discrete type. + + Returns: + Boolean: True if the type is a discrete type. + """ + raise NotImplementedError # pragma: no cover
+ + def accept_value(self, numeric_value, tolerance=1e-6): + raise NotImplementedError # pragma: no cover + + @classmethod + def _is_within_bounds_and_tolerance(cls, candidate_value, lb, ub, tolerance): + assert tolerance >= 0 + if candidate_value < lb - tolerance: + res = False + elif candidate_value > ub + tolerance: + res = False + else: + res = True + return res + + @classmethod + def _is_int_within_tolerance(cls, candidate_value, tolerance): + assert tolerance >= 0 + return abs(candidate_value - round(candidate_value)) <= tolerance + + def accept_domain_value(self, candidate_value, lb, ub, tolerance): + return self.accept_value(candidate_value, tolerance) and\ + self._is_within_bounds_and_tolerance(candidate_value, lb, ub, tolerance) + +
[docs] def to_string(self): + """ + Returns: + string: A string representation of the type. + """ + return "VarType_%s" % self.short_name
+ + def __str__(self): + return self.to_string() + + def __eq__(self, other): + return type(other) == type(self) + + def __ne__(self, other): + return type(other) != type(self) + + def _hash_vartype(self): # pragma: no cover + return hash(self.cplex_typecode)
+ + +
[docs]class BinaryVarType(VarType): + """BinaryVarType() + + This class models the binary variable type and + is not meant to be instantiated. Each model contains one instance of + this type. + """ + + def __init__(self): + VarType.__init__(self, short_name="binary", lb=0, ub=1, cplex_typecode='B') + + def _compute_lb(self, candidate_lb, logger): + # INTERNAL + if candidate_lb >= 1 + 1e-6: + logger.fatal('Lower bound for binary variable should be less than 1, {0} was passed '.format(candidate_lb)) + # return the user bound anyway + return candidate_lb + + def _compute_ub(self, candidate_ub, logger): + # INTERNAL + if candidate_ub <= -1e-6: + logger.fatal('Upper bound for binary variable should be greater than 0, {0} was passed'.format(candidate_ub)) + # return the user bound anyway + return candidate_ub + +
[docs] def is_discrete(self): + """ Checks if this is a discrete type. + + Returns: + Boolean: True as this is a discrete type. + """ + return True
+ + def accept_value(self, numeric_value, tolerance=1e-6): + return -tolerance <= numeric_value <= tolerance or\ + (1-tolerance <= numeric_value <= 1 + tolerance) + + def __hash__(self): # pragma: no cover + return VarType._hash_vartype(self)
+ + +
[docs]class ContinuousVarType(VarType): + """ContinuousVarType() + + This class models the continuous variable type and + is not meant to be instantiated. Each model contains one instance of this type. + """ + + def __init__(self, plus_infinity=1e+20): + VarType.__init__(self, short_name="continuous", lb=0, ub=plus_infinity, cplex_typecode='C') + self._plus_infinity = plus_infinity + self._minus_infinity = - plus_infinity + + def _compute_ub(self, candidate_ub, logger): + return min(candidate_ub, self._plus_infinity) + + def _compute_lb(self, candidate_lb, logger): + return max(candidate_lb, self._minus_infinity) + +
[docs] def is_discrete(self): + """ Checks if this is a discrete type. + + Returns: + Boolean: False because this type is not a discrete type. + """ + return False
+ + def accept_value(self, numeric_value, tolerance=1e-6): + return self._minus_infinity <= numeric_value <= self._plus_infinity + + def __hash__(self): # pragma: no cover + return VarType._hash_vartype(self)
+ + +
[docs]class IntegerVarType(VarType): + """IntegerVarType() + This class models the integer variable type and + is not meant to be instantiated. Each models contains one instance + of this type. + + """ + + def __init__(self, plus_infinity=1e+20): + VarType.__init__(self, short_name="integer", lb=0, ub=plus_infinity, cplex_typecode='I') + self._plus_infinity = plus_infinity + self._minus_infinity = -plus_infinity + + def _compute_ub(self, candidate_ub, logger): + return min(candidate_ub, self._plus_infinity) + + def _compute_lb(self, candidate_lb, logger): + return max(candidate_lb, self._minus_infinity) + +
[docs] def is_discrete(self): + """ Checks if this is a discrete type. + + Returns: + Boolean: True as this is a discrete type. + """ + return True
+ + def accept_value(self, numeric_value, tolerance=1e-6): + return self._is_int_within_tolerance(numeric_value, tolerance) + + def __hash__(self): # pragma: no cover + return VarType._hash_vartype(self)
+ + +
[docs]class SemiContinuousVarType(VarType): + """SemiContinuousVarType() + + This class models the :index:`semi-continuous` variable type and + is not meant to be instantiated. + """ + + def __init__(self, plus_infinity=1e+20): + VarType.__init__(self, short_name="semi-continuous", lb=1e-6, ub=plus_infinity, cplex_typecode='S') + self._plus_infinity = plus_infinity + + def is_semi_type(self): + return True + + def _compute_ub(self, candidate_ub, logger): + return self._plus_infinity if candidate_ub >= self._plus_infinity else float(candidate_ub) + + def _compute_lb(self, candidate_lb, logger): + if candidate_lb <= 0: + logger.fatal( + 'semi-continuous variable expects strict positive lower bound, not: {0}'.format(candidate_lb)) + return candidate_lb + + @property + def default_lb(self): + # there is NO default lb + docplex_fatal("Type {0} has no default lower bound".format(self.short_name)) + +
[docs] def is_discrete(self): + """ Checks if this is a discrete type. + + Returns: + Boolean: False because this type is not a discrete type. + """ + return False
+ + def accept_value(self, numeric_value, tolerance=1e-6): + return 0 <= numeric_value <= self._plus_infinity + + def accept_domain_value(self, candidate_value, lb, ub, tolerance): + return 0 == candidate_value or self._is_within_bounds_and_tolerance(candidate_value, lb, ub, tolerance) + + def __hash__(self): # pragma: no cover + return VarType._hash_vartype(self)
+ + +
[docs]class SemiIntegerVarType(VarType): + """SemiIntegerVarType() + + This class models the :index:`semi-integer` variable type and + is not meant to be instantiated. + """ + def __init__(self, plus_infinity=1e+20): + VarType.__init__(self, short_name="semi-integer", lb=1e-6, ub=plus_infinity, cplex_typecode='N') + self._plus_infinity = plus_infinity + + def is_semi_type(self): + return True + + @property + def default_lb(self): + # there is NO default lb + docplex_fatal("Type {0} has no default lower bound".format(self.short_name)) + + def _compute_ub(self, candidate_ub, logger): + return min(candidate_ub, self._plus_infinity) + + def _compute_lb(self, candidate_lb, logger): + if candidate_lb <= 0: + logger.fatal('semi-integer variable expects strict positive lower bound, not: {0}'.format(candidate_lb)) + return candidate_lb + +
[docs] def is_discrete(self): + """ Checks if this is a discrete type. + + Returns: + Boolean: True because this type is an integer type. + """ + return True
+ + def accept_value(self, numeric_value, tolerance=1e-6): + if 0 == numeric_value: + return True + return numeric_value >= 0 and self._is_int_within_tolerance(numeric_value, tolerance) + + def accept_domain_value(self, candidate_value, lb, ub, tolerance): + return 0 == candidate_value or self._is_within_bounds_and_tolerance(candidate_value, lb, ub, tolerance) + + def __hash__(self): # pragma: no cover + return VarType._hash_vartype(self)
+ + + + +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/mp/with_funcs.html b/docs/2.24.232/mp/_modules/docplex/mp/with_funcs.html new file mode 100644 index 0000000..49343d0 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/mp/with_funcs.html @@ -0,0 +1,219 @@ + + + + + + + + + docplex.mp.with_funcs — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.mp.with_funcs

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2020, 2022
+# --------------------------------------------------------------------------
+
+from contextlib import contextmanager
+
+from docplex.mp.constants import ObjectiveSense
+
+
+
[docs]@contextmanager +def model_parameters(mdl, temp_parameters): + """ This contextual function is used to override a model's parameters. + As a contextual function, it is intended to be used with the `with` construct, for example: + + >>> with model_parameters(mdl, {"timelimit": 30, "empahsis.mip": 4}) as mdl2: + >>> mdl2.solve() + + + The new model returned from the `with` has temporary parameters overriding those of the initial model. + + when exiting the with block, initial parameters are restored. + + :param mdl: an instance of `:class:Model`. + :param temp_parameters: accepts either a dictionary of qualified names to values, for example + {"mip.tolernaces.mipgap": 0.03, "emphasis.mip": 4}, or a dictionary from parameter objects to values. + :return: the same model, with overridden parameters. + + See Also: + - :func:`docplex.mp.params.Parameter.qualified_name` + + *New in version 2.21* + """ + if not temp_parameters: + try: + yield mdl + finally: + pass + else: + ctx = mdl.context + saved_context = ctx + temp_ctx = ctx.copy() + try: + temp_ctx.update_cplex_parameters(temp_parameters) + mdl.context = temp_ctx + yield mdl + finally: + mdl.context = saved_context + return mdl
+ + +
[docs]@contextmanager +def model_objective(mdl, temp_obj, temp_sense=None): + """ This contextual function is used to temporarily override the objective of a model. + As a contextual function, it is intended to be used with the `with` construct, for example: + + >>> with model_objective(mdl, x+y) as mdl2: + >>> mdl2.solve() + + + The new model returned from the `with` has a temporary objective overriding the initial objective. + + when exiting the with block, the initial objective and sense are restored. + + :param mdl: an instance of `:class:Model`. + :param temp_obj: an expression. + :param temp_sense: an optional objective sense to override the model's. Default is None (keep same objective). + Accepts either an instance of enumerated value `:class:docplex.mp.constants.ObjectiveSense` or a string + 'min' or 'max'. + :return: the same model, with overridden objective. + + *New in version 2.21* + """ + saved_obj = mdl.objective_expr + saved_sense = mdl.objective_sense + new_sense_ = ObjectiveSense.parse(temp_sense, mdl) if temp_sense is not None else None + + try: + mdl.set_objective_expr(temp_obj) + if new_sense_: + mdl.set_objective_sense(new_sense_) + + yield mdl + finally: + mdl.set_objective_expr(saved_obj) + if new_sense_: + mdl.set_objective_sense(saved_sense)
+ + +
[docs]@contextmanager +def model_solvefixed(mdl): + """ This contextual function is used to temporarily change the type of the model + to "solveFixed". + As a contextual function, it is intended to be used with the `with` construct, for example: + + >>> with model_solvefixed(mdl) as mdl2: + >>> mdl2.solve() + + The model returned from the `with` has a temporary problem type set to "solveFixex overriding the + actual problem type. + This function is useful for MIP models which have been successfully solved; the modified model + can be solved as a LP, with all discrete values fixed to their solutions in the previous solve. + + when exiting the with block, the actual problem type is restored. + + :param mdl: an instance of `:class:Model`. + + :return: the same model, with overridden problem type. + + Note: + - an exception is raised if the model has not been solved + - LP models are returned unchanged, as this mfunction has no use. + + *New in version 2.22* + """ + cpx = mdl._get_cplex(do_raise=True, msgfn=lambda: "model_solvefixed requires CPLEX runtime") + + # save initial problem type, to be restored. + saved_problem_type = cpx.get_problem_type() + if saved_problem_type == 0: + mdl.warning("Model {0} is a LP model, solvefixed does nothing".format(mdl.name)) + return mdl + + if mdl.solution is None: + # a solution is required. + mdl.fatal(f"model_solvefixed requires that the model has been solved successfully") + try: + cpx.set_problem_type(3) # 3 is constant fixed_MILP + yield mdl + finally: + cpx.set_problem_type(saved_problem_type)
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/util/csv_utils.html b/docs/2.24.232/mp/_modules/docplex/util/csv_utils.html new file mode 100644 index 0000000..b814501 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/util/csv_utils.html @@ -0,0 +1,146 @@ + + + + + + + + + docplex.util.csv_utils — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.util.csv_utils

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2020
+# --------------------------------------------------------------------------
+'''Some csv utilities
+'''
+import os
+
+
+
[docs]def encode_csv_string(text): + """ Encode a string to be used in CSV file + + Args: + text: String to encode + Returns: + Encoded string, including starting and ending double quote + """ + res = ['"'] + for c in text: + res.append(c) + if c == '"': + res.append('"') + res.append('"') + return ''.join(res)
+ + +def write_csv_line(output, line, encoding): + line = ','.join([encode_csv_string('%s' % c) for c in line]) + output.write(line.encode(encoding)) + output.write('\n'.encode(encoding)) + + +def write_csv(env, table, fields, name): + # table must be a named tuple + encoding = 'utf-8' + with env.get_output_stream(name) as ostr: + write_csv_line(ostr, fields, encoding) + for line in table: + write_csv_line(ostr, line, encoding) + + +
[docs]def write_table_as_csv(env, table, name, field_names): + '''Writes a kpis dataframe as file which name is specified. + The data type depends of extension of name. + + This uses the specfied env to write data as attachments + ''' + _, ext = os.path.splitext(name) + ext = ext.lower() + if ext == '.csv': + encoding = 'utf-8' + with env.get_output_stream(name) as ostr: + write_csv_line(ostr, field_names, encoding) + for line in table: + write_csv_line(ostr, line, encoding) + else: + # right now, only csv is supported + raise ValueError('file format not supported for KPIs file: %s' % ext)
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/util/environment.html b/docs/2.24.232/mp/_modules/docplex/util/environment.html new file mode 100644 index 0000000..46a30bf --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/util/environment.html @@ -0,0 +1,1251 @@ + + + + + + + + + docplex.util.environment — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.util.environment

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2022
+# --------------------------------------------------------------------------
+
+'''
+Representation of the DOcplex solving environment.
+
+This module handles the various elements that allow an
+optimization program to run independently from the solving environment.
+This environment may be:
+
+ * on premise, using a local version of CPLEX Optimization Studio to solve MP problems, or
+ * on DOcplexcloud, with the Python program running inside the Python Worker.
+ * on Decision Optimization in Watson Machine Learning.
+
+As much as possible, the adaptation to the solving environment is
+automatic. The functions that are presented here are useful for handling
+very specific use cases.
+
+The following code is a program that sums its input (``sum.py``)::
+
+    import json
+    import docplex.util.environment as environment
+
+    sum = 0
+    # open program input named "data.txt" and sum the contents
+    with environment.get_input_stream("data.txt") as input:
+        for i in input.read().split():
+            sum += int(i)
+    # write the result as a simple json in program output "solution.json"
+    with environment.get_output_stream("solution.json") as output:
+        output.write(json.dumps({'result': sum}))
+
+Let's put some data in a ``data.txt`` file::
+
+    4 7 8
+    19
+
+When you run ``sum.py`` with a Python interpreter, it opens the ``data.txt`` file and sums all of the integers
+in that file. The result is saved as a JSON fragment in file ``solution.json``::
+
+    $ python sum.py
+    $ more solution.json
+    {"result": 38}
+
+Environment representation can be accessed with different ways:
+
+    * direct object method calls, after retrieving an instance using
+      :meth:`docplex.util.environment.get_environment` and using methods of
+      :class:`docplex.util.environment.Environment`.
+    * using the function in package `docplex.util.environment`. They will call
+       the corresponding methods of Environment in the platform
+       `default_environment`:
+
+           * :meth:`docplex.util.environment.get_input_stream`
+           * :meth:`docplex.util.environment.get_output_stream`
+           * :meth:`docplex.util.environment.read_df`
+           * :meth:`docplex.util.environment.write_df`
+           * :meth:`docplex.util.environment.get_available_core_count`
+           * :meth:`docplex.util.environment.get_parameter`
+           * :meth:`docplex.util.environment.update_solve_details`
+           * :meth:`docplex.util.environment.add_abort_callback`
+           * :meth:`docplex.util.environment.remove_abort_callback`
+
+'''
+from collections import deque
+import json
+from functools import partial
+import logging
+import os
+import shutil
+import sys
+import tempfile
+import threading
+import time
+import uuid
+import warnings
+
+try:
+    from string import maketrans, translate
+except ImportError:
+    maketrans = str.maketrans
+    translate = str.translate
+
+try:
+    import pandas
+except ImportError:
+    pandas = None
+
+from six import iteritems
+
+from docplex.util import lazy
+from docplex.util.logging_utils import LoggerToDocloud
+from docplex.util.csv_utils import write_table_as_csv
+
+from .ws.util import START_SOLVE_EVENT, END_SOLVE_EVENT, Tracker
+
+in_ws_nb = None
+
+if in_ws_nb is None:
+    in_notebook = ('ipykernel' in sys.modules)
+    dsx_home_set = 'dsxuser' in os.environ.get('HOME', '').split('/')
+    has_hw_spec = 'RUNTIME_HARDWARE_SPEC' in os.environ
+    rt_region_set = 'RUNTIME_ENV_REGION' in os.environ
+
+    in_ws_nb = in_notebook and dsx_home_set and has_hw_spec and rt_region_set
+
+
+log_level_mapping = {'OFF': None,
+                     'SEVERE': logging.ERROR,
+                     'WARNING': logging.WARNING,
+                     'INFO': logging.INFO,
+                     'CONFIG': logging.INFO,
+                     'FINE': logging.DEBUG,
+                     'FINER': logging.DEBUG,
+                     'FINEST': logging.DEBUG,
+                     'ALL': logging.DEBUG}
+
+
+
[docs]class NotAvailableError(Exception): + ''' The exception raised when a feature is not available + ''' + pass
+ + +
[docs]def default_solution_storage_handler(env, solution): + ''' The default solution storage handler. + + The storage handler is a function which first argument is the + :class:`~Environment` on which a solution should be saved. The `solution` + is a dict containing all the data for an optimization solution. + + The storage handler is responsible for storing the solution in the + environment. + + For each (key, value) pairs of the solution, the default solution storage + handler does the following depending of the type of `value`, in the + following order: + + * If `value` is a `pandas.DataFrame`, then the data frame is saved + as an output with the specified `name`. Note that `name` must include + an extension file for the serialization. See + :meth:`Environment.write_df` for supported formats. + * If `value` is a `bytes`, it is saved as binary data with `name`. + * The `value` is saved as an output with the `name`, after it has been + converted to JSON. + + Args: + env: The :class:`~Environment` + solution: a dict containing the solution. + ''' + for (name, value) in iteritems(solution): + if pandas and isinstance(value, pandas.DataFrame): + _, ext = os.path.splitext(name) + if ext.lower() == '': + name = '%s.csv' % name # defaults to csv if no format specified + env.write_df(value, name) + elif isinstance(value, bytes): + with env.get_output_stream(name) as fp: + fp.write(value) + else: + # try jsonify + with env.get_output_stream(name) as fp: + json.dump(value, fp)
+ + +# The global output lock +global_output_lock = threading.Lock() + + +
[docs]class SolveDetailsFilter(object): + '''Default solve detail filter class. + + This default class filters details so that there are no more than 1 solve + details per second. + ''' + def __init__(self, interval=1): + self.last_accept_time = 0 + self.interval = interval + +
[docs] def filter(self, details): + '''Filters the details. + + Returns: + True if the details are to be published. + ''' + ret_val = None + now = time.time() + if (now - self.last_accept_time > self.interval): + ret_val = details + self.last_accept_time = now + return ret_val
+ + +
[docs]class Environment(object): + ''' Methods for interacting with the execution environment. + + Internally, the ``docplex`` package provides the appropriate implementation + according to the actual execution environment. + The correct instance of this class is returned by the method + :meth:`docplex.util.environment.get_environment` that is provided in this + module. + + Attributes: + abort_callbacks: A list of callbacks that are called when the script is + run on DOcplexcloud and a job abort operation is requested. You + add your own callback using:: + + env.abort_callbacks += [your_cb] + + or:: + + env.abort_callbacks.append(your_cb) + + You remove a callback using:: + + env.abort_callbacks.remove(your_cb) + + solution_storage_handler: A function called when a solution is to be + stored. The storage handler is a function which first argument is + the :class:`~Environment` on which a solution should be saved. The + `solution` is a dict containing all the data for an optimization + solution. The default is :meth:`~default_solution_storage_handler`. + record_history_fields: Fields which history is to be kept + record_history_size: maximum number of records in history + record_interval: min time between to history records + ''' + def __init__(self): + self.output_lock = global_output_lock + self.solution_storage_handler = default_solution_storage_handler + self.abort_callbacks = [] + self.interpret_df_type = True + self.update_solve_details_dict = True + self.last_solve_details = {} # stores the latest published details + # private behaviour for now: allows to filter details + # the SolveDetailsFilter.filter() method returns true if the details + # are to be kept + self.details_filter = None + self.unpublished_details = None + self._record_history_fields = None + # self.record_history_fields = ['PROGRESS_CURRENT_OBJECTIVE'] + self.record_history = {} # maps name -> deque + self.last_history_record = {} # we keep the last here so that we can publish at end of solve + self.record_history_time_decimals = 2 # number of decimals for time + self.record_history_size = 100 + self.record_min_time = 1 + self.recorded_solve_details_count = 0 # number of solve details that have been sent to recording + self.autoreset = True + self.logger = logging.getLogger("docplex.util.environment.logger") + + def _reset_record_history(self, force=False): + if self.autoreset or force: + self.record_history = {} + self.unpublished_details = None + self.last_history_record = {} + self.recorded_solve_details_count = 0 + + def get_record_history_fields(self): + if self._record_history_fields is None: + if self.is_dods(): + self._record_history_fields = ['PROGRESS_BEST_OBJECTIVE', + 'PROGRESS_CURRENT_OBJECTIVE', + 'PROGRESS_GAP'] + else: + # the default out of dods is to not record any history + self._record_history_fields = [] + return self._record_history_fields + + def set_record_history_fields(self, value): + self._record_history_fields = value + + # let record_history_fields be a property that is lazy initialized + # this gives the opportunity to set is_dods before record history fields are needed + record_history_fields = property(get_record_history_fields, set_record_history_fields) + +
[docs] def store_solution(self, solution): + '''Stores the specified solution. + + This method guarantees that the solution is fully saved if the model + is running on DOcplexcloud python worker and an abort of the job is + triggered. + + For each (key, value) pairs of the solution, the default solution + storage handler does the following depending of the type of `value`, in + the following order: + + * If `value` is a `pandas.DataFrame`, then the data frame is saved + as an output with the specified `name`. Note that `name` must include + an extension file for the serialization. See + :meth:`Environment.write_df` for supported formats. + * If `value` is a `bytes`, it is saved as binary data with `name`. + * The `value` is saved as an output with the `name`, after it has been + converted to JSON. + + Args: + solution: a dict containing the solution. + ''' + with self.output_lock: + self.solution_storage_handler(self, solution)
+ +
[docs] def get_input_stream(self, name): + ''' Get an input of the program as a stream (file-like object). + + An input of the program is a file that is available in the working directory. + + When run on DOcplexcloud, all input attachments are copied to the working directory before + the program is run. ``get_input_stream`` lets you open the input attachments of the job. + + Args: + name: Name of the input object. + Returns: + A file object to read the input from. + ''' + self.logger.debug(lazy(lambda: f"set input stream: name={name}")) + return None
+ +
[docs] def read_df(self, name, reader=None, **kwargs): + ''' Reads an input of the program as a ``pandas.DataFrame``. + + ``pandas`` must be installed. + + ``name`` is the name of the input object, as a filename. If a reader + is not user provided, the reader used depends on the filename extension. + + The default reader used depending on extension are: + + * ``.csv``: ``pandas.read_csv()`` + * ``.msg``: ``pandas.read_msgpack()`` + + Args: + name: The name of the input object + reader: an optional reader function + **kwargs: additional parameters passed to the reader + Raises: + NotAvailableError: raises this error when ``pandas`` is not + available. + ''' + if pandas is None: + raise NotAvailableError('read_df() is only available if pandas is installed') + _, ext = os.path.splitext(name) + default_kwargs = None + if reader is None: + default_readers = {'.csv': (pandas.read_csv, {'index_col': 0}), + '.msg': (pandas.read_msgpack, None)} + reader, default_kwargs = default_readers.get(ext.lower(), None) + if reader is None: + raise ValueError('no default reader defined for files with extension: \'%s\'' % ext) + with self.get_input_stream(name) as ost: + # allow + params = {} + if default_kwargs: + params.update(default_kwargs) + if kwargs: + params.update(kwargs) + return reader(ost, **params)
+ +
[docs] def write_df(self, df, name, writer=None, **kwargs): + ''' Write a ``pandas.DataFrame`` as an output of the program. + + ``pandas`` must be installed. + + ``name`` is the name of the input object, as a filename. If a writer + is not user provided, the writer used depends on the filename extension. + + This currently only supports csv output. + + Args: + name: The name of the input object + writer: an optional writer function + **kwargs: additional parameters passed to the writer + Raises: + NotAvailableError: raises this error when ``pandas`` is not + available. + ''' + if pandas is None: + raise NotAvailableError('write_df() is only available if pandas is installed') + _, ext = os.path.splitext(name) + if writer is None: + try: + default_writers = {'.csv': df.to_csv} + writer = default_writers.get(ext.lower(), None) + except AttributeError: + raise NotAvailableError('Could not write writer function for extension: %s' % ext) + if writer is None: + raise ValueError('no default writer defined for files with extension: \'%s\'' % ext) + with self.get_output_stream(name) as ost: + if sys.version_info[0] < 3: + ost.write(writer(index=False, encoding='utf8')) + else: + ost.write(writer(index=False).encode(encoding='utf8'))
+ +
[docs] def set_output_attachment(self, name, filename): + '''Attach the file which filename is specified as an output of the + program. + + The file is recorded as being part of the program output. + This method can be called multiple times if the program contains + multiple output objects. + + When run on premise, ``filename`` is copied to the the working + directory (if not already there) under the name ``name``. + + When run on DOcplexcloud, the file is attached as output attachment. + + Args: + name: Name of the output object. + filename: The name of the file to attach. + ''' + self.logger.debug(lazy(lambda: f"set output attachment: name={name}, filename={filename}"))
+ +
[docs] def get_output_stream(self, name): + ''' Get a file-like object to write the output of the program. + + The file is recorded as being part of the program output. + This method can be called multiple times if the program contains + multiple output objects. + + When run on premise, the output of the program is written as files in + the working directory. When run on DOcplexcloud, the files are attached + as output attachments. + + The stream is opened in binary mode, and will accept 8 bits data. + + Args: + name: Name of the output object. + Returns: + A file object to write the output to. + ''' + self.logger.debug(lazy(lambda: f"set output stream: name={name}")) + return open(os.devnull, "w+b")
+ +
[docs] def get_available_core_count(self): + ''' Returns the number of cores available for processing if the environment + sets a limit. + + This number is used in the solving engine as the number of threads. + + Returns: + The available number of cores or ``None`` if the environment does not + limit the number of cores. + ''' + return None
+ +
[docs] def get_parameters(self): + ''' Returns a dict containing all parameters of the program. + + On DOcplexcloud, this method returns the job parameters. + On local solver, this method returns ``os.environ``. + + Returns: + The job parameters + ''' + return None
+ +
[docs] def get_parameter(self, name): + ''' Returns a parameter of the program. + + On DOcplexcloud, this method returns the job parameter whose name is specified. + On local solver, this method returns the environment variable whose name is specified. + + Args: + name: The name of the parameter. + Returns: + The parameter whose name is specified or None if the parameter does + not exists. + ''' + return None
+ + def notify_start_solve(self, solve_details, engine_type=None): + # =============================================================================== + # '''Notify the solving environment that a solve is starting. + # + # If ``context.solver.auto_publish.solve_details`` is set, the underlying solver will automatically + # send details. If you want to craft and send your own solve details, you can use the following + # keys (non exhaustive list): + # + # - MODEL_DETAIL_TYPE : Model type + # - MODEL_DETAIL_CONTINUOUS_VARS : Number of continuous variables + # - MODEL_DETAIL_INTEGER_VARS : Number of integer variables + # - MODEL_DETAIL_BOOLEAN_VARS : Number of boolean variables + # - MODEL_DETAIL_INTERVAL_VARS : Number of interval variables + # - MODEL_DETAIL_SEQUENCE_VARS : Number of sequence variables + # - MODEL_DETAIL_NON_ZEROS : Number of non zero variables + # - MODEL_DETAIL_CONSTRAINTS : Number of constraints + # - MODEL_DETAIL_LINEAR_CONSTRAINTS : Number of linear constraints + # - MODEL_DETAIL_QUADRATIC_CONSTRAINTS : Number of quadratic constraints + # + # Args: + # solve_details: A ``dict`` with solve details as key/value pairs + # See: + # :attr:`.Context.solver.auto_publish.solve_details` + # ''' + # =============================================================================== + self.logger.debug(lazy(lambda: f"Notify start solve: engine_type={engine_type}, solve_details={json.dumps(solve_details, indent=3)}")) + self._reset_record_history() + +
[docs] def update_solve_details(self, details): + '''Update the solve details. + + You use this method to send solve details to the solve service. + If ``context.solver.auto_publish`` is set, the underlying + solver will automatically update solve details once the solve has + finished. + + This method might filter details and publish them with rate limitations. + It actually publish de details by calling `publish_solve_details`. + + Args: + details: A ``dict`` with solve details as key/value pairs. + ''' + self.logger.debug(lazy(lambda: f"Update solve details: {json.dumps(details, indent=3)}")) + # publish details + to_publish = None + if self.update_solve_details_dict: + previous = self.last_solve_details + to_publish = {} + if details: + to_publish.update(previous) + to_publish.update(details) + self.last_solve_details = to_publish + else: + to_publish = details + # process history + to_publish = self.record_in_history(to_publish) + + if self.details_filter: + if self.details_filter.filter(details): + self.logger.debug("Published as filtered details") + self.publish_solve_details(to_publish) + else: + # just store the details for later use + self.logger.debug("Publish filter refused details, stored as unpublished") + self.unpublished_details = to_publish + else: + self.logger.debug("Published as unfiltered details") + self.publish_solve_details(to_publish)
+ + def record_in_history(self, details): + self.recorded_solve_details_count += 1 + self.logger.debug(lazy(lambda: f"record in history: {json.dumps(details)}")) + for f in self.record_history_fields: + if f in details: + current_ts = round(time.time(), self.record_history_time_decimals) + current_history_element = [current_ts, details[f]] + l = self.record_history.get(f, deque([], self.record_history_size)) + self.record_history[f] = l + last_ts = l[-1][0] if len(l) >= 1 else -9999 + if (current_ts - last_ts) >= self.record_min_time: + self.logger.debug(lazy(lambda: f"record added in history for field {f}")) + l.append(current_history_element) + details['%s.history' % f] = json.dumps(list(l)) # make new copy + # make current also last history record + self.last_history_record[f] = current_history_element + else: + self.logger.debug(lazy(lambda: f"record stored as current for field {f}")) + self.last_history_record[f] = current_history_element + return details + + def prepare_last_history(self): + details = {} + details.update(self.last_solve_details) + any_added = False + for k, v in self.last_history_record.items(): + the_list = self.record_history[k] + do_append = True + if len(the_list) >= 1: + last_date_history = the_list[-1][0] + last_date = v[0] + do_append = (abs(last_date - last_date_history) >= 0.01) + if do_append: + the_list.append(v) + any_added = True + details['%s.history' % k] = json.dumps(list(the_list)) + return details if any_added else False + +
[docs] def publish_solve_details(self, details): + '''Actually publish the solve specified details. + + Returns: + The published details + ''' + self.logger.debug(lazy(lambda: f"Publish solve details: {json.dumps(details, indent=3)}"))
+ + def notify_end_solve(self, status, solve_time=None): + # =============================================================================== + # '''Notify the solving environment that the solve as ended. + # + # The ``status`` can be a JobSolveStatus enum or an integer. + # + # When ``status`` is an integer, it is converted with the following conversion table: + # + # 0 - UNKNOWN: The algorithm has no information about the solution. + # 1 - FEASIBLE_SOLUTION: The algorithm found a feasible solution. + # 2 - OPTIMAL_SOLUTION: The algorithm found an optimal solution. + # 3 - INFEASIBLE_SOLUTION: The algorithm proved that the model is infeasible. + # 4 - UNBOUNDED_SOLUTION: The algorithm proved the model unbounded. + # 5 - INFEASIBLE_OR_UNBOUNDED_SOLUTION: The model is infeasible or unbounded. + # + # Args: + # status: The solve status + # solve_time: The solve time + # ''' + # =============================================================================== + self.logger.debug(f"Notify end solve, status={status}, solve_time={solve_time}") + if self.unpublished_details: + self.logger.debug("Notify end solve: has unpublished details, so publish them") + self.publish_solve_details(self.unpublished_details) + if self.recorded_solve_details_count >= 1 and self.last_history_record: + self.logger.debug("Notify end solve: has more than 1 solve details, prepare and publish history") + last_details = self.prepare_last_history() + if last_details: + self.publish_solve_details(last_details) + +
[docs] def set_stop_callback(self, cb): + '''Sets a callback that is called when the script is run on + DOcplexcloud and a job abort operation is requested. + + You can also use the ``stop_callback`` property to set the callback. + + Deprecated since 2.4 - Use self.abort_callbacks += [cb] instead' + + Args: + cb: The callback function + ''' + warnings.warn('set_stop_callback() is deprecated since 2.4 - Use Environment.abort_callbacks.append(cb) instead')
+ +
[docs] def get_stop_callback(self): + '''Returns the stop callback that is called when the script is run on + DOcplexcloud and a job abort operation is requested. + + You can also use the ``stop_callback`` property to get the callback. + + Deprecated since 2.4 - Use the abort_callbacks property instead') + + ''' + warnings.warn('get_stop_callback() is deprecated since 2.4 - Use the abort_callbacks property instead') + return None
+ + stop_callback = property(get_stop_callback, set_stop_callback) + +
[docs] def get_engine_log_level(self): + '''Returns the engine log level as set by job parameter oaas.engineLogLevel. + + oaas.engineLogLevel values are: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST, ALL + + The mapping to logging levels in python are: + + * OFF: None + * SEVERE: logging.ERROR + * WARNING: logging.WARNING + * INFO, CONFIG: logging.INFO + * FINE, FINER, FINEST: logging.DEBUG + * ALL: logging.DEBUG + + + All other values are considered invalid values and will return None. + + Returns: + The logging level or None if not set (off) + ''' + oaas_level = self.get_parameter('oaas.engineLogLevel') + log_level = log_level_mapping.get(oaas_level.upper(), None) if oaas_level else None + return log_level
+ +
[docs] def is_debug_mode(self): + '''Returns true if the engine should run in debug mode. + + This is equivalent to ``env.get_engine_log_level() <= logging.DEBUG`` + ''' + lvl = self.get_engine_log_level() + # logging.NOTSET is zero so will return false + return (self.get_engine_log_level() <= logging.DEBUG) if lvl is not None else False
+ +
[docs] def is_dods(self): + '''Returns true if this environment in running in DODS. + ''' + value = os.environ.get("IS_DODS") + return str(value).lower() == "true"
+ + +
[docs]class AbstractLocalEnvironment(Environment): + # The environment solving environment using all local input and outputs. + def __init__(self): + super(AbstractLocalEnvironment, self).__init__() + self.logger = logging.getLogger('docplex.util.environment.logger') + + # init number of cores. Default is no limits (engines will use + # number of cores reported by system). + # On Watson studio runtimes, the system reports the total number + # of physical cores but not the number of cores available to the + # runtime. The number of cores available to the runtime are + # specified in an environment variable instead. + self._available_cores = None + RUNTIME_HARDWARE_SPEC = os.environ.get('RUNTIME_HARDWARE_SPEC', None) + if RUNTIME_HARDWARE_SPEC: + try: + spec = json.loads(RUNTIME_HARDWARE_SPEC) + num = int(spec.get('num_cpu')) if ('num_cpu' in spec) else None + self._available_cores = num + except: + pass + +
[docs] def get_input_stream(self, name): + return open(name, "rb")
+ +
[docs] def get_output_stream(self, name): + return open(name, "wb")
+ +
[docs] def get_parameter(self, name): + return os.environ.get(name, None)
+ +
[docs] def get_parameters(self): + return os.environ
+ +
[docs] def set_output_attachment(self, name, filename): + # check that name leads to a file in cwd + attachment_abs_path = os.path.dirname(os.path.abspath(name)) + if attachment_abs_path != os.getcwd(): + raise ValueError('Illegal attachment name') + + if os.path.dirname(os.path.abspath(filename)) != os.getcwd(): + shutil.copyfile(filename, name) # copy to current
+ +
[docs] def get_available_core_count(self): + return self._available_cores
+ + +
[docs]class LocalEnvironment(AbstractLocalEnvironment): + def __init__(self): + super(LocalEnvironment, self).__init__()
+ + +
[docs]class WSNotebookEnvironment(AbstractLocalEnvironment): + def __init__(self, tracker=None): + super(WSNotebookEnvironment, self).__init__() + self._start_time = None + self.solve_id = str(uuid.uuid4()) # generate random uuid for each session + self.model_type = None # set in start solve + self.tracker = tracker if tracker else Tracker() + + def notify_start_solve(self, solve_details): + super(WSNotebookEnvironment, self).notify_start_solve(solve_details) + # Prepare data for WS + detail_type = solve_details.get('MODEL_DETAIL_TYPE', None) + model_type = "cpo" if detail_type and detail_type.startswith('CPO') else "cplex" + num_constraints = solve_details.get('MODEL_DETAIL_CONSTRAINTS', 0) + num_variables = solve_details.get('MODEL_DETAIL_CONTINUOUS_VARS', 0) \ + + solve_details.get('MODEL_DETAIL_INTEGER_VARS', 0) \ + + solve_details.get('MODEL_DETAIL_BOOLEAN_VARS', 0) \ + + solve_details.get('MODEL_DETAIL_INTERVAL_VARS', 0) \ + + solve_details.get('MODEL_DETAIL_SEQUENCE_VARS', 0) + model_statistics = {'numConstraints': num_constraints, + 'numVariables': num_variables} + cplex_edition = _get_cplex_edition() + details = {'modelType': model_type, + 'modelSize': model_statistics, + 'solveId': self.solve_id, + 'edition': cplex_edition} + self.model_type = model_type + self.tracker.notify_ws(START_SOLVE_EVENT, details) + self._start_time = time.time() + + def notify_end_solve(self, status, solve_time=None): + super(WSNotebookEnvironment, self).notify_end_solve(status, solve_time=solve_time) + # do the watson studio things + if (self._start_time and solve_time is None): + solve_time = (time.time() - self._start_time) + details = {'solveTime': solve_time, + 'modelType': self.model_type, + 'solveId': self.solve_id} + self.tracker.notify_ws(END_SOLVE_EVENT, details) + self._start_time = None
+ + +class OutputFileWrapper(object): + # Wraps a file object so that on __exit__() and on close(), the wrapped file is closed and + # the output attachments are actually set in the worker + def __init__(self, file, solve_hook, attachment_name): + self.file = file + self.solve_hook = solve_hook + self.attachment_name = attachment_name + self.closed = False + + def __getattr__(self, name): + if name == 'close': + return self.my_close + else: + return getattr(self.file, name) + + def __enter__(self, *args, **kwargs): + return self.file.__enter__(*args, **kwargs) + + def __exit__(self, *args, **kwargs): + self.file.__exit__(*args, **kwargs) + self.close() + + def close(self): + # actually close the output then set attachment + if not self.closed: + self.file.close() + self.solve_hook.set_output_attachments({self.attachment_name: self.file.name}) + self.closed = True + + +def worker_env_stop_callback(env): + # wait for the output lock to be released to make sure that the latest + # solution store operation has ended. + with env.output_lock: + pass + # call all abort callbacks + for cb in env.abort_callbacks: + cb() + + +
[docs]class WorkerEnvironment(Environment): + # The solving environment when we run in the DOcplexCloud worker. + def __init__(self, solve_hook): + super(WorkerEnvironment, self).__init__() + self.solve_hook = solve_hook + if solve_hook: + self.solve_hook.stop_callback = partial(worker_env_stop_callback, self) + self.logger = None + if hasattr(self.solve_hook, 'logger'): + self.logger = LoggerToDocloud(self.solve_hook.logger) + else: + self.logger = logging.getLogger('docplex.util.environment.logger') + +
[docs] def get_available_core_count(self): + return self.solve_hook.get_available_core_count()
+ +
[docs] def get_input_stream(self, name): + # inputs are in the current working directory + return open(name, "rb")
+ +
[docs] def get_output_stream(self, name): + # open the output in a place we know we can write + f = tempfile.NamedTemporaryFile(mode="w+b", delete=False) + return OutputFileWrapper(f, self.solve_hook, name)
+ +
[docs] def set_output_attachment(self, name, filename): + self.solve_hook.set_output_attachments({name: filename})
+ +
[docs] def get_parameter(self, name): + return self.solve_hook.get_parameter_value(name)
+ +
[docs] def get_parameters(self, ): + # This is a typo in _DockerSolveHook, this should be "parameters" + return self.solve_hook.parameter
+ +
[docs] def publish_solve_details(self, details): + super(WorkerEnvironment, self).publish_solve_details(details) + self.solve_hook.update_solve_details(details) + # if on dods, we want to publish stats.csv if any + if self.is_dods(): + self._publish_stats_csv(details)
+ + def _publish_stats_csv(self, stats): + # generate the stats.csv file with the specified stats + names = ['stats.csv'] + stats_table = [] + for k in stats: + if k.startswith("STAT."): + stats_table.append([k, stats[k]]) + if stats_table: + field_names = ['Name', 'Value'] + for name in names: + write_table_as_csv(self, stats_table, name, field_names) + + def notify_start_solve(self, solve_details): + super(WorkerEnvironment, self).notify_start_solve(solve_details) + self.solve_hook.notify_start_solve(None, # model + solve_details) + + def notify_end_solve(self, status, solve_time=None): + super(WorkerEnvironment, self).notify_end_solve(status) + try: + from docplex.util.status import JobSolveStatus + engine_status = JobSolveStatus(status) if status else JobSolveStatus.UNKNOWN + self.solve_hook.notify_end_solve(None, # model, unused + None, # has_solution, unused + engine_status, + None, # reported_obj, unused + None, # var_value_dict, unused + ) + except ImportError: + raise RuntimeError("This should have been called only when in a worker environment") + +
[docs] def set_stop_callback(self, cb): + warnings.warn('set_stop_callback() is deprecated since 2.4 - Use Environment.abort_callbacks.append(cb) instead') + self.abort_callbacks += [cb]
+ +
[docs] def get_stop_callback(self): + warnings.warn('get_stop_callback() is deprecated since 2.4 - Use the abort_callbacks property instead') + return self.abort_callbacks[1] if self.abort_callbacks else None
+ + +
[docs]class OverrideEnvironment(object): + '''Allows to temporarily replace the default environment. + + If the override environment is None, nothing happens and the default + environment is not replaced + ''' + def __init__(self, new_env=None): + self.set_env = new_env + self.saved_env = None + + def __enter__(self): + if self.set_env: + global default_environment + self.saved_env = default_environment + default_environment = self.set_env + else: + self.saved_env = None + + def __exit__(self, type, value, traceback): + if self.saved_env: + global default_environment + default_environment = self.saved_env
+ + +def _get_default_environment(): + # creates a new instance of the default environment + try: + import docplex.worker.solvehook as worker_env + hook = worker_env.get_solve_hook() + if hook: + return WorkerEnvironment(hook) + except ImportError: + pass + if in_ws_nb: + return WSNotebookEnvironment() + return LocalEnvironment() + + +default_environment = _get_default_environment() + + +def _get_cplex_edition(): + with OverrideEnvironment(Environment()): + import docplex.mp.model + import docplex.mp.environment + edition = " ce" if docplex.mp.model.Model.is_cplex_ce() else "" + version = docplex.mp.environment.Environment().cplex_version + return "%s%s" % (version, edition) + + +
[docs]def get_environment(): + ''' Returns the Environment object that represents the actual execution + environment. + + Note: the default environment is the value of the + ``docplex.util.environment.default_environment`` property. + + Returns: + An instance of the :class:`.Environment` class that implements methods + corresponding to actual execution environment. + ''' + return default_environment
+ + +
[docs]def get_input_stream(name): + ''' Get an input of the program as a stream (file-like object), + with the default environment. + + An input of the program is a file that is available in the working directory. + + When run on DOcplexcloud, all input attachments are copied to the working directory before + the program is run. ``get_input_stream`` lets you open the input attachments of the job. + + Args: + name: Name of the input object. + Returns: + A file object to read the input from. + ''' + return default_environment.get_input_stream(name)
+ + +
[docs]def set_output_attachment(name, filename): + ''' Attach the file which filename is specified as an output of the + program. + + The file is recorded as being part of the program output. + This method can be called multiple times if the program contains + multiple output objects. + + When run on premise, ``filename`` is copied to the the working + directory (if not already there) under the name ``name``. + + When run on DOcplexcloud, the file is attached as output attachment. + + Args: + name: Name of the output object. + filename: The name of the file to attach. + ''' + return default_environment.set_output_attachment(name, filename)
+ + +
[docs]def get_output_stream(name): + ''' Get a file-like object to write the output of the program. + + The file is recorded as being part of the program output. + This method can be called multiple times if the program contains + multiple output objects. + + When run on premise, the output of the program is written as files in + the working directory. When run on DOcplexcloud, the files are attached + as output attachments. + + The stream is opened in binary mode, and will accept 8 bits data. + + Args: + name: Name of the output object. + Returns: + A file object to write the output to. + ''' + return default_environment.get_output_stream(name)
+ + +
[docs]def read_df(name, reader=None, **kwargs): + ''' Reads an input of the program as a ``pandas.DataFrame`` with the + default environment. + + ``pandas`` must be installed. + + ``name`` is the name of the input object, as a filename. If a reader + is not user provided, the reader used depends on the filename extension. + + The default reader used depending on extension are: + + * ``.csv``: ``pandas.read_csv()`` + * ``.msg``: ``pandas.read_msgpack()`` + + Args: + name: The name of the input object + reader: an optional reader function + **kwargs: additional parameters passed to the reader + Raises: + NotAvailableError: raises this error when ``pandas`` is not + available. + ''' + return default_environment.read_df(name, reader=reader, **kwargs)
+ + +
[docs]def write_df(df, name, writer=None, **kwargs): + ''' Write a ``pandas.DataFrame`` as an output of the program with the + default environment. + + ``pandas`` must be installed. + + ``name`` is the name of the input object, as a filename. If a writer + is not user provided, the writer used depends on the filename extension. + + The default writer used depending on extension are: + + * ``.csv``: ``DataFrame.to_csv()`` + * ``.msg``: ``DataFrame.to_msgpack()`` + + Args: + name: The name of the input object + writer: an optional writer function + **kwargs: additional parameters passed to the writer + Raises: + NotAvailableError: raises this error when ``pandas`` is not + available. + ''' + return default_environment.write_df(df, name, writer=writer, **kwargs)
+ + +
[docs]def get_available_core_count(): + ''' Returns the number of cores available for processing if the environment + sets a limit, with the default environment. + + This number is used in the solving engine as the number of threads. + + Returns: + The available number of cores or ``None`` if the environment does not + limit the number of cores. + ''' + return default_environment.get_available_core_count()
+ + +
[docs]def get_parameter(name): + ''' Returns a parameter of the program, with the default environment. + + On DOcplexcloud, this method returns the job parameter whose name is specified. + + Args: + name: The name of the parameter. + Returns: + The parameter whose name is specified. + ''' + return default_environment.get_parameter(name)
+ + +
[docs]def update_solve_details(details): + '''Update the solve details, with the default environment + + You use this method to send solve details to the DOcplexcloud service. + If ``context.solver.auto_publish`` is set, the underlying + solver will automatically update solve details once the solve has + finished. + + Args: + details: A ``dict`` with solve details as key/value pairs. + ''' + return default_environment.update_solve_details(details)
+ + +
[docs]def add_abort_callback(cb): + '''Adds the specified callback to the default environment. + + The abort callback is called when the script is run on + DOcplexcloud and a job abort operation is requested. + + Args: + cb: The abort callback + ''' + default_environment.abort_callbacks += [cb]
+ + +
[docs]def remove_abort_callback(cb): + '''Adds the specified callback to the default environment. + + The abort callback is called when the script is run on + DOcplexcloud and a job abort operation is requested. + + Args: + cb: The abort callback + ''' + default_environment.abort_callbacks.remove(cb)
+ + +attachment_invalid_characters = '/\\?%*:|"#<> ' +attachment_trans_table = maketrans(attachment_invalid_characters, '_' * len(attachment_invalid_characters)) + + +
[docs]def make_attachment_name(name): + '''From `name`, create an attachment name that is correct for DOcplexcloud. + + Attachment filenames in DOcplexcloud has certain restrictions. A file name: + + - is limited to 255 characters; + - can include only ASCII characters; + - cannot include the characters `/\?%*:|"<>`, the space character, or the null character; and + - cannot include _ as the first character. + + This method replace all unauthorized characters with _, then removing leading + '_'. + + Args: + name: The original attachment name + Returns: + An attachment name that conforms to the restrictions. + Raises: + ValueError if the attachment name is more than 255 characters + ''' + new_name = translate(name, attachment_trans_table) + while (new_name.startswith('_')): + new_name = new_name[1:] + if len(new_name) > 255: + raise ValueError('Attachment names are limited to 255 characters') + return new_name
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/docplex/util/logging_utils.html b/docs/2.24.232/mp/_modules/docplex/util/logging_utils.html new file mode 100644 index 0000000..0351b87 --- /dev/null +++ b/docs/2.24.232/mp/_modules/docplex/util/logging_utils.html @@ -0,0 +1,165 @@ + + + + + + + + + docplex.util.logging_utils — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Source code for docplex.util.logging_utils

+# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2018
+# --------------------------------------------------------------------------
+'''This package provide simple logging facilities.
+'''
+import logging
+
+
+
[docs]class DocplexLogger(object): + '''Simple logger interface. + ''' +
[docs] def log(self, level, msg): + '''Logs a message with the specified level. + ''' + raise NotImplementedError('subclasses must override log()!')
+ +
[docs] def debug(self, msg): + '''Logs a message with level DEBUG. + ''' + self.log(logging.DEBUG, msg)
+ +
[docs] def info(self, msg): + '''Logs a message with level INFO. + ''' + self.log(logging.INFO, msg)
+ +
[docs] def warning(self, msg): + '''Logs a message with level WARNING. + ''' + self.log(logging.WARNING, msg)
+ +
[docs] def error(self, msg): + '''Logs a message with level ERROR. + ''' + self.log(logging.ERROR, msg)
+ +
[docs] def critical(self, msg): + '''Logs a message with level CRITICAL. + ''' + self.log(logging.CRITICAL, msg)
+ + +
[docs]class LoggerToDocloud(DocplexLogger): + '''This logger maps logs with python style logging levels to a docplexcloud + logger expecting java style logging levels. + ''' + def __init__(self, docloudlogger): + super(LoggerToDocloud, self).__init__() + self.docloudlogger = docloudlogger + +
[docs] def log(self, level, msg): + # fix for https://github.ibm.com/IBMDecisionOptimization/docplex/issues/312 + # => force msg to be a `str` before it goes on code that can potentially JSONify it + msg = msg if isinstance(msg, str) else str(msg) + if level == logging.DEBUG: + self.docloudlogger.fine(msg) + elif level == logging.INFO: + self.docloudlogger.info(msg) + elif level == logging.WARNING: + self.docloudlogger.warning(msg) + elif level == logging.ERROR or level == logging.CRITICAL: + self.docloudlogger.error(msg) + else: + raise ValueError('Supported logging levels are: DEBUG, INFO, WARNING, ERROR, CRITICAL. Provided = %s' % level)
+ + +
[docs]class LoggerToFile(DocplexLogger): + '''This logger logs log records with python style logging levels and just print them + on the specified stream + ''' + def __init__(self, file): + super(LoggerToDocloud, self).__init__() + self.file = file + +
[docs] def log(self, level, msg): + self.file.write('[%s] %s\n' % (level, msg))
+
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_modules/index.html b/docs/2.24.232/mp/_modules/index.html new file mode 100644 index 0000000..32d08cc --- /dev/null +++ b/docs/2.24.232/mp/_modules/index.html @@ -0,0 +1,115 @@ + + + + + + + + + Overview: module code — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/_static/ajax-loader.gif b/docs/2.24.232/mp/_static/ajax-loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..61faf8cab23993bd3e1560bff0668bd628642330 GIT binary patch literal 673 zcmZ?wbhEHb6krfw_{6~Q|Nno%(3)e{?)x>&1u}A`t?OF7Z|1gRivOgXi&7IyQd1Pl zGfOfQ60;I3a`F>X^fL3(@);C=vM_KlFfb_o=k{|A33hf2a5d61U}gjg=>Rd%XaNQW zW@Cw{|b%Y*pl8F?4B9 zlo4Fz*0kZGJabY|>}Okf0}CCg{u4`zEPY^pV?j2@h+|igy0+Kz6p;@SpM4s6)XEMg z#3Y4GX>Hjlml5ftdH$4x0JGdn8~MX(U~_^d!Hi)=HU{V%g+mi8#UGbE-*ao8f#h+S z2a0-5+vc7MU$e-NhmBjLIC1v|)9+Im8x1yacJ7{^tLX(ZhYi^rpmXm0`@ku9b53aN zEXH@Y3JaztblgpxbJt{AtE1ad1Ca>{v$rwwvK(>{m~Gf_=-Ro7Fk{#;i~+{{>QtvI yb2P8Zac~?~=sRA>$6{!(^3;ZP0TPFR(G_-UDU(8Jl0?(IXu$~#4A!880|o%~Al1tN literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_static/background_b01.png b/docs/2.24.232/mp/_static/background_b01.png new file mode 100644 index 0000000000000000000000000000000000000000..353f26dde0803aa172c23e21ef6ac068e1253bc8 GIT binary patch literal 78 zcmeAS@N?(olHy`uVBq!ia0vp^%plAGBp8aFUnK)6QBN1gkP61+AN9}d56}GnU-I97 adu9eB2afnIr`wM~3O!x@T-G@yGywpsd=;et literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_static/basic.css b/docs/2.24.232/mp/_static/basic.css new file mode 100644 index 0000000..0807176 --- /dev/null +++ b/docs/2.24.232/mp/_static/basic.css @@ -0,0 +1,676 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 450px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist td { + vertical-align: top; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +div.code-block-caption { + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +div.code-block-caption + div > div.highlight > pre { + margin-top: 0; +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + padding: 1em 1em 0; +} + +div.literal-block-wrapper div.highlight { + margin: 0; +} + +code.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +code.descclassname { + background-color: transparent; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: relative; + left: 0px; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/2.24.232/mp/_static/bizstyle.css b/docs/2.24.232/mp/_static/bizstyle.css new file mode 100644 index 0000000..0464a74 --- /dev/null +++ b/docs/2.24.232/mp/_static/bizstyle.css @@ -0,0 +1,490 @@ +/* + * bizstyle.css_t + * ~~~~~~~~~~~~~~ + * + * Sphinx stylesheet -- business style theme. + * + * :copyright: Copyright 2011-2014 by Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', + 'Verdana', sans-serif; + font-size: 14px; + letter-spacing: -0.01em; + line-height: 150%; + text-align: center; + background-color: white; + background-image: url(background_b01.png); + color: black; + padding: 0; + border-right: 1px solid #336699; + border-left: 1px solid #336699; + + margin: 0px 40px 0px 40px; +} + +div.document { + background-color: white; + text-align: left; + background-repeat: repeat-x; + + -moz-box-shadow: 2px 2px 5px #000; + -webkit-box-shadow: 2px 2px 5px #000; +} + +div.bodywrapper { + margin: 0 0 0 240px; + border-left: 1px solid #ccc; +} + +div.body { + margin: 0; + padding: 0.5em 20px 20px 20px; +} + +div.related { + font-size: 1em; + + -moz-box-shadow: 2px 2px 5px #000; + -webkit-box-shadow: 2px 2px 5px #000; +} + +div.related ul { + background-color: #336699; + height: 100%; + overflow: hidden; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; +} + +div.related ul li { + color: white; + margin: 0; + padding: 0; + height: 2em; + float: left; +} + +div.related ul li.right { + float: right; + margin-right: 5px; +} + +div.related ul li a { + margin: 0; + padding: 0 5px 0 5px; + line-height: 1.75em; + color: #fff; +} + +div.related ul li a:hover { + color: #fff; + text-decoration: underline; +} + +div.sphinxsidebarwrapper { + padding: 0; +} + +div.sphinxsidebar { + margin: 0; + padding: 0.5em 12px 12px 12px; + width: 210px; + font-size: 1em; + text-align: left; +} + +div.sphinxsidebar h3, div.sphinxsidebar h4 { + margin: 1em 0 0.5em 0; + font-size: 1em; + padding: 0.1em 0 0.1em 0.5em; + color: white; + border: 1px solid #336699; + background-color: #336699; +} + +div.sphinxsidebar h3 a { + color: white; +} + +div.sphinxsidebar ul { + padding-left: 1.5em; + margin-top: 7px; + padding: 0; + line-height: 130%; +} + +div.sphinxsidebar ul ul { + margin-left: 20px; +} + +div.sphinxsidebar input { + border: 1px solid #336699; +} + +div.footer { + background-color: white; + color: #336699; + padding: 3px 8px 3px 0; + clear: both; + font-size: 0.8em; + text-align: right; + border-bottom: 1px solid #336699; + + -moz-box-shadow: 2px 2px 5px #000; + -webkit-box-shadow: 2px 2px 5px #000; +} + +div.footer a { + color: #336699; + text-decoration: underline; +} + +/* -- body styles ----------------------------------------------------------- */ + +p { + margin: 0.8em 0 0.5em 0; +} + +a { + color: #336699; + text-decoration: none; +} + +a:hover { + color: #336699; + text-decoration: underline; +} + +div.body a { + text-decoration: underline; +} + +h1, h2, h3 { + color: #336699; +} + +h1 { + margin: 0; + padding: 0.7em 0 0.3em 0; + font-size: 1.5em; +} + +h2 { + margin: 1.3em 0 0.2em 0; + font-size: 1.35em; + padding-bottom: .5em; + border-bottom: 1px solid #336699; +} + +h3 { + margin: 1em 0 -0.3em 0; + font-size: 1.2em; + padding-bottom: .3em; + border-bottom: 1px solid #CCCCCC; +} + +div.body h1 a, div.body h2 a, div.body h3 a, +div.body h4 a, div.body h5 a, div.body h6 a { + color: black!important; +} + +h1 a.anchor, h2 a.anchor, h3 a.anchor, +h4 a.anchor, h5 a.anchor, h6 a.anchor { + display: none; + margin: 0 0 0 0.3em; + padding: 0 0.2em 0 0.2em; + color: #aaa!important; +} + +h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, +h5:hover a.anchor, h6:hover a.anchor { + display: inline; +} + +h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, +h5 a.anchor:hover, h6 a.anchor:hover { + color: #777; + background-color: #eee; +} + +a.headerlink { + color: #c60f0f!important; + font-size: 1em; + margin-left: 6px; + padding: 0 4px 0 4px; + text-decoration: none!important; +} + +a.headerlink:hover { + background-color: #ccc; + color: white!important; +} + +cite, code, tt { + font-family: 'Consolas', 'Deja Vu Sans Mono', + 'Bitstream Vera Sans Mono', monospace; + font-size: 0.95em; + letter-spacing: 0.01em; +} + +code { + background-color: #F2F2F2; + border-bottom: 1px solid #ddd; + color: #333; +} + +code.descname, code.descclassname, code.xref { + border: 0; +} + +hr { + border: 1px solid #abc; + margin: 2em; +} + +a code { + border: 0; + color: #CA7900; +} + +a code:hover { + color: #2491CF; +} + +pre { + background-color: transparent !important; + font-family: 'Consolas', 'Deja Vu Sans Mono', + 'Bitstream Vera Sans Mono', monospace; + font-size: 0.95em; + letter-spacing: 0.015em; + line-height: 120%; + padding: 0.5em; + border-right: 5px solid #ccc; + border-left: 5px solid #ccc; +} + +pre a { + color: inherit; + text-decoration: underline; +} + +td.linenos pre { + padding: 0.5em 0; +} + +div.quotebar { + background-color: #f8f8f8; + max-width: 250px; + float: right; + padding: 2px 7px; + border: 1px solid #ccc; +} + +div.topic { + background-color: #f8f8f8; +} + +table { + border-collapse: collapse; + margin: 0 -0.5em 0 -0.5em; +} + +table td, table th { + padding: 0.2em 0.5em 0.2em 0.5em; +} + +div.admonition { + font-size: 0.9em; + margin: 1em 0 1em 0; + border: 3px solid #cccccc; + background-color: #f7f7f7; + padding: 0; +} + +div.admonition p { + margin: 0.5em 1em 0.5em 1em; + padding: 0; +} + +div.admonition li p { + margin-left: 0; +} + +div.admonition pre, div.warning pre { + margin: 0; +} + +div.highlight { + margin: 0.4em 1em; +} + +div.admonition p.admonition-title { + margin: 0; + padding: 0.1em 0 0.1em 0.5em; + color: white; + border-bottom: 3px solid #cccccc; + font-weight: bold; + background-color: #165e83; +} + +div.danger { border: 3px solid #f0908d; background-color: #f0cfa0; } +div.error { border: 3px solid #f0908d; background-color: #ede4cd; } +div.warning { border: 3px solid #f8b862; background-color: #f0cfa0; } +div.caution { border: 3px solid #f8b862; background-color: #ede4cd; } +div.attention { border: 3px solid #f8b862; background-color: #f3f3f3; } +div.important { border: 3px solid #f0cfa0; background-color: #ede4cd; } +div.note { border: 3px solid #f0cfa0; background-color: #f3f3f3; } +div.hint { border: 3px solid #bed2c3; background-color: #f3f3f3; } +div.tip { border: 3px solid #bed2c3; background-color: #f3f3f3; } + +div.danger p.admonition-title, div.error p.admonition-title { + background-color: #b7282e; + border-bottom: 3px solid #f0908d; +} + +div.caution p.admonition-title, +div.warning p.admonition-title, +div.attention p.admonition-title { + background-color: #f19072; + border-bottom: 3px solid #f8b862; +} + +div.note p.admonition-title, div.important p.admonition-title { + background-color: #f8b862; + border-bottom: 3px solid #f0cfa0; +} + +div.hint p.admonition-title, div.tip p.admonition-title { + background-color: #7ebea5; + border-bottom: 3px solid #bed2c3; +} + +div.admonition ul, div.admonition ol, +div.warning ul, div.warning ol { + margin: 0.1em 0.5em 0.5em 3em; + padding: 0; +} + +div.versioninfo { + margin: 1em 0 0 0; + border: 1px solid #ccc; + background-color: #DDEAF0; + padding: 8px; + line-height: 1.3em; + font-size: 0.9em; +} + +.viewcode-back { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', + 'Verdana', sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} + +p.versionchanged span.versionmodified { + font-size: 0.9em; + margin-right: 0.2em; + padding: 0.1em; + background-color: #DCE6A0; +} + +/* -- table styles ---------------------------------------------------------- */ + +table.docutils { + margin: 1em 0; + padding: 0; + border: 1px solid white; + background-color: #f7f7f7; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 1px solid white; + border-bottom: 1px solid white; +} + +table.docutils td p { + margin-top: 0; + margin-bottom: 0.3em; +} + +table.field-list td, table.field-list th { + border: 0 !important; + word-break: break-word; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + color: white; + text-align: left; + padding-right: 5px; + background-color: #82A0BE; +} + +div.literal-block-wrapper div.code-block-caption { + background-color: #EEE; + border-style: solid; + border-color: #CCC; + border-width: 1px 5px; +} + +/* WIDE DESKTOP STYLE */ +@media only screen and (min-width: 1176px) { +body { + margin: 0 40px 0 40px; +} +} + +/* TABLET STYLE */ +@media only screen and (min-width: 768px) and (max-width: 991px) { +body { + margin: 0 40px 0 40px; +} +} + +/* MOBILE LAYOUT (PORTRAIT/320px) */ +@media only screen and (max-width: 767px) { +body { + margin: 0; +} +div.bodywrapper { + margin: 0; + width: 100%; + border: none; +} +div.sphinxsidebar { + display: none; +} +} + +/* MOBILE LAYOUT (LANDSCAPE/480px) */ +@media only screen and (min-width: 480px) and (max-width: 767px) { +body { + margin: 0 20px 0 20px; +} +} + +/* RETINA OVERRIDES */ +@media +only screen and (-webkit-min-device-pixel-ratio: 2), +only screen and (min-device-pixel-ratio: 2) { +} + +/* -- end ------------------------------------------------------------------- */ \ No newline at end of file diff --git a/docs/2.24.232/mp/_static/bizstyle.js b/docs/2.24.232/mp/_static/bizstyle.js new file mode 100644 index 0000000..4e6eb25 --- /dev/null +++ b/docs/2.24.232/mp/_static/bizstyle.js @@ -0,0 +1,41 @@ +// +// bizstyle.js +// ~~~~~~~~~~~ +// +// Sphinx javascript -- for bizstyle theme. +// +// This theme was created by referring to 'sphinxdoc' +// +// :copyright: Copyright 2012-2014 by Sphinx team, see AUTHORS. +// :license: BSD, see LICENSE for details. +// +$(document).ready(function(){ + if (navigator.userAgent.indexOf('iPhone') > 0 || + navigator.userAgent.indexOf('Android') > 0) { + $("li.nav-item-0 a").text("Top"); + } + + $("div.related:first ul li:not(.right) a").slice(1).each(function(i, item){ + if (item.text.length > 20) { + var tmpstr = item.text + $(item).attr("title", tmpstr); + $(item).text(tmpstr.substr(0, 17) + "..."); + } + }); + $("div.related:last ul li:not(.right) a").slice(1).each(function(i, item){ + if (item.text.length > 20) { + var tmpstr = item.text + $(item).attr("title", tmpstr); + $(item).text(tmpstr.substr(0, 17) + "..."); + } + }); +}); + +$(window).resize(function(){ + if ($(window).width() <= 776) { + $("li.nav-item-0 a").text("Top"); + } + else { + $("li.nav-item-0 a").text("DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation"); + } +}); \ No newline at end of file diff --git a/docs/2.24.232/mp/_static/comment-bright.png b/docs/2.24.232/mp/_static/comment-bright.png new file mode 100644 index 0000000000000000000000000000000000000000..15e27edb12ac25701ac0ac21b97b52bb4e45415e GIT binary patch literal 756 zcmVgfIX78 z$8Pzv({A~p%??+>KickCb#0FM1rYN=mBmQ&Nwp<#JXUhU;{|)}%&s>suq6lXw*~s{ zvHx}3C%<;wE5CH!BR{p5@ml9ws}y)=QN-kL2?#`S5d*6j zk`h<}j1>tD$b?4D^N9w}-k)bxXxFg>+#kme^xx#qg6FI-%iv2U{0h(Y)cs%5a|m%Pn_K3X_bDJ>EH#(Fb73Z zfUt2Q3B>N+ot3qb*DqbTZpFIn4a!#_R-}{?-~Hs=xSS6p&$sZ-k1zDdtqU`Y@`#qL z&zv-~)Q#JCU(dI)Hf;$CEnK=6CK50}q7~wdbI->?E07bJ0R;!GSQTs5Am`#;*WHjvHRvY?&$Lm-vq1a_BzocI^ULXV!lbMd%|^B#fY;XX)n<&R^L z=84u1e_3ziq;Hz-*k5~zwY3*oDKt0;bM@M@@89;@m*4RFgvvM_4;5LB!@OB@^WbVT zjl{t;a8_>od-~P4 m{5|DvB&z#xT;*OnJqG}gk~_7HcNkCr0000W zanA~u9RIXo;n7c96&U)YLgs-FGlx~*_c{Jgvesu1E5(8YEf&5wF=YFPcRe@1=MJmi zag(L*xc2r0(slpcN!vC5CUju;vHJkHc*&70_n2OZsK%O~A=!+YIw z7zLLl7~Z+~RgWOQ=MI6$#0pvpu$Q43 zP@36QAmu6!_9NPM?o<1_!+stoVRRZbW9#SPe!n;#A_6m8f}|xN1;H{`0RoXQ2LM47 zt(g;iZ6|pCb@h2xk&(}S3=EVBUO0e90m2Lp5CB<(SPIaB;n4))3JB87Or#XPOPcum z?<^(g+m9}VNn4Y&B`g8h{t_$+RB1%HKRY6fjtd-<7&EsU;vs0GM(Lmbhi%Gwcfs0FTF}T zL{_M6Go&E0Eg8FuB*(Yn+Z*RVTBE@10eIOb3El^MhO`GabDll(V0&FlJi2k^;q8af zkENdk2}x2)_KVp`5OAwXZM;dG0?M-S)xE1IKDi6BY@5%Or?#aZ9$gcX)dPZ&wA1a< z$rFXHPn|TBf`e?>Are8sKtKrKcjF$i^lp!zkL?C|y^vlHr1HXeVJd;1I~g&Ob-q)& z(fn7s-KI}G{wnKzg_U5G(V%bX6uk zIa+<@>rdmZYd!9Y=C0cuchrbIjuRB_Wq{-RXlic?flu1*_ux}x%(HDH&nT`k^xCeC ziHi1!ChH*sQ6|UqJpTTzX$aw8e(UfcS^f;6yBWd+(1-70zU(rtxtqR%j z-lsH|CKQJXqD{+F7V0OTv8@{~(wp(`oIP^ZykMWgR>&|RsklFMCnOo&Bd{le} zV5F6424Qzl;o2G%oVvmHgRDP9!=rK8fy^!yV8y*4p=??uIRrrr0?>O!(z*g5AvL2!4z0{sq%vhG*Po}`a<6%kTK5TNhtC8}rXNu&h^QH4A&Sk~Autm*s~45(H7+0bi^MraaRVzr05hQ3iK?j` zR#U@^i0WhkIHTg29u~|ypU?sXCQEQgXfObPW;+0YAF;|5XyaMAEM0sQ@4-xCZe=0e z7r$ofiAxn@O5#RodD8rh5D@nKQ;?lcf@tg4o+Wp44aMl~c47azN_(im0N)7OqdPBC zGw;353_o$DqGRDhuhU$Eaj!@m000000NkvXXu0mjfjZ7Z_ literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/_static/css3-mediaqueries.js b/docs/2.24.232/mp/_static/css3-mediaqueries.js new file mode 100644 index 0000000..59735f5 --- /dev/null +++ b/docs/2.24.232/mp/_static/css3-mediaqueries.js @@ -0,0 +1 @@ +if(typeof Object.create!=="function"){Object.create=function(e){function t(){}t.prototype=e;return new t}}var ua={toString:function(){return navigator.userAgent},test:function(e){return this.toString().toLowerCase().indexOf(e.toLowerCase())>-1}};ua.version=(ua.toString().toLowerCase().match(/[\s\S]+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[])[1];ua.webkit=ua.test("webkit");ua.gecko=ua.test("gecko")&&!ua.webkit;ua.opera=ua.test("opera");ua.ie=ua.test("msie")&&!ua.opera;ua.ie6=ua.ie&&document.compatMode&&typeof document.documentElement.style.maxHeight==="undefined";ua.ie7=ua.ie&&document.documentElement&&typeof document.documentElement.style.maxHeight!=="undefined"&&typeof XDomainRequest==="undefined";ua.ie8=ua.ie&&typeof XDomainRequest!=="undefined";var domReady=function(){var e=[];var t=function(){if(!arguments.callee.done){arguments.callee.done=true;for(var t=0;t=200&&r.status<300||r.status===304||navigator.userAgent.indexOf("Safari")>-1&&typeof r.status==="undefined"){t(r.responseText)}else{n()}document.documentElement.style.cursor="";r=null}};r.send("")};var l=function(t){t=t.replace(e.REDUNDANT_COMPONENTS,"");t=t.replace(e.REDUNDANT_WHITESPACE,"$1");t=t.replace(e.WHITESPACE_IN_PARENTHESES,"($1)");t=t.replace(e.MORE_WHITESPACE," ");t=t.replace(e.FINAL_SEMICOLONS,"}");return t};var c={stylesheet:function(t){var n={};var r=[],i=[],s=[],o=[];var u=t.cssHelperText;var a=t.getAttribute("media");if(a){var f=a.toLowerCase().split(",")}else{var f=["all"]}for(var l=0;l-1&&a.href&&a.href.length!==0&&!a.disabled){r[r.length]=a}}if(r.length>0){var c=0;var d=function(){c++;if(c===r.length){i()}};var v=function(t){var n=t.href;f(n,function(r){r=l(r).replace(e.RELATIVE_URLS,"url("+n.substring(0,n.lastIndexOf("/"))+"/$1)");t.cssHelperText=r;d()},d)};for(u=0;u0){r.setAttribute("media",t.join(","))}document.getElementsByTagName("head")[0].appendChild(r);if(r.styleSheet){r.styleSheet.cssText=e}else{r.appendChild(document.createTextNode(e))}r.addedWithCssHelper=true;if(typeof n==="undefined"||n===true){cssHelper.parsed(function(t){var n=p(r,e);for(var i in n){if(n.hasOwnProperty(i)){g(i,n[i])}}a("newStyleParsed",r)})}else{r.parsingDisallowed=true}return r},removeStyle:function(e){return e.parentNode.removeChild(e)},parsed:function(e){if(n){s(e)}else{if(typeof t!=="undefined"){if(typeof e==="function"){e(t)}}else{s(e);d()}}},stylesheets:function(e){cssHelper.parsed(function(t){e(m.stylesheets||y("stylesheets"))})},mediaQueryLists:function(e){cssHelper.parsed(function(t){e(m.mediaQueryLists||y("mediaQueryLists"))})},rules:function(e){cssHelper.parsed(function(t){e(m.rules||y("rules"))})},selectors:function(e){cssHelper.parsed(function(t){e(m.selectors||y("selectors"))})},declarations:function(e){cssHelper.parsed(function(t){e(m.declarations||y("declarations"))})},properties:function(e){cssHelper.parsed(function(t){e(m.properties||y("properties"))})},broadcast:a,addListener:function(e,t){if(typeof t==="function"){if(!u[e]){u[e]={listeners:[]}}u[e].listeners[u[e].listeners.length]=t}},removeListener:function(e,t){if(typeof t==="function"&&u[e]){var n=u[e].listeners;for(var r=0;r=a||s&&l0}}else if("device-height"===e.substring(r-13,r)){c=screen.height;if(t!==null){if(u==="length"){return i&&c>=a||s&&c0}}else if("width"===e.substring(r-5,r)){l=document.documentElement.clientWidth||document.body.clientWidth;if(t!==null){if(u==="length"){return i&&l>=a||s&&l0}}else if("height"===e.substring(r-6,r)){c=document.documentElement.clientHeight||document.body.clientHeight;if(t!==null){if(u==="length"){return i&&c>=a||s&&c0}}else if("device-aspect-ratio"===e.substring(r-19,r)){return u==="aspect-ratio"&&screen.width*a[1]===screen.height*a[0]}else if("color-index"===e.substring(r-11,r)){var h=Math.pow(2,screen.colorDepth);if(t!==null){if(u==="absolute"){return i&&h>=a||s&&h0}}else if("color"===e.substring(r-5,r)){var p=screen.colorDepth;if(t!==null){if(u==="absolute"){return i&&p>=a||s&&p0}}else if("resolution"===e.substring(r-10,r)){var d;if(f==="dpcm"){d=o("1cm")}else{d=o("1in")}if(t!==null){if(u==="resolution"){return i&&d>=a||s&&d0}}else{return false}};var a=function(e){var t=e.getValid();var n=e.getExpressions();var r=n.length;if(r>0){for(var i=0;i0){u=false;for(var f=0;f0){l[c++]=","}l[c++]=h}}if(l.length>0){r[r.length]=cssHelper.addStyle("@media "+l.join("")+"{"+e.getCssText()+"}",t,false)}};var l=function(e,t){for(var n=0;n0}}var o=[],u=[];for(var f in i){if(i.hasOwnProperty(f)){o[o.length]=f;if(i[f]){u[u.length]=f}if(f==="all"){n=true}}}if(u.length>0){r[r.length]=cssHelper.addStyle(e.getCssText(),u,false)}var c=e.getMediaQueryLists();if(n){l(c)}else{l(c,o)}};var h=function(e){for(var t=0;td||Math.abs(s-t)>d){e=n;t=s;clearTimeout(r);r=setTimeout(function(){if(!i()){p()}else{cssHelper.broadcast("cssMediaQueriesTested")}},500)}};window.onresize=function(){var e=window.onresize||function(){};return function(){e();s()}}()};var m=document.documentElement;m.style.marginLeft="-32767px";setTimeout(function(){m.style.marginLeft=""},5e3);return function(){if(!i()){cssHelper.addListener("newStyleParsed",function(e){c(e.cssHelperParsed.stylesheet)});cssHelper.addListener("cssMediaQueriesTested",function(){if(ua.ie){m.style.width="1px"}setTimeout(function(){m.style.width="";m.style.marginLeft=""},0);cssHelper.removeListener("cssMediaQueriesTested",arguments.callee)});s();p()}else{m.style.marginLeft=""}v()}}());try{document.execCommand("BackgroundImageCache",false,true)}catch(e){} diff --git a/docs/2.24.232/mp/_static/css3-mediaqueries_src.js b/docs/2.24.232/mp/_static/css3-mediaqueries_src.js new file mode 100644 index 0000000..f21dd49 --- /dev/null +++ b/docs/2.24.232/mp/_static/css3-mediaqueries_src.js @@ -0,0 +1,1104 @@ +/* +css3-mediaqueries.js - CSS Helper and CSS3 Media Queries Enabler + +author: Wouter van der Graaf +version: 1.0 (20110330) +license: MIT +website: http://code.google.com/p/css3-mediaqueries-js/ + +W3C spec: http://www.w3.org/TR/css3-mediaqueries/ + +Note: use of embedded + + + + + +
+

Indices and tables

+ +
+ + + + + +
+ + + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/lagrangian_relaxation.html b/docs/2.24.232/mp/lagrangian_relaxation.html new file mode 100644 index 0000000..0988a25 --- /dev/null +++ b/docs/2.24.232/mp/lagrangian_relaxation.html @@ -0,0 +1,379 @@ + + + + + + + + + lagrangian_relaxation.py — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

lagrangian_relaxation.py

+

This example is inspired by an entry on the “adventures in optimization” blog. +The sources of the article can be found here. +This example solves the generalized assignment problem, with or without Lagrangian relaxation.

+
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2018
+# --------------------------------------------------------------------------
+
+import json
+
+from docplex.util.environment import get_environment
+from docplex.mp.model import Model
+
+
+# ----------------------------------------------------------------------------
+# Initialize the problem data
+# ----------------------------------------------------------------------------
+B = [15, 15, 15]
+C = [
+    [ 6, 10, 1],
+    [12, 12, 5],
+    [15,  4, 3],
+    [10,  3, 9],
+    [8,   9, 5]
+]
+A = [
+    [ 5,  7,  2],
+    [14,  8,  7],
+    [10,  6, 12],
+    [ 8,  4, 15],
+    [ 6, 12,  5]
+]
+
+
+# ----------------------------------------------------------------------------
+# Build the model
+# ----------------------------------------------------------------------------
+def run_GAP_model(As, Bs, Cs, **kwargs):
+    with Model('GAP per Wolsey -without- Lagrangian Relaxation', **kwargs) as mdl:
+        print("#As={}, #Bs={}, #Cs={}".format(len(As), len(Bs), len(Cs)))
+        number_of_cs = len(C)
+        # variables
+        x_vars = [mdl.binary_var_list(c, name=None) for c in Cs]
+
+        # constraints
+        mdl.add_constraints(mdl.sum(xv) <= 1 for xv in x_vars)
+
+        mdl.add_constraints(mdl.sum(x_vars[ii][j] * As[ii][j] for ii in range(number_of_cs)) <= bs
+                            for j, bs in enumerate(Bs))
+
+        # objective
+        total_profit = mdl.sum(mdl.scal_prod(x_i, c_i) for c_i, x_i in zip(Cs, x_vars))
+        mdl.maximize(total_profit)
+        #  mdl.print_information()
+        s = mdl.solve()
+        assert s is not None
+        obj = s.objective_value
+        print("* GAP with no relaxation run OK, best objective is: {:g}".format(obj))
+    return obj
+
+
+def run_GAP_model_with_Lagrangian_relaxation(As, Bs, Cs, max_iters=101, **kwargs):
+    with Model('GAP per Wolsey -with- Lagrangian Relaxation', **kwargs) as mdl:
+        print("#As={}, #Bs={}, #Cs={}".format(len(As), len(Bs), len(Cs)))
+        number_of_cs = len(Cs)
+        c_range = range(number_of_cs)
+        # variables
+        x_vars = [mdl.binary_var_list(c, name=None) for c in Cs]
+        p_vars = mdl.continuous_var_list(Cs, name='p')  # new for relaxation
+
+        mdl.add_constraints(mdl.sum(xv) == 1 - pv for xv, pv in zip(x_vars, p_vars))
+
+        mdl.add_constraints(mdl.sum(x_vars[ii][j] * As[ii][j] for ii in c_range) <= bs
+                            for j, bs in enumerate(Bs))
+
+        # lagrangian relaxation loop
+        eps = 1e-6
+        loop_count = 0
+        best = 0
+        initial_multiplier = 1
+        multipliers = [initial_multiplier] * len(Cs)
+
+        total_profit = mdl.sum(mdl.scal_prod(x_i, c_i) for c_i, x_i in zip(Cs, x_vars))
+        mdl.add_kpi(total_profit, "Total profit")
+
+        while loop_count <= max_iters:
+            loop_count += 1
+            # rebuilt at each loop iteration
+            total_penalty = mdl.scal_prod(p_vars, multipliers)
+            mdl.maximize(total_profit + total_penalty)
+            s = mdl.solve()
+            if not s:
+                print("*** solve fails, stopping at iteration: %d" % loop_count)
+                break
+            best = s.objective_value
+            penalties = [pv.solution_value for pv in p_vars]
+            print('%d> new lagrangian iteration:\n\t obj=%g, m=%s, p=%s' % (loop_count, best, str(multipliers), str(penalties)))
+
+            do_stop = True
+            justifier = 0
+            for k in c_range:
+                penalized_violation = penalties[k] * multipliers[k]
+                if penalized_violation >= eps:
+                    do_stop = False
+                    justifier = penalized_violation
+                    break
+
+            if do_stop:
+                print("* Lagrangian relaxation succeeds, best={:g}, penalty={:g}, #iterations={}"
+                      .format(best, total_penalty.solution_value, loop_count))
+                break
+            else:
+                # update multipliers and start loop again.
+                scale_factor = 1.0 / float(loop_count)
+                multipliers = [max(multipliers[i] - scale_factor * penalties[i], 0.) for i in c_range]
+                print('{0}> -- loop continues, m={1!s}, justifier={2:g}'.format(loop_count, multipliers, justifier))
+
+    return best
+
+
+def run_default_GAP_model_with_lagrangian_relaxation(**kwargs):
+    return run_GAP_model_with_Lagrangian_relaxation(As=A, Bs=B, Cs=C, **kwargs)
+
+
+# ----------------------------------------------------------------------------
+# Solve the model and display the result
+# ----------------------------------------------------------------------------
+if __name__ == '__main__':
+    # Run the model. If a key has been specified above, the model will run on
+    # IBM Decision Optimization on cloud.
+    gap_best_obj = run_GAP_model(A, B, C)
+    relaxed_best = run_GAP_model_with_Lagrangian_relaxation(A, B, C)
+    # save the relaxed solution
+    with get_environment().get_output_stream("solution.json") as fp:
+        fp.write(json.dumps({"objectiveValue": relaxed_best}).encode('utf-8'))
+
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/load_balancing.html b/docs/2.24.232/mp/load_balancing.html new file mode 100644 index 0000000..870edef --- /dev/null +++ b/docs/2.24.232/mp/load_balancing.html @@ -0,0 +1,609 @@ + + + + + + + + + load_balancing.py — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

load_balancing.py

+

This example looks at cloud load balancing to keep a service running in the cloud at reasonable cost +by reducing the expense of running cloud servers, +minimizing risk and human time due to rebalancing, and doing balance sleeping models across servers.

+

The different KPIs are optimized using multiobjective solve. +This optimization is achieved by the minimize_static_lex() method.

+
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2018
+# --------------------------------------------------------------------------
+
+# Source: http://blog.yhathq.com/posts/how-yhat-does-cloud-balancing.html
+
+from collections import namedtuple
+
+from docplex.mp.model import Model
+
+
+# ----------------------------------------------------------------------------
+# Initialize the problem data
+# ----------------------------------------------------------------------------
+class TUser(namedtuple("TUser", ["id", "running", "sleeping", "current_server"])):
+    def __str__(self):
+        return self.id
+
+
+SERVERS = ["server002", "server003", "server001", "server006", "server007", "server004", "server005"]
+
+USERS = [("user013", 2, 1, "server002"),
+         ("user014", 0, 2, "server002"),
+         ("user015", 0, 4, "server002"),
+         ("user016", 1, 4, "server002"),
+         ("user017", 0, 3, "server002"),
+         ("user018", 0, 2, "server002"),
+         ("user019", 0, 2, "server002"),
+         ("user020", 0, 1, "server002"),
+         ("user021", 4, 4, "server002"),
+         ("user022", 0, 1, "server002"),
+         ("user023", 0, 3, "server002"),
+         ("user024", 1, 2, "server002"),
+         ("user025", 0, 1, "server003"),
+         ("user026", 0, 1, "server003"),
+         ("user027", 1, 1, "server003"),
+         ("user028", 0, 1, "server003"),
+         ("user029", 2, 1, "server003"),
+         ("user030", 0, 5, "server003"),
+         ("user031", 0, 2, "server003"),
+         ("user032", 0, 3, "server003"),
+         ("user033", 1, 1, "server003"),
+         ("user034", 0, 1, "server003"),
+         ("user035", 0, 1, "server003"),
+         ("user036", 4, 1, "server003"),
+         ("user037", 7, 1, "server003"),
+         ("user038", 2, 1, "server003"),
+         ("user039", 0, 3, "server003"),
+         ("user040", 1, 2, "server003"),
+         ("user001", 0, 2, "server001"),
+         ("user002", 0, 3, "server001"),
+         ("user003", 5, 4, "server001"),
+         ("user004", 0, 1, "server001"),
+         ("user005", 0, 1, "server001"),
+         ("user006", 0, 2, "server001"),
+         ("user007", 0, 4, "server001"),
+         ("user008", 0, 1, "server001"),
+         ("user009", 5, 1, "server001"),
+         ("user010", 7, 1, "server001"),
+         ("user011", 4, 5, "server001"),
+         ("user012", 0, 4, "server001"),
+         ("user062", 0, 1, "server006"),
+         ("user063", 3, 5, "server006"),
+         ("user064", 0, 1, "server006"),
+         ("user065", 0, 3, "server006"),
+         ("user066", 3, 1, "server006"),
+         ("user067", 0, 1, "server006"),
+         ("user068", 0, 1, "server006"),
+         ("user069", 0, 2, "server006"),
+         ("user070", 3, 2, "server006"),
+         ("user071", 0, 1, "server006"),
+         ("user072", 5, 3, "server006"),
+         ("user073", 0, 1, "server006"),
+         ("user074", 0, 1, "server006"),
+         ("user075", 0, 2, "server007"),
+         ("user076", 1, 1, "server007"),
+         ("user077", 1, 1, "server007"),
+         ("user078", 0, 1, "server007"),
+         ("user079", 0, 3, "server007"),
+         ("user080", 0, 1, "server007"),
+         ("user081", 4, 1, "server007"),
+         ("user082", 1, 1, "server007"),
+         ("user041", 0, 1, "server004"),
+         ("user042", 2, 1, "server004"),
+         ("user043", 5, 2, "server004"),
+         ("user044", 5, 2, "server004"),
+         ("user045", 0, 2, "server004"),
+         ("user046", 1, 5, "server004"),
+         ("user047", 0, 1, "server004"),
+         ("user048", 0, 3, "server004"),
+         ("user049", 5, 1, "server004"),
+         ("user050", 0, 2, "server004"),
+         ("user051", 0, 3, "server004"),
+         ("user052", 0, 3, "server004"),
+         ("user053", 0, 1, "server004"),
+         ("user054", 0, 2, "server004"),
+         ("user055", 0, 3, "server005"),
+         ("user056", 3, 1, "server005"),
+         ("user057", 0, 3, "server005"),
+         ("user058", 0, 2, "server005"),
+         ("user059", 0, 1, "server005"),
+         ("user060", 0, 5, "server005"),
+         ("user061", 0, 2, "server005")
+         ]
+
+# ----------------------------------------------------------------------------
+# Prepare the data for modeling
+# ----------------------------------------------------------------------------
+DEFAULT_MAX_PROCESSES_PER_SERVER = 50
+
+
+def _is_migration(user, server):
+    """ Returns True if server is not the user's current
+        Used in setup of constraints.
+    """
+    return server != user.current_server
+
+
+# ----------------------------------------------------------------------------
+# Build the model
+# ----------------------------------------------------------------------------
+
+def build_load_balancing_model(servers, users_, max_process_per_server=DEFAULT_MAX_PROCESSES_PER_SERVER, **kwargs):
+    m = Model(name='load_balancing', **kwargs)
+
+    # decision objects
+
+    users = [TUser(*user_row) for user_row in users_]
+
+    active_var_by_server = m.binary_var_dict(servers, name='isActive')
+
+    def user_server_pair_namer(u_s):
+        u, s = u_s
+        return '%s_to_%s' % (u.id, s)
+
+    assign_user_to_server_vars = m.binary_var_matrix(users, servers, user_server_pair_namer)
+
+    m.add_constraints(
+        m.sum(assign_user_to_server_vars[u, s] * u.running for u in users) <= max_process_per_server for s in servers)
+    # each assignment var <u, s>  is <= active_server(s)
+    for s in servers:
+        for u in users:
+            ct_name = 'ct_assign_to_active_{0!s}_{1!s}'.format(u, s)
+            m.add_constraint(assign_user_to_server_vars[u, s] <= active_var_by_server[s], ct_name)
+
+        # sum of assignment vars for (u, all s in servers) == 1
+        for u in users:
+            ct_name = 'ct_unique_server_%s' % (u[0])
+            m.add_constraint(m.sum((assign_user_to_server_vars[u, s] for s in servers)) == 1, ct_name)
+
+    number_of_active_servers = m.sum((active_var_by_server[svr] for svr in servers))
+    m.add_kpi(number_of_active_servers, "Number of active servers")
+
+    number_of_migrations = m.sum(
+        assign_user_to_server_vars[u, s] for u in users for s in servers if
+        _is_migration(u, s))
+    m.add_kpi(number_of_migrations, "Total number of migrations")
+
+    max_sleeping_workload = m.integer_var(name="max_sleeping_processes")
+    for s in servers:
+        ct_name = 'ct_define_max_sleeping_%s' % s
+        m.add_constraint(
+            m.sum(
+                assign_user_to_server_vars[u, s] * u.sleeping for u in users) <= max_sleeping_workload,
+            ct_name)
+    m.add_kpi(max_sleeping_workload, "Max sleeping workload")
+    # Set objective function
+    # m.minimize(number_of_active_servers)
+    m.minimize_static_lex([number_of_active_servers, number_of_migrations, max_sleeping_workload])
+
+    # attach artefacts to model for reporting
+    m.users = users
+    m.servers = servers
+    m.active_var_by_server = active_var_by_server
+    m.assign_user_to_server_vars = assign_user_to_server_vars
+    m.max_sleeping_workload = max_sleeping_workload
+
+    return m
+
+
+def lb_report(mdl):
+    active_servers = sorted([s for s in mdl.servers if mdl.active_var_by_server[s].solution_value == 1])
+    print("Active Servers: {0} = {1}".format(len(active_servers), active_servers))
+    print("*** User/server assignments , #migrations={0} ***".format(
+        mdl.kpi_by_name("number of migrations").solution_value))
+    # for (u, s) in sorted(mdl.assign_user_to_server_vars):
+    #     if mdl.assign_user_to_server_vars[(u, s)].solution_value == 1:
+    #         print("{} uses {}, migration: {}".format(u, s, "yes" if _is_migration(u, s) else "no"))
+    print("*** Servers sleeping processes ***")
+    for s in active_servers:
+        sleeping = sum(mdl.assign_user_to_server_vars[u, s].solution_value * u.sleeping for u in mdl.users)
+        print("Server: {} #sleeping={}".format(s, sleeping))
+
+
+def make_default_load_balancing_model(**kwargs):
+    return build_load_balancing_model(SERVERS, USERS, **kwargs)
+
+
+def lb_save_solution_as_json(mdl, json_file):
+    """Saves the solution for this model as JSON.
+
+    Note that this is not a CPLEX Solution file, as this is the result of post-processing a CPLEX solution
+    """
+    import json
+    solution_dict = {}
+    # active server
+    active_servers = sorted([s for s in mdl.servers if mdl.active_var_by_server[s].solution_value == 1])
+    solution_dict["active servers"] = active_servers
+
+    # sleeping processes by server
+    sleeping_processes = {}
+    for s in active_servers:
+        sleeping = sum(mdl.assign_user_to_server_vars[u, s].solution_value * u.sleeping for u in mdl.users)
+        sleeping_processes[s] = sleeping
+    solution_dict["sleeping processes by server"] = sleeping_processes
+
+# user assignment
+    user_assignment = []
+    for (u, s) in sorted(mdl.assign_user_to_server_vars):
+        if mdl.assign_user_to_server_vars[(u, s)].solution_value == 1:
+            n = {
+                'user': u.id,
+                'server': s,
+                'migration': "yes" if _is_migration(u, s) else "no"
+            }
+            user_assignment.append(n)
+    solution_dict['user assignment'] = user_assignment
+    json_file.write(json.dumps(solution_dict, indent=3).encode('utf-8'))
+
+# ----------------------------------------------------------------------------
+# Solve the model and display the result
+# ----------------------------------------------------------------------------
+
+if __name__ == '__main__':
+    lbm = make_default_load_balancing_model()
+
+    # Run the model.
+    lbs = lbm.solve(log_output=True)
+    lb_report(lbm)
+    # save json, used in worker tests
+    from docplex.util.environment import get_environment
+    with get_environment().get_output_stream("solution.json") as fp:
+        lb_save_solution_as_json(lbm, fp)
+    lbm.end()
+
+
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/marketing_campaign.html b/docs/2.24.232/mp/marketing_campaign.html new file mode 100644 index 0000000..111e297 --- /dev/null +++ b/docs/2.24.232/mp/marketing_campaign.html @@ -0,0 +1,790 @@ + + + + + + + + + How to make targeted offers to customers? — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

How to make targeted offers to customers?

+

This tutorial includes everything you need to set up IBM Decision +Optimization CPLEX Modeling for Python (DOcplex), build a Mathematical +Programming model, and get its solution by solving the model with IBM +ILOG CPLEX Optimizer.

+

When you finish this tutorial, you’ll have a foundational knowledge of +Prescriptive Analytics.

+
+

This notebook is part of Prescriptive Analytics for +Python

+

It requires either an installation of CPLEX +Optimizers +or it can be run on IBM Watson Studio +Cloud (Sign up for a +free IBM Cloud +account +and you can start using Watson Studio Cloud right away).

+
+

Table of contents:

+ +
+

Describe the business problem

+
    +
  • The Self-Learning Response Model (SLRM) node enables you to build a +model that you can continually update. Such updates are useful in +building a model that assists with predicting which offers are most +appropriate for customers and the probability of the offers being +accepted. These sorts of models are most beneficial in customer +relationship management, such as marketing applications or call +centers.
  • +
  • This example is based on a fictional banking company.
  • +
  • The marketing department wants to achieve more profitable results in +future campaigns by matching the right offer of financial services to +each customer.
  • +
  • Specifically, the datascience department identified the +characteristics of customers who are most likely to respond favorably +based on previous offers and responses and to promote the best +current offer based on the results and now need to compute the best +offerig plan.
  • +
+

A set of business constraints have to be respected:

+
    +
  • We have a limited budget to run a marketing campaign based on +“gifts”, “newsletter”, “seminar”.
  • +
  • We want to determine which is the best way to contact the customers.
  • +
  • We need to identify which customers to contact.
  • +
+
+
+

How decision optimization can help

+
    +
  • Prescriptive analytics technology recommends actions based on desired +outcomes, taking into account specific scenarios, resources, and +knowledge of past and current events. This insight can help your +organization make better decisions and have greater control of +business outcomes.

    +
  • +
  • Prescriptive analytics is the next step on the path to insight-based +actions. It creates value through synergy with predictive analytics, +which analyzes data to predict future outcomes.

    +
  • +
  • +
    Prescriptive analytics takes that insight to the next level by +suggesting the optimal way to handle that future situation. +Organizations that can act fast in dynamic conditions and make +superior decisions in uncertain environments gain a strong +competitive advantage.
    +

    +
    +
  • +
  • For example:

    +
      +
    • Automate complex decisions and trade-offs to better manage limited +resources.
    • +
    • Take advantage of a future opportunity or mitigate a future risk.
    • +
    • Proactively update recommendations based on changing events.
    • +
    • Meet operational goals, increase customer loyalty, prevent threats +and fraud, and optimize business processes.
    • +
    +
  • +
+
+
+

Prepare the data

+

The predictions show which offers a customer is most likely to accept, +and the confidence that they will accept, depending on each customer’s +details.

+

For example: (139987, “Pension”, 0.13221, “Mortgage”, 0.10675) indicates +that customer Id=139987 will certainly not buy a Pension as the level +is only 13.2%, whereas (140030, “Savings”, 0.95678, “Pension”, 0.84446) +is more than likely to buy Savings and a Pension as the rates are +95.7% and 84.4%.

+

This data is taken from a SPSS example, except that the names of the +customers were modified.

+

A Python data analysis library, pandas, +is used to store the data. Let’s set up and declare the data.

+

Offers are stored in a pandas +DataFrame.

+

Let’s customize the display of this data and show the confidence +forecast for each customer.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nameProduct1Confidence1Product2Confidence2
17Cassio LombardoPension0.13221Mortgage0.10675
7Christian AusterlitzPension0.13221Mortgage0.10675
24Earl B. WoodSavings0.95678Pension0.83426
19Eldar MuravyovPension0.13221Mortgage0.10675
6Fabien MailhotPension0.13221Mortgage0.10675
26Franca PalermoPension0.13221Mortgage0.10675
25Gabrielly Sousa MartinsSavings0.95678Pension0.75925
13George BlomqvistSavings0.16428Pension0.13221
0Guadalupe J. MartinezPension0.13221Mortgage0.10675
21Jameel Abdul-Ghani GergesPension0.13221Mortgage0.10675
10Lee TsouPension0.13221Mortgage0.10675
23Matheus Azevedo MeloPension0.13221Mortgage0.10675
1Michelle M. LopezSavings0.95678Pension0.84446
3Miranda B. RoushPension0.13221Mortgage0.10675
12Miroslav SkaroupkaSavings0.95676Mortgage0.82269
5Roland Gu�rettePension0.13221Mortgage0.10675
11Sanaa' Hikmah HakimiPension0.13221Mortgage0.10675
4Sandra J. WynkoopPension0.80506Savings0.28391
20Shu T'anSavings0.95675Pension0.27248
8Steffen MeisterPension0.13221Mortgage0.10675
2Terry L. RidgleySavings0.95678Pension0.80233
18Trinity Zelaya MiramontesSavings0.28934Pension0.13221
16Vlad AlekseevaPension0.13221Mortgage0.10675
14Will HendersonSavings0.95678Pension0.86779
9Wolfgang SangerPension0.13221Mortgage0.10675
15Yuina OhiraPension0.13225Mortgage0.10675
22Zeeb Longoria MarreroSavings0.16188Pension0.13221
+
+
+

Use IBM Decision Optimization CPLEX Modeling for Python

+

Let’s create the optimization model to select the best ways to contact +customers and stay within the limited budget.

+
+

Step 1: Import the library

+

Run the following code to import the Decision Optimization CPLEX +Modeling library. The DOcplex library contains the two modeling +packages, Mathematical Programming (docplex.mp) and Constraint +Programming (docplex.cp).

+

If cplex is not installed, install CPLEX Community edition.

+
+
+

Step 2: Set up the prescriptive model

+
+

Create the model

+
+
+

Define the decision variables

+
    +
  • The integer decision variables channelVars, represent whether or +not a customer will be made an offer for a particular product via a +particular channel.
  • +
  • The integer decision variable totaloffers represents the total +number of offers made.
  • +
  • The continuous variable budgetSpent represents the total cost of +the offers made.
  • +
+
+
+

Set up the constraints

+
    +
  • Offer only one product per customer.
  • +
  • Compute the budget and set a maximum on it.
  • +
  • Compute the number of offers to be made.
  • +
+
Model: marketing_campaign
+ - number of variables: 326
+   - binary=324, integer=1, continuous=1
+ - number of constraints: 34
+   - linear=34
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+

Express the objective

+

We want to maximize the expected revenue.

+
+
+

Solve the model

+

If you’re using a Community Edition of CPLEX runtimes, depending on the +size of the problem, the solve stage may fail and will need a paying +subscription or product installation.

+
+
+
+

Step 3: Analyze the solution

+

First, let’s display the Optimal Marketing Channel per customer.

+
Marketing plan has 20 offers costing 364.0
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
channelproductcustomer
0newsletterCar loanFabien Mailhot
1newsletterCar loanChristian Austerlitz
2newsletterCar loanLee Tsou
3newsletterCar loanSanaa' Hikmah Hakimi
4newsletterCar loanGeorge Blomqvist
5newsletterCar loanYuina Ohira
6newsletterCar loanVlad Alekseeva
7newsletterCar loanCassio Lombardo
8newsletterCar loanTrinity Zelaya Miramontes
9newsletterCar loanEldar Muravyov
10newsletterCar loanJameel Abdul-Ghani Gerges
11newsletterCar loanZeeb Longoria Marrero
12seminarSavingsTerry L. Ridgley
13seminarSavingsGabrielly Sousa Martins
14seminarMortgageMiranda B. Roush
15seminarMortgageMiroslav Skaroupka
16seminarMortgageMatheus Azevedo Melo
17seminarMortgageFranca Palermo
18seminarPensionMichelle M. Lopez
19seminarPensionWill Henderson
+

Then let’s focus on seminar.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
productcustomer
12SavingsTerry L. Ridgley
13SavingsGabrielly Sousa Martins
14MortgageMiranda B. Roush
15MortgageMiroslav Skaroupka
16MortgageMatheus Azevedo Melo
17MortgageFranca Palermo
18PensionMichelle M. Lopez
19PensionWill Henderson
+
+
+
+

Summary

+

You learned how to set up and use IBM Decision Optimization CPLEX +Modeling for Python to formulate a Mathematical Programming model and +solve it with CPLEX.

+
+
+

References

+ +

Copyright � 2017-2019 IBM. IPLA licensed Sample Materials.

+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/mining_pandas.html b/docs/2.24.232/mp/mining_pandas.html new file mode 100644 index 0000000..490bada --- /dev/null +++ b/docs/2.24.232/mp/mining_pandas.html @@ -0,0 +1,1034 @@ + + + + + + + + + Optimizing mining operations — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

Optimizing mining operations

+

This tutorial includes everything you need to set up IBM Decision +Optimization CPLEX Modeling for Python (DOcplex), build a Mathematical +Programming model, and get its solution by solving the model on Cloud +with IBM ILOG CPLEX Optimizer.

+

When you finish this tutorial, you’ll have a foundational knowledge of +Prescriptive Analytics.

+
+

This notebook is part of Prescriptive Analytics for +Python

+

It requires either an installation of CPLEX +Optimizers +or it can be run on IBM Watson Studio +Cloud (Sign up for a +free IBM Cloud +account +and you can start using Watson Studio Cloud right away).

+
+

Table of contents:

+ +
+
+

Describe the business problem

+

This mining operations optimization problem is an implementation of +Problem 7 from “Model Building in Mathematical Programming” by H.P. +Williams. The operational decisions that need to be made are which mines +should be operated each year and how much each mine should produce.

+
+

Business constraints

+
    +
  • A mine that is closed cannot be worked.
  • +
  • Once closed, a mine stays closed until the end of the horizon.
  • +
  • Each year, a maximum number of mines can be worked.
  • +
  • For each mine and year, the quantity extracted is limited by the +mine’s maximum extracted quantity.
  • +
  • The average blend quality must be greater than or equal to the +requirement of the year.
  • +
+
+
+

Objective and KPIs

+
+

Total actualized revenue

+

Each year, the total revenue is equal to the total quantity extracted +multiplied by the blend price. The time series of revenues is aggregated +in one expected revenue by applying the discount rate; in other terms, a +revenue of $1000 next year is counted as $900 actualized, $810 if the +revenue is expected in two years, etc.

+
+
+

Total expected royalties

+

A mine that stays open must pay royalties (see the column royalties +in the DataFrame). Again, royalties from different years are actualized +using the discount rate.

+
+
+

Business objective

+

The business objective is to maximize the net actualized profit, that is +the difference between the total actualized revenue and total actualized +royalties.

+
+
+
+
+

How decision optimization can help

+
    +
  • Prescriptive analytics (decision optimization) technology recommends +actions that are based on desired outcomes. It takes into account +specific scenarios, resources, and knowledge of past and current +events. With this insight, your organization can make better +decisions and have greater control of business outcomes.

    +
  • +
  • Prescriptive analytics is the next step on the path to insight-based +actions. It creates value through synergy with predictive analytics, +which analyzes data to predict future outcomes.

    +
  • +
  • +
    Prescriptive analytics takes that insight to the next level by +suggesting the optimal way to handle that future situation. +Organizations that can act fast in dynamic conditions and make +superior decisions in uncertain environments gain a strong +competitive advantage.
    +

    +
    +
  • +
+

With prescriptive analytics, you can:

+
    +
  • Automate the complex decisions and trade-offs to better manage your +limited resources.
  • +
  • Take advantage of a future opportunity or mitigate a future risk.
  • +
  • Proactively update recommendations based on changing events.
  • +
  • Meet operational goals, increase customer loyalty, prevent threats +and fraud, and optimize business processes.
  • +
+
+
+

Checking minimum requirements

+

This notebook uses some features of pandas that are available in version +0.17.1 or above.

+
+
+

Use decision optimization

+
+

Step 1: Import the library

+

Run the following code to import the Decision Optimization CPLEX +Modeling library. The DOcplex library contains the two modeling +packages, Mathematical Programming and Constraint Programming, referred +to earlier.

+

If CPLEX is not installed, install CPLEX Community edition.

+
+
+

Step 2: Model the data

+
+

Mining Data

+

The mine data is provided as a pandas DataFrame. For each mine, we are +given the amount of royalty to pay when operating the mine, its ore +quality, and the maximum quantity that we can extract from the mine.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
royaltiesore_qualitymax_extract
range_mines
051.02.0
140.72.5
241.51.3
350.53.0
+
+
+

Blend quality data

+

Each year, the average blend quality of all ore extracted from the mines +must be greater than a minimum quality. This data is provided as a +pandas Series, the length of which is the plan horizon in years.

+
* Planning mining operations for: 5 years
+
+
+
count    5.000000
+mean     0.900000
+std      0.223607
+min      0.600000
+25%      0.800000
+50%      0.900000
+75%      1.000000
+max      1.200000
+dtype: float64
+
+
+
+
+

Additional (global) data

+

We need extra global data to run our planning model:

+
    +
  • a blend price (supposedly flat),
  • +
  • a maximum number of worked mines for any given years (typically 3), +and
  • +
  • a discount rate to compute the actualized revenue over the horizon.
  • +
+
+
+
+

Step 3: Prepare the data

+

The data is clean and does not need any cleansing.

+
+
+

Step 4: Set up the prescriptive model

+
+* system is: Windows 64bit
+* Python version 3.7.3, located at: c:\local\python373\python.exe
+* docplex is present, version is (2, 11, 0)
+* pandas is present, version is 0.25.1
+
+
+

Create DOcplex model

+

The model contains all the business constraints and defines the +objective.

+

What are the decisions we need to make?

+
    +
  • What mines do we work each year? (a yes/no decision)
  • +
  • What mine do we keep open each year? (again a yes/no decision)
  • +
  • What quantity is extracted from each mine, each year? (a positive +number)
  • +
+

We need to define some decision variables and add constraints to our +model related to these decisions.

+
+
+

Define the decision variables

+
Model: mining_pandas
+ - number of variables: 60
+   - binary=40, integer=0, continuous=20
+ - number of constraints: 0
+   - linear=0
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+

Express the business constraints

+
+
Constraint 1: Only open mines can be worked.
+

In order to take advantage of the pandas operations to create the +optimization model, decision variables are organized in a DataFrame +which is automatically indexed by ‘range_mines’ and ‘range_years’ +(that is, the same keys as the dictionary created by the +binary_var_matrix() method).

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
workopenore
range_minesrange_years
00work_0_0open_0_0ore_0_0
1work_0_1open_0_1ore_0_1
2work_0_2open_0_2ore_0_2
3work_0_3open_0_3ore_0_3
4work_0_4open_0_4ore_0_4
+

Now, let’s iterate over rows of the DataFrame “df_decision_vars” and +enforce the desired constraints.

+

The pandas method itertuples() returns a named tuple for each row of +a DataFrame. This method is efficient and convenient for iterating over +all rows.

+
Model: mining_pandas
+ - number of variables: 60
+   - binary=40, integer=0, continuous=20
+ - number of constraints: 20
+   - linear=20
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+
Constraint 2: Once closed, a mine stays closed.
+

These constraints are a little more complex: we state that the series of +open_vars[m,y] for a given mine m is decreasing. In other terms, +once some open_vars[m,y] is zero, all subsequent values for future +years are also zero.

+

Let’s use the pandas groupby operation to collect all “open” +decision variables for each mine in separate pandas Series. Then, we +iterate over the mines and invoke the aggregate() method, passing the +postOpenCloseConstraint() function as the argument. The pandas +aggregate() method invokes postOpenCloseConstraint() for each mine, +passing the associated Series of “open” decision variables as +argument. The postOpenCloseConstraint() function posts a set of +constraints on the sequence of “open” decision variables to enforce +that a mine cannot re-open.

+
range_mines
+0    posted 4 open/close constraints
+1    posted 4 open/close constraints
+2    posted 4 open/close constraints
+3    posted 4 open/close constraints
+Name: open, dtype: object
+
+
+
+
+
Constraint 3: The number of worked mines each year is limited.
+

This time, we use the pandas groupby operation to collect all +“work” decision variables for each year in separate pandas +Series. Each Series contains the “work” decision variables for all +mines. Then, the maximum number of worked mines constraint is enforced +by making sure that the sum of all the terms of each Series is smaller +or equal to the maximum number of worked mines. The aggregate() method +is used to post this constraint for each year.

+
range_years
+0    work_0_0+work_1_0+work_2_0+work_3_0 <= 3
+1    work_0_1+work_1_1+work_2_1+work_3_1 <= 3
+2    work_0_2+work_1_2+work_2_2+work_3_2 <= 3
+3    work_0_3+work_1_3+work_2_3+work_3_3 <= 3
+4    work_0_4+work_1_4+work_2_4+work_3_4 <= 3
+Name: work, dtype: object
+
+
+
+
+
Constraint 4: The quantity extracted is limited.
+

This constraint expresses two things: * Only a worked mine can give +ore. (Note that there is no minimum on the quantity extracted, this +model is very simplified). * The quantity extracted is less than the +mine’s maximum extracted quantity.

+

To illustrate the pandas join operation, let’s build a DataFrame +that joins the “df_decision_vars” DataFrame and the +“df_mines.max_extract” Series such that each row contains the +information to enforce the quantity extracted limit constraint. The +default behaviour of the pandas join operation is to look at the +index of left DataFrame and to append columns of the right Series or +DataFrame which have same index. Here is the result of this operation in +our case:

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
workopenoremax_extract
range_minesrange_years
00work_0_0open_0_0ore_0_02.0
1work_0_1open_0_1ore_0_12.0
2work_0_2open_0_2ore_0_22.0
3work_0_3open_0_3ore_0_32.0
4work_0_4open_0_4ore_0_42.0
10work_1_0open_1_0ore_1_02.5
1work_1_1open_1_1ore_1_12.5
2work_1_2open_1_2ore_1_22.5
3work_1_3open_1_3ore_1_32.5
4work_1_4open_1_4ore_1_42.5
+

Now, the constraint to limit quantity extracted is easily created by +iterating over all rows of the joined DataFrames:

+
Model: mining_pandas
+ - number of variables: 60
+   - binary=40, integer=0, continuous=20
+ - number of constraints: 61
+   - linear=61
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+
Blend constraints
+

We need to compute the total production of each year, stored in +auxiliary variables.

+

Again, we use the pandas groupby operation, this time to collect all +“ore” decision variables for each year in separate pandas +Series. The “blend” variable for a given year is the sum of “ore” +decision variables for the corresponding Series.

+
Model: mining_pandas
+ - number of variables: 65
+   - binary=40, integer=0, continuous=25
+ - number of constraints: 66
+   - linear=66
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+
Minimum average blend quality constraint
+

The average quality of the blend is the weighted sum of extracted +quantities, divided by the total extracted quantity. Because we cannot +use division here, we transform the inequality:

+
Model: mining_pandas
+ - number of variables: 65
+   - binary=40, integer=0, continuous=25
+ - number of constraints: 71
+   - linear=71
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+
+

KPIs and objective

+

Since both revenues and royalties are actualized using the same rate, we +compute an auxiliary discount rate array.

+
+
The discount rate array
+
range_years
+0    1.0000
+1    0.9000
+2    0.8100
+3    0.7290
+4    0.6561
+Name: discounts, dtype: float64
+
+
+
+
+
Total actualized revenue
+

Total expected revenue is the sum of actualized yearly revenues, +computed as total extracted quantities multiplied by the blend price +(assumed to be constant over the years in this simplified model).

+
+
+
Total actualized royalty cost
+

The total actualized royalty cost is computed for all open mines, also +actualized using the discounts array.

+

This time, we use the pandas join operation twice to build a +DataFrame that joins the “df_decision_vars” DataFrame with the +“df_mines.royalties” and “s_discounts” Series such that each row +contains the relevant information to calculate its contribution to the +total actualized royalty cost. The join with the “df_mines.royalties” +Series is performed by looking at the common “range_mines” index, +while the join with the “s_discounts” Series is performed by looking +at the common “range_years” index.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
workopenoreroyaltiesdiscountsdisc_royalties
range_minesrange_years
00work_0_0open_0_0ore_0_051.00005.0000
1work_0_1open_0_1ore_0_150.90004.5000
2work_0_2open_0_2ore_0_250.81004.0500
3work_0_3open_0_3ore_0_350.72903.6450
4work_0_4open_0_4ore_0_450.65613.2805
+

The total royalty is now calculated by multiplying the columns “open”, +“royalties” and “discounts”, and to sum over all rows. Using +pandas constructs, this can be written in a very compact way as +follows:

+
+
+
+

Express the objective

+

The business objective is to maximize the expected net profit, which is +the difference between revenue and royalties.

+
+
+

Solve with Decision Optimization

+
Model: mining_pandas
+ - number of variables: 65
+   - binary=40, integer=0, continuous=25
+ - number of constraints: 71
+   - linear=71
+ - parameters: defaults
+ - problem type is: MILP
+* model mining_pandas solved with objective = 161.438
+*  KPI: Total Actualized Revenue   = 214.674
+*  KPI: Total Actualized Royalties = 53.236
+
+
+
+
+
+

Step 5: Investigate the solution and then run an example analysis

+

To analyze the results, we again leverage pandas, by storing the +solution value of the ore variables in a new DataFrame. Note that we +use the float function of Python to convert the variable to its +solution value. Of course, this requires that the model be successfully +solved. For convenience, we want to organize the ore solution values +in a pivot table with years as row index and mines as columns. The +pandas unstack operation does this for us.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
mine1mine2mine3mine4
y12.002.5000001.30.0
y22.002.5000001.30.0
y31.950.0000001.30.0
y42.002.5000001.30.0
y52.002.1666671.30.0
+
+

Visualize results

+

In this section you’ll need the matplotlib module to visualize the +results of the solve.

+_images/mining_pandas_61_0.png +
+
+
+
+

Adding operational constraints.

+

What if we wish to add operational constraints? For example, let us +forbid work on certain pairs of (mines, years). Let’s see how this +impacts profit.

+

First, we add extra constraints to forbid work on those tuples.

+
Model: mining_pandas
+ - number of variables: 65
+   - binary=40, integer=0, continuous=25
+ - number of constraints: 77
+   - linear=77
+ - parameters: defaults
+ - problem type is: MILP
+
+
+

The previous solution does not satisfy these constraints; for example +(0, 1) means mine 1 should not be worked on year 2, but it was in fact +worked in the above solution.

+

To help CPLEX find a feasible solution, we will build a heuristic +feasible solution and pass it to CPLEX.

+
+
+

Using an heuristic start solution

+

In this section, we show how one can provide a start solution to CPLEX, +based on heuristics.

+

First, we build a solution in which mines are worked whenever possible, +that is for all couples (m,y) except for those in forced_stops.

+

Then we pass this solution to the model as a MIP start solution and +re-solve, this time with CPLEX logging turned on.

+
CPXPARAM_Read_DataCheck                          1
+2 of 5 MIP starts provided solutions.
+MIP start 'm1' defined initial solution with objective 108.9605.
+Tried aggregator 5 times.
+MIP Presolve eliminated 41 rows and 29 columns.
+MIP Presolve modified 23 coefficients.
+Aggregator did 36 substitutions.
+All rows and columns eliminated.
+Presolve time = 0.00 sec. (0.18 ticks)
+
+Root node processing (before b&c):
+  Real time             =    0.00 sec. (0.27 ticks)
+Parallel b&c, 12 threads:
+  Real time             =    0.00 sec. (0.00 ticks)
+  Sync time (average)   =    0.00 sec.
+  Wait time (average)   =    0.00 sec.
+                          ------------
+Total (root+branch&cut) =    0.00 sec. (0.27 ticks)
+* model mining_pandas solved with objective = 157.936
+*  KPI: Total Actualized Revenue   = 228.367
+*  KPI: Total Actualized Royalties = 70.431
+
+
+

You can see in the CPLEX log above, that our MIP start solution provided +a good start for CPLEX, defining an initial solution with objective +157.9355

+

Now we can again visualize the results with pandas and matplotlib.

+_images/mining_pandas_70_0.png +

As expected, mine1 is not worked in year 2: there is no blue bar at y2.

+
+
+

Summary

+

You learned how to set up and use IBM Decision Optimization CPLEX +Modeling for Python to formulate a Mathematical Programming model and +solve it with CPLEX.

+
+
+

References

+ +

Copyright � 2017-2019 IBM. IPLA licensed Sample Materials.

+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/nurses.html b/docs/2.24.232/mp/nurses.html new file mode 100644 index 0000000..0b27266 --- /dev/null +++ b/docs/2.24.232/mp/nurses.html @@ -0,0 +1,1173 @@ + + + + + + + + + nurses.py — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

nurses.py

+

This example solves the problem of finding an optimal assignment of nurses to shifts.

+
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2018
+# --------------------------------------------------------------------------
+
+from collections import namedtuple
+
+from docplex.mp.model import Model
+from docplex.util.environment import get_environment
+
+# ----------------------------------------------------------------------------
+# Initialize the problem data
+# ----------------------------------------------------------------------------
+
+# utility to convert a weekday string to an index in 0..6
+_all_days = ["monday",
+             "tuesday",
+             "wednesday",
+             "thursday",
+             "friday",
+             "saturday",
+             "sunday"]
+
+
+def day_to_day_week(day):
+    day_map = {day: d for d, day in enumerate(_all_days)}
+    return day_map[day.lower()]
+
+
+TWorkRules = namedtuple("TWorkRules", ["work_time_max"])
+TVacation = namedtuple("TVacation", ["nurse", "day"])
+TNursePair = namedtuple("TNursePair", ["firstNurse", "secondNurse"])
+TSkillRequirement = namedtuple("TSkillRequirement", ["department", "skill", "required"])
+
+
+NURSES = [("Anne", 11, 1, 25),
+          ("Bethanie", 4, 5, 28),
+          ("Betsy", 2, 2, 17),
+          ("Cathy", 2, 2, 17),
+          ("Cecilia", 9, 5, 38),
+          ("Chris", 11, 4, 38),
+          ("Cindy", 5, 2, 21),
+          ("David", 1, 2, 15),
+          ("Debbie", 7, 2, 24),
+          ("Dee", 3, 3, 21),
+          ("Gloria", 8, 2, 25),
+          ("Isabelle", 3, 1, 16),
+          ("Jane", 3, 4, 23),
+          ("Janelle", 4, 3, 22),
+          ("Janice", 2, 2, 17),
+          ("Jemma", 2, 4, 22),
+          ("Joan", 5, 3, 24),
+          ("Joyce", 8, 3, 29),
+          ("Jude", 4, 3, 22),
+          ("Julie", 6, 2, 22),
+          ("Juliet", 7, 4, 31),
+          ("Kate", 5, 3, 24),
+          ("Nancy", 8, 4, 32),
+          ("Nathalie", 9, 5, 38),
+          ("Nicole", 0, 2, 14),
+          ("Patricia", 1, 1, 13),
+          ("Patrick", 6, 1, 19),
+          ("Roberta", 3, 5, 26),
+          ("Suzanne", 5, 1, 18),
+          ("Vickie", 7, 1, 20),
+          ("Wendie", 5, 2, 21),
+          ("Zoe", 8, 3, 29)
+          ]
+
+SHIFTS = [("Emergency", "monday", 2, 8, 3, 5),
+          ("Emergency", "monday", 8, 12, 4, 7),
+          ("Emergency", "monday", 12, 18, 2, 5),
+          ("Emergency", "monday", 18, 2, 3, 7),
+          ("Consultation", "monday", 8, 12, 10, 13),
+          ("Consultation", "monday", 12, 18, 8, 12),
+          ("Cardiac_Care", "monday", 8, 12, 10, 13),
+          ("Cardiac_Care", "monday", 12, 18, 8, 12),
+          ("Emergency", "tuesday", 8, 12, 4, 7),
+          ("Emergency", "tuesday", 12, 18, 2, 5),
+          ("Emergency", "tuesday", 18, 2, 3, 7),
+          ("Consultation", "tuesday", 8, 12, 10, 13),
+          ("Consultation", "tuesday", 12, 18, 8, 12),
+          ("Cardiac_Care", "tuesday", 8, 12, 4, 7),
+          ("Cardiac_Care", "tuesday", 12, 18, 2, 5),
+          ("Cardiac_Care", "tuesday", 18, 2, 3, 7),
+          ("Emergency", "wednesday", 2, 8, 3, 5),
+          ("Emergency", "wednesday", 8, 12, 4, 7),
+          ("Emergency", "wednesday", 12, 18, 2, 5),
+          ("Emergency", "wednesday", 18, 2, 3, 7),
+          ("Consultation", "wednesday", 8, 12, 10, 13),
+          ("Consultation", "wednesday", 12, 18, 8, 12),
+          ("Emergency", "thursday", 2, 8, 3, 5),
+          ("Emergency", "thursday", 8, 12, 4, 7),
+          ("Emergency", "thursday", 12, 18, 2, 5),
+          ("Emergency", "thursday", 18, 2, 3, 7),
+          ("Consultation", "thursday", 8, 12, 10, 13),
+          ("Consultation", "thursday", 12, 18, 8, 12),
+          ("Emergency", "friday", 2, 8, 3, 5),
+          ("Emergency", "friday", 8, 12, 4, 7),
+          ("Emergency", "friday", 12, 18, 2, 5),
+          ("Emergency", "friday", 18, 2, 3, 7),
+          ("Consultation", "friday", 8, 12, 10, 13),
+          ("Consultation", "friday", 12, 18, 8, 12),
+          ("Emergency", "saturday", 2, 12, 5, 7),
+          ("Emergency", "saturday", 12, 20, 7, 9),
+          ("Emergency", "saturday", 20, 2, 12, 12),
+          ("Emergency", "sunday", 2, 12, 5, 7),
+          ("Emergency", "sunday", 12, 20, 7, 9),
+          ("Emergency", "sunday", 20, 2, 12, 12),
+          ("Geriatrics", "sunday", 8, 10, 2, 5)]
+
+NURSE_SKILLS = {"Anne": ["Anaesthesiology", "Oncology", "Pediatrics"],
+                "Betsy": ["Cardiac_Care"],
+                "Cathy": ["Anaesthesiology"],
+                "Cecilia": ["Anaesthesiology", "Oncology", "Pediatrics"],
+                "Chris": ["Cardiac_Care", "Oncology", "Geriatrics"],
+                "Gloria": ["Pediatrics"], "Jemma": ["Cardiac_Care"],
+                "Joyce": ["Anaesthesiology", "Pediatrics"],
+                "Julie": ["Geriatrics"], "Juliet": ["Pediatrics"],
+                "Kate": ["Pediatrics"], "Nancy": ["Cardiac_Care"],
+                "Nathalie": ["Anaesthesiology", "Geriatrics"],
+                "Patrick": ["Oncology"], "Suzanne": ["Pediatrics"],
+                "Wendie": ["Geriatrics"],
+                "Zoe": ["Cardiac_Care"]
+                }
+
+VACATIONS = [("Anne", "friday"),
+             ("Anne", "sunday"),
+             ("Cathy", "thursday"),
+             ("Cathy", "tuesday"),
+             ("Joan", "thursday"),
+             ("Joan", "saturday"),
+             ("Juliet", "monday"),
+             ("Juliet", "tuesday"),
+             ("Juliet", "thursday"),
+             ("Nathalie", "sunday"),
+             ("Nathalie", "thursday"),
+             ("Isabelle", "monday"),
+             ("Isabelle", "thursday"),
+             ("Patricia", "saturday"),
+             ("Patricia", "wednesday"),
+             ("Nicole", "friday"),
+             ("Nicole", "wednesday"),
+             ("Jude", "tuesday"),
+             ("Jude", "friday"),
+             ("Debbie", "saturday"),
+             ("Debbie", "wednesday"),
+             ("Joyce", "sunday"),
+             ("Joyce", "thursday"),
+             ("Chris", "thursday"),
+             ("Chris", "tuesday"),
+             ("Cecilia", "friday"),
+             ("Cecilia", "wednesday"),
+             ("Patrick", "saturday"),
+             ("Patrick", "sunday"),
+             ("Cindy", "sunday"),
+             ("Dee", "tuesday"),
+             ("Dee", "friday"),
+             ("Jemma", "friday"),
+             ("Jemma", "wednesday"),
+             ("Bethanie", "wednesday"),
+             ("Bethanie", "tuesday"),
+             ("Betsy", "monday"),
+             ("Betsy", "thursday"),
+             ("David", "monday"),
+             ("Gloria", "monday"),
+             ("Jane", "saturday"),
+             ("Jane", "sunday"),
+             ("Janelle", "wednesday"),
+             ("Janelle", "friday"),
+             ("Julie", "sunday"),
+             ("Kate", "tuesday"),
+             ("Kate", "monday"),
+             ("Nancy", "sunday"),
+             ("Roberta", "friday"),
+             ("Roberta", "saturday"),
+             ("Janice", "tuesday"),
+             ("Janice", "friday"),
+             ("Suzanne", "monday"),
+             ("Vickie", "wednesday"),
+             ("Vickie", "friday"),
+             ("Wendie", "thursday"),
+             ("Wendie", "saturday"),
+             ("Zoe", "saturday"),
+             ("Zoe", "sunday")]
+
+NURSE_ASSOCIATIONS = [("Isabelle", "Dee"),
+                      ("Anne", "Patrick")]
+
+NURSE_INCOMPATIBILITIES = [("Patricia", "Patrick"),
+                           ("Janice", "Wendie"),
+                           ("Suzanne", "Betsy"),
+                           ("Janelle", "Jane"),
+                           ("Gloria", "David"),
+                           ("Dee", "Jemma"),
+                           ("Bethanie", "Dee"),
+                           ("Roberta", "Zoe"),
+                           ("Nicole", "Patricia"),
+                           ("Vickie", "Dee"),
+                           ("Joan", "Anne")
+                           ]
+
+SKILL_REQUIREMENTS = [("Emergency", "Cardiac_Care", 1)]
+
+DEFAULT_WORK_RULES = TWorkRules(40)
+
+
+# ----------------------------------------------------------------------------
+# Prepare the data for modeling
+# ----------------------------------------------------------------------------
+# subclass the namedtuple to refine the str() method as the nurse's name
+class TNurse(namedtuple("TNurse1", ["name", "seniority", "qualification", "pay_rate"])):
+    def __str__(self):
+        return self.name
+
+
+# specialized namedtuple to redefine its str() method
+class TShift(namedtuple("TShift",
+                        ["department", "day", "start_time", "end_time", "min_requirement", "max_requirement"])):
+
+    def __str__(self):
+        # keep first two characters in department, uppercase
+        dept2 = self.department[0:4].upper()
+        # keep 3 days of weekday
+        dayname = self.day[0:3]
+        return '{}_{}_{:02d}'.format(dept2, dayname, self.start_time).replace(" ", "_")
+
+
+class ShiftActivity(object):
+    @staticmethod
+    def to_abstime(day_index, time_of_day):
+        """ Convert a pair (day_index, time) into a number of hours since Monday 00:00
+
+        :param day_index: The index of the day from 1 to 7 (Monday is 1).
+        :param time_of_day: An integer number of hours.
+
+        :return:
+        """
+        time = 24 * (day_index - 1)
+        time += time_of_day
+        return time
+
+    def __init__(self, weekday, start_time_of_day, end_time_of_day):
+        assert (start_time_of_day >= 0)
+        assert (start_time_of_day <= 24)
+        assert (end_time_of_day >= 0)
+        assert (end_time_of_day <= 24)
+
+        self._weekday = weekday
+        self._start_time_of_day = start_time_of_day
+        self._end_time_of_day = end_time_of_day
+        # conversion to absolute time.
+        start_day_index = day_to_day_week(self._weekday)
+        self.start_time = self.to_abstime(start_day_index, start_time_of_day)
+        end_day_index = start_day_index if end_time_of_day > start_time_of_day else start_day_index + 1
+        self.end_time = self.to_abstime(end_day_index, end_time_of_day)
+        assert self.end_time > self.start_time
+
+    @property
+    def duration(self):
+        return self.end_time - self.start_time
+
+    def overlaps(self, other_shift):
+        if not isinstance(other_shift, ShiftActivity):
+            return False
+        else:
+            return other_shift.end_time > self.start_time and other_shift.start_time < self.end_time
+
+
+def solve(model, **kwargs):
+    # Here, we set the number of threads for CPLEX to 2 and set the time limit to 2mins.
+    model.parameters.threads = 2
+    model.parameters.timelimit = 120  # nurse should not take more than that !
+    sol = model.solve(log_output=True, **kwargs)
+    if sol is not None:
+        print("solution for a cost of {}".format(model.objective_value))
+        print_information(model)
+        print_solution(model)
+        return model.objective_value
+    else:
+        print("* model is infeasible")
+        return None
+
+
+def load_data(model, shifts_, nurses_, nurse_skills, vacations_=None,
+              nurse_associations_=None, nurse_imcompatibilities_=None, verbose=True):
+    """ Usage: load_data(shifts, nurses, nurse_skills, vacations) """
+    model.number_of_overlaps = 0
+    model.work_rules = DEFAULT_WORK_RULES
+    model.shifts = [TShift(*shift_row) for shift_row in shifts_]
+    model.nurses = [TNurse(*nurse_row) for nurse_row in nurses_]
+    model.skill_requirements = SKILL_REQUIREMENTS
+    model.nurse_skills = nurse_skills
+    # transactional data
+    model.vacations = [TVacation(*vacation_row) for vacation_row in vacations_] if vacations_ else []
+    model.nurse_associations = [TNursePair(*npr) for npr in nurse_associations_]\
+    if nurse_associations_ else []
+    model.nurse_incompatibilities = [TNursePair(*npr) for npr in nurse_imcompatibilities_]\
+    if nurse_imcompatibilities_ else []
+
+    # computed
+    model.departments = set(sh.department for sh in model.shifts)
+
+    if verbose:
+        print('#nurses: {0}'.format(len(model.nurses)))
+        print('#shifts: {0}'.format(len(model.shifts)))
+        print('#vacations: {0}'.format(len(model.vacations)))
+        print("#associations=%d" % len(model.nurse_associations))
+        print("#incompatibilities=%d" % len(model.nurse_incompatibilities))
+
+
+def setup_data(model):
+    """ compute internal data """
+    # compute shift activities (start, end duration) and stor ethem in a dict indexed by shifts
+    model.shift_activities = {s: ShiftActivity(s.day, s.start_time, s.end_time) for s in model.shifts}
+    # map from nurse names to nurse tuples.
+    model.nurses_by_id = {n.name: n for n in model.nurses}
+
+
+def setup_variables(model):
+    all_nurses, all_shifts = model.nurses, model.shifts
+    # one binary variable for each pair (nurse, shift) equal to 1 iff nurse n is assigned to shift s
+    model.nurse_assignment_vars = model.binary_var_matrix(all_nurses, all_shifts, 'NurseAssigned')
+    # for each nurse, allocate one variable for work time
+    model.nurse_work_time_vars = model.continuous_var_dict(all_nurses, lb=0, name='NurseWorkTime')
+    # and two variables for over_average and under-average work time
+    model.nurse_over_average_time_vars = model.continuous_var_dict(all_nurses, lb=0,
+                                                                   name='NurseOverAverageWorkTime')
+    model.nurse_under_average_time_vars = model.continuous_var_dict(all_nurses, lb=0,
+                                                                    name='NurseUnderAverageWorkTime')
+    # finally the global average work time
+    model.average_nurse_work_time = model.continuous_var(lb=0, name='AverageWorkTime')
+
+
+def setup_constraints(model):
+    all_nurses = model.nurses
+    all_shifts = model.shifts
+    nurse_assigned = model.nurse_assignment_vars
+    nurse_work_time = model.nurse_work_time_vars
+    shift_activities = model.shift_activities
+    nurses_by_id = model.nurses_by_id
+    max_work_time = model.work_rules.work_time_max
+
+    # define average
+    model.add_constraint(
+        len(all_nurses) * model.average_nurse_work_time == model.sum(nurse_work_time[n] for n in all_nurses), "average")
+
+    # compute nurse work time , average and under, over
+    for n in all_nurses:
+        work_time_var = nurse_work_time[n]
+        model.add_constraint(
+            work_time_var == model.sum(nurse_assigned[n, s] * shift_activities[s].duration for s in all_shifts),
+            "work_time_{0!s}".format(n))
+
+        # relate over/under average worktime variables to the worktime variables
+        # the trick here is that variables have zero lower bound
+        # however, thse variables are not completely defined by this constraint,
+        # only their difference is.
+        # if these variables are part of the objective, CPLEX wil naturally minimize their value,
+        # as expected
+        model.add_constraint(
+            work_time_var == model.average_nurse_work_time
+            + model.nurse_over_average_time_vars[n]
+            - model.nurse_under_average_time_vars[n],
+            "average_work_time_{0!s}".format(n))
+
+        # state the maximum work time as a constraint, so that is can be relaxed,
+        # should the problem become infeasible.
+        model.add_constraint(work_time_var <= max_work_time, "max_time_{0!s}".format(n))
+
+    # vacations
+    v = 0
+    for vac_nurse_id, vac_day in model.vacations:
+        vac_n = nurses_by_id[vac_nurse_id]
+        for shift in (s for s in all_shifts if s.day == vac_day):
+            v += 1
+            model.add_constraint(nurse_assigned[vac_n, shift] == 0,
+                                 "medium_vacations_{0!s}_{1!s}_{2!s}".format(vac_n, vac_day, shift))
+    #print('#vacation cts: {0}'.format(v))
+
+    # a nurse cannot be assigned overlapping shifts
+    # post only one constraint per couple(s1, s2)
+    number_of_overlaps = 0
+    nb_shifts = len(all_shifts)
+    for i1 in range(nb_shifts):
+        for i2 in range(i1 + 1, nb_shifts):
+            s1 = all_shifts[i1]
+            s2 = all_shifts[i2]
+            if shift_activities[s1].overlaps(shift_activities[s2]):
+                number_of_overlaps += 1
+                for n in all_nurses:
+                    model.add_constraint(nurse_assigned[n, s1] + nurse_assigned[n, s2] <= 1,
+                                         "high_overlapping_{0!s}_{1!s}_{2!s}".format(s1, s2, n))
+    #print('# overlapping cts: {0}'.format(number_of_overlaps))
+
+    for s in all_shifts:
+        demand_min = s.min_requirement
+        demand_max = s.max_requirement
+        total_assigned = model.sum(nurse_assigned[n, s] for n in model.nurses)
+        model.add_constraint(total_assigned >= demand_min,
+                             "high_req_min_{0!s}_{1}".format(s, demand_min))
+        model.add_constraint(total_assigned <= demand_max,
+                             "medium_req_max_{0!s}_{1}".format(s, demand_max))
+
+    for (dept, skill, required) in model.skill_requirements:
+        if required > 0:
+            for dsh in (s for s in all_shifts if dept == s.department):
+                model.add_constraint(model.sum(nurse_assigned[skilled_nurse, dsh] for skilled_nurse in
+                                               (n for n in all_nurses if
+                                                n.name in model.nurse_skills.keys() and skill in model.nurse_skills[
+                                                    n.name])) >= required,
+                                     "high_required_{0!s}_{1!s}_{2!s}_{3!s}".format(dept, skill, required, dsh))
+
+    # nurse-nurse associations
+    # for each pair of associated nurses, their assignment variables are equal
+    # over all shifts.
+    c = 0
+    for (nurse_id1, nurse_id2) in model.nurse_associations:
+        if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id:
+            nurse1 = nurses_by_id[nurse_id1]
+            nurse2 = nurses_by_id[nurse_id2]
+            for s in all_shifts:
+                c += 1
+                ctname = 'medium_ct_nurse_assoc_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c)
+                model.add_constraint(nurse_assigned[nurse1, s] == nurse_assigned[nurse2, s], ctname)
+
+    # nurse-nurse incompatibilities
+    # for each pair of incompatible nurses, the sum of assigned variables is less than one
+    # in other terms, both nurses can never be assigned to the same shift
+    c = 0
+    for (nurse_id1, nurse_id2) in model.nurse_incompatibilities:
+        if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id:
+            nurse1 = nurses_by_id[nurse_id1]
+            nurse2 = nurses_by_id[nurse_id2]
+            for s in all_shifts:
+                c += 1
+                ctname = 'medium_ct_nurse_incompat_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c)
+                model.add_constraint(nurse_assigned[nurse1, s] + nurse_assigned[nurse2, s] <= 1, ctname)
+
+    model.total_number_of_assignments = model.sum(nurse_assigned[n, s] for n in all_nurses for s in all_shifts)
+    # model.nurse_costs = [model.nurse_assignment_vars[n, s] * n.pay_rate * model.shift_activities[s].duration
+    #                      for n in model.nurses for s in model.shifts]
+
+    def assignment_cost_f(ns):
+        n, s = ns
+        return n.pay_rate * model.shift_activities[s].duration
+
+    model.nurse_costs = model.scal_prod_f(nurse_assigned, assignment_cost_f)
+    model.total_salary_cost = model.sum(model.nurse_costs)
+
+
+def setup_objective(model):
+    model.add_kpi(model.total_salary_cost, "Total salary cost")
+    model.add_kpi(model.total_number_of_assignments, "Total number of assignments")
+    model.add_kpi(model.average_nurse_work_time, "average work time")
+
+    total_over_average_worktime = model.sum(model.nurse_over_average_time_vars[n] for n in model.nurses)
+    total_under_average_worktime = model.sum(model.nurse_under_average_time_vars[n] for n in model.nurses)
+    model.add_kpi(total_over_average_worktime, "Total over-average worktime")
+    model.add_kpi(total_under_average_worktime, "Total under-average worktime")
+    total_fairness = total_over_average_worktime + total_under_average_worktime
+    model.add_kpi(total_fairness, "Total fairness")
+
+    model.minimize(model.total_salary_cost + total_fairness + model.total_number_of_assignments)
+
+
+def print_information(model):
+    print("#shifts=%d" % len(model.shifts))
+    print("#nurses=%d" % len(model.nurses))
+    print("#vacations=%d" % len(model.vacations))
+    print("#nurse skills=%d" % len(model.nurse_skills))
+    print("#nurse associations=%d" % len(model.nurse_associations))
+    print("#incompatibilities=%d" % len(model.nurse_incompatibilities))
+    model.print_information()
+    model.report_kpis()
+
+
+def print_solution(model):
+    print("*************************** Solution ***************************")
+    print("Allocation By Department:")
+    for d in model.departments:
+        print("\t{}: {}".format(d, sum(
+            model.nurse_assignment_vars[n, s].solution_value for n in model.nurses for s in model.shifts if
+            s.department == d)))
+    print("Cost By Department:")
+    for d in model.departments:
+        cost = sum(
+            model.nurse_assignment_vars[n, s].solution_value * n.pay_rate * model.shift_activities[s].duration for n in
+            model.nurses for s in model.shifts if s.department == d)
+        print("\t{}: {}".format(d, cost))
+    print("Nurses Assignments")
+    for n in sorted(model.nurses):
+        total_hours = sum(
+            model.nurse_assignment_vars[n, s].solution_value * model.shift_activities[s].duration for s in model.shifts)
+        print("\t{}: total hours:{}".format(n.name, total_hours))
+        for s in model.shifts:
+            if model.nurse_assignment_vars[n, s].solution_value == 1:
+                print("\t\t{}: {} {}-{}".format(s.day, s.department, s.start_time, s.end_time))
+
+
+# ----------------------------------------------------------------------------
+# Build the model
+# ----------------------------------------------------------------------------
+
+def build(context=None, verbose=False, **kwargs):
+    mdl = Model("Nurses", context=context, **kwargs)
+    load_data(mdl, SHIFTS, NURSES, NURSE_SKILLS, VACATIONS, NURSE_ASSOCIATIONS,
+              NURSE_INCOMPATIBILITIES, verbose=verbose)
+    setup_data(mdl)
+    setup_variables(mdl)
+    setup_constraints(mdl)
+    setup_objective(mdl)
+    return mdl
+
+
+# ----------------------------------------------------------------------------
+# Solve the model and display the result
+# ----------------------------------------------------------------------------
+
+if __name__ == '__main__':
+    # Build model
+    model = build()
+
+    # Solve the model and print solution
+    solve(model)
+
+    # Save the CPLEX solution as "solution.json" program output
+    with get_environment().get_output_stream("solution.json") as fp:
+        model.solution.export(fp, "json")
+    model.end()
+
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/nurses_multiobj.html b/docs/2.24.232/mp/nurses_multiobj.html new file mode 100644 index 0000000..f5b4593 --- /dev/null +++ b/docs/2.24.232/mp/nurses_multiobj.html @@ -0,0 +1,1211 @@ + + + + + + + + + nurses_multiobj.py — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

nurses_multiobj.py

+

This example solves the problem of finding an optimal assignment of nurses to shifts, +using multi-objectives. Instead of minimizing an overall cost made of salary cost, +fairness and number of assignments, we use COS 12.9 multiobjective solve +to specify the 3 kpis.

+

This sample require COS 12.9.

+
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2018
+# --------------------------------------------------------------------------
+
+from collections import namedtuple
+
+from docplex.mp.model import Model
+from docplex.mp.constants import ObjectiveSense
+from docplex.util.environment import get_environment
+
+# ----------------------------------------------------------------------------
+# Initialize the problem data
+# ----------------------------------------------------------------------------
+
+# utility to convert a weekday string to an index in 0..6
+_all_days = ["monday",
+             "tuesday",
+             "wednesday",
+             "thursday",
+             "friday",
+             "saturday",
+             "sunday"]
+
+
+def day_to_day_week(day):
+    day_map = {day: d for d, day in enumerate(_all_days)}
+    return day_map[day.lower()]
+
+
+TWorkRules = namedtuple("TWorkRules", ["work_time_max"])
+TVacation = namedtuple("TVacation", ["nurse", "day"])
+TNursePair = namedtuple("TNursePair", ["firstNurse", "secondNurse"])
+TSkillRequirement = namedtuple("TSkillRequirement", ["department", "skill", "required"])
+
+
+NURSES = [("Anne", 11, 1, 25),
+          ("Bethanie", 4, 5, 28),
+          ("Betsy", 2, 2, 17),
+          ("Cathy", 2, 2, 17),
+          ("Cecilia", 9, 5, 38),
+          ("Chris", 11, 4, 38),
+          ("Cindy", 5, 2, 21),
+          ("David", 1, 2, 15),
+          ("Debbie", 7, 2, 24),
+          ("Dee", 3, 3, 21),
+          ("Gloria", 8, 2, 25),
+          ("Isabelle", 3, 1, 16),
+          ("Jane", 3, 4, 23),
+          ("Janelle", 4, 3, 22),
+          ("Janice", 2, 2, 17),
+          ("Jemma", 2, 4, 22),
+          ("Joan", 5, 3, 24),
+          ("Joyce", 8, 3, 29),
+          ("Jude", 4, 3, 22),
+          ("Julie", 6, 2, 22),
+          ("Juliet", 7, 4, 31),
+          ("Kate", 5, 3, 24),
+          ("Nancy", 8, 4, 32),
+          ("Nathalie", 9, 5, 38),
+          ("Nicole", 0, 2, 14),
+          ("Patricia", 1, 1, 13),
+          ("Patrick", 6, 1, 19),
+          ("Roberta", 3, 5, 26),
+          ("Suzanne", 5, 1, 18),
+          ("Vickie", 7, 1, 20),
+          ("Wendie", 5, 2, 21),
+          ("Zoe", 8, 3, 29)
+          ]
+
+SHIFTS = [("Emergency", "monday", 2, 8, 3, 5),
+          ("Emergency", "monday", 8, 12, 4, 7),
+          ("Emergency", "monday", 12, 18, 2, 5),
+          ("Emergency", "monday", 18, 2, 3, 7),
+          ("Consultation", "monday", 8, 12, 10, 13),
+          ("Consultation", "monday", 12, 18, 8, 12),
+          ("Cardiac_Care", "monday", 8, 12, 10, 13),
+          ("Cardiac_Care", "monday", 12, 18, 8, 12),
+          ("Emergency", "tuesday", 8, 12, 4, 7),
+          ("Emergency", "tuesday", 12, 18, 2, 5),
+          ("Emergency", "tuesday", 18, 2, 3, 7),
+          ("Consultation", "tuesday", 8, 12, 10, 13),
+          ("Consultation", "tuesday", 12, 18, 8, 12),
+          ("Cardiac_Care", "tuesday", 8, 12, 4, 7),
+          ("Cardiac_Care", "tuesday", 12, 18, 2, 5),
+          ("Cardiac_Care", "tuesday", 18, 2, 3, 7),
+          ("Emergency", "wednesday", 2, 8, 3, 5),
+          ("Emergency", "wednesday", 8, 12, 4, 7),
+          ("Emergency", "wednesday", 12, 18, 2, 5),
+          ("Emergency", "wednesday", 18, 2, 3, 7),
+          ("Consultation", "wednesday", 8, 12, 10, 13),
+          ("Consultation", "wednesday", 12, 18, 8, 12),
+          ("Emergency", "thursday", 2, 8, 3, 5),
+          ("Emergency", "thursday", 8, 12, 4, 7),
+          ("Emergency", "thursday", 12, 18, 2, 5),
+          ("Emergency", "thursday", 18, 2, 3, 7),
+          ("Consultation", "thursday", 8, 12, 10, 13),
+          ("Consultation", "thursday", 12, 18, 8, 12),
+          ("Emergency", "friday", 2, 8, 3, 5),
+          ("Emergency", "friday", 8, 12, 4, 7),
+          ("Emergency", "friday", 12, 18, 2, 5),
+          ("Emergency", "friday", 18, 2, 3, 7),
+          ("Consultation", "friday", 8, 12, 10, 13),
+          ("Consultation", "friday", 12, 18, 8, 12),
+          ("Emergency", "saturday", 2, 12, 5, 7),
+          ("Emergency", "saturday", 12, 20, 7, 9),
+          ("Emergency", "saturday", 20, 2, 12, 12),
+          ("Emergency", "sunday", 2, 12, 5, 7),
+          ("Emergency", "sunday", 12, 20, 7, 9),
+          ("Emergency", "sunday", 20, 2, 12, 12),
+          ("Geriatrics", "sunday", 8, 10, 2, 5)]
+
+NURSE_SKILLS = {"Anne": ["Anaesthesiology", "Oncology", "Pediatrics"],
+                "Betsy": ["Cardiac_Care"],
+                "Cathy": ["Anaesthesiology"],
+                "Cecilia": ["Anaesthesiology", "Oncology", "Pediatrics"],
+                "Chris": ["Cardiac_Care", "Oncology", "Geriatrics"],
+                "Gloria": ["Pediatrics"], "Jemma": ["Cardiac_Care"],
+                "Joyce": ["Anaesthesiology", "Pediatrics"],
+                "Julie": ["Geriatrics"], "Juliet": ["Pediatrics"],
+                "Kate": ["Pediatrics"], "Nancy": ["Cardiac_Care"],
+                "Nathalie": ["Anaesthesiology", "Geriatrics"],
+                "Patrick": ["Oncology"], "Suzanne": ["Pediatrics"],
+                "Wendie": ["Geriatrics"],
+                "Zoe": ["Cardiac_Care"]
+                }
+
+VACATIONS = [("Anne", "friday"),
+             ("Anne", "sunday"),
+             ("Cathy", "thursday"),
+             ("Cathy", "tuesday"),
+             ("Joan", "thursday"),
+             ("Joan", "saturday"),
+             ("Juliet", "monday"),
+             ("Juliet", "tuesday"),
+             ("Juliet", "thursday"),
+             ("Nathalie", "sunday"),
+             ("Nathalie", "thursday"),
+             ("Isabelle", "monday"),
+             ("Isabelle", "thursday"),
+             ("Patricia", "saturday"),
+             ("Patricia", "wednesday"),
+             ("Nicole", "friday"),
+             ("Nicole", "wednesday"),
+             ("Jude", "tuesday"),
+             ("Jude", "friday"),
+             ("Debbie", "saturday"),
+             ("Debbie", "wednesday"),
+             ("Joyce", "sunday"),
+             ("Joyce", "thursday"),
+             ("Chris", "thursday"),
+             ("Chris", "tuesday"),
+             ("Cecilia", "friday"),
+             ("Cecilia", "wednesday"),
+             ("Patrick", "saturday"),
+             ("Patrick", "sunday"),
+             ("Cindy", "sunday"),
+             ("Dee", "tuesday"),
+             ("Dee", "friday"),
+             ("Jemma", "friday"),
+             ("Jemma", "wednesday"),
+             ("Bethanie", "wednesday"),
+             ("Bethanie", "tuesday"),
+             ("Betsy", "monday"),
+             ("Betsy", "thursday"),
+             ("David", "monday"),
+             ("Gloria", "monday"),
+             ("Jane", "saturday"),
+             ("Jane", "sunday"),
+             ("Janelle", "wednesday"),
+             ("Janelle", "friday"),
+             ("Julie", "sunday"),
+             ("Kate", "tuesday"),
+             ("Kate", "monday"),
+             ("Nancy", "sunday"),
+             ("Roberta", "friday"),
+             ("Roberta", "saturday"),
+             ("Janice", "tuesday"),
+             ("Janice", "friday"),
+             ("Suzanne", "monday"),
+             ("Vickie", "wednesday"),
+             ("Vickie", "friday"),
+             ("Wendie", "thursday"),
+             ("Wendie", "saturday"),
+             ("Zoe", "saturday"),
+             ("Zoe", "sunday")]
+
+NURSE_ASSOCIATIONS = [("Isabelle", "Dee"),
+                      ("Anne", "Patrick")]
+
+NURSE_INCOMPATIBILITIES = [("Patricia", "Patrick"),
+                           ("Janice", "Wendie"),
+                           ("Suzanne", "Betsy"),
+                           ("Janelle", "Jane"),
+                           ("Gloria", "David"),
+                           ("Dee", "Jemma"),
+                           ("Bethanie", "Dee"),
+                           ("Roberta", "Zoe"),
+                           ("Nicole", "Patricia"),
+                           ("Vickie", "Dee"),
+                           ("Joan", "Anne")
+                           ]
+
+SKILL_REQUIREMENTS = [("Emergency", "Cardiac_Care", 1)]
+
+DEFAULT_WORK_RULES = TWorkRules(40)
+
+
+# ----------------------------------------------------------------------------
+# Prepare the data for modeling
+# ----------------------------------------------------------------------------
+# subclass the namedtuple to refine the str() method as the nurse's name
+class TNurse(namedtuple("TNurse1", ["name", "seniority", "qualification", "pay_rate"])):
+    def __str__(self):
+        return self.name
+
+
+# specialized namedtuple to redefine its str() method
+class TShift(namedtuple("TShift",
+                        ["department", "day", "start_time", "end_time", "min_requirement", "max_requirement"])):
+
+    def __str__(self):
+        # keep first two characters in department, uppercase
+        dept2 = self.department[0:4].upper()
+        # keep 3 days of weekday
+        dayname = self.day[0:3]
+        return '{}_{}_{:02d}'.format(dept2, dayname, self.start_time).replace(" ", "_")
+
+
+class ShiftActivity(object):
+    @staticmethod
+    def to_abstime(day_index, time_of_day):
+        """ Convert a pair (day_index, time) into a number of hours since Monday 00:00
+
+        :param day_index: The index of the day from 1 to 7 (Monday is 1).
+        :param time_of_day: An integer number of hours.
+
+        :return:
+        """
+        time = 24 * (day_index - 1)
+        time += time_of_day
+        return time
+
+    def __init__(self, weekday, start_time_of_day, end_time_of_day):
+        assert (start_time_of_day >= 0)
+        assert (start_time_of_day <= 24)
+        assert (end_time_of_day >= 0)
+        assert (end_time_of_day <= 24)
+
+        self._weekday = weekday
+        self._start_time_of_day = start_time_of_day
+        self._end_time_of_day = end_time_of_day
+        # conversion to absolute time.
+        start_day_index = day_to_day_week(self._weekday)
+        self.start_time = self.to_abstime(start_day_index, start_time_of_day)
+        end_day_index = start_day_index if end_time_of_day > start_time_of_day else start_day_index + 1
+        self.end_time = self.to_abstime(end_day_index, end_time_of_day)
+        assert self.end_time > self.start_time
+
+    @property
+    def duration(self):
+        return self.end_time - self.start_time
+
+    def overlaps(self, other_shift):
+        if not isinstance(other_shift, ShiftActivity):
+            return False
+        else:
+            return other_shift.end_time > self.start_time and other_shift.start_time < self.end_time
+
+
+def solve(model, **kwargs):
+    # Here, we set the number of threads for CPLEX to 2 and set the time limit to 2mins.
+    if kwargs.pop('parameter_sets', None) == None:
+        model.parameters.threads = 2
+        model.parameters.mip.tolerances.mipgap = 0.000001
+        model.parameters.timelimit = 120  # nurse should not take more than that !
+    sol = model.solve(log_output=True, **kwargs)
+    if sol is not None:
+        print("solution for a cost of {}".format(model.objective_value))
+        print_information(model)
+        # print_solution(model)
+        return model.objective_value
+    else:
+        print("* model is infeasible")
+        return None
+
+
+def load_data(model, shifts_, nurses_, nurse_skills, vacations_=None,
+              nurse_associations_=None, nurse_imcompatibilities_=None):
+    """ Usage: load_data(shifts, nurses, nurse_skills, vacations) """
+    model.number_of_overlaps = 0
+    model.work_rules = DEFAULT_WORK_RULES
+    model.shifts = [TShift(*shift_row) for shift_row in shifts_]
+    model.nurses = [TNurse(*nurse_row) for nurse_row in nurses_]
+    model.skill_requirements = SKILL_REQUIREMENTS
+    model.nurse_skills = nurse_skills
+    # transactional data
+    model.vacations = [TVacation(*vacation_row) for vacation_row in vacations_] if vacations_ else []
+    model.nurse_associations = [TNursePair(*npr) for npr in nurse_associations_]\
+    if nurse_associations_ else []
+    model.nurse_incompatibilities = [TNursePair(*npr) for npr in nurse_imcompatibilities_]\
+    if nurse_imcompatibilities_ else []
+
+    # computed
+    model.departments = set(sh.department for sh in model.shifts)
+
+
+    print('#nurses: {0}'.format(len(model.nurses)))
+    print('#shifts: {0}'.format(len(model.shifts)))
+    print('#vacations: {0}'.format(len(model.vacations)))
+    print("#associations=%d" % len(model.nurse_associations))
+    print("#incompatibilities=%d" % len(model.nurse_incompatibilities))
+
+
+def setup_data(model):
+    """ compute internal data """
+    # compute shift activities (start, end duration) and stor ethem in a dict indexed by shifts
+    model.shift_activities = {s: ShiftActivity(s.day, s.start_time, s.end_time) for s in model.shifts}
+    # map from nurse names to nurse tuples.
+    model.nurses_by_id = {n.name: n for n in model.nurses}
+
+
+def setup_variables(model):
+    all_nurses, all_shifts = model.nurses, model.shifts
+    # one binary variable for each pair (nurse, shift) equal to 1 iff nurse n is assigned to shift s
+    model.nurse_assignment_vars = model.binary_var_matrix(all_nurses, all_shifts, 'NurseAssigned')
+    # for each nurse, allocate one variable for work time
+    model.nurse_work_time_vars = model.continuous_var_dict(all_nurses, lb=0, name='NurseWorkTime')
+    # and two variables for over_average and under-average work time
+    model.nurse_over_average_time_vars = model.continuous_var_dict(all_nurses, lb=0,
+                                                                   name='NurseOverAverageWorkTime')
+    model.nurse_under_average_time_vars = model.continuous_var_dict(all_nurses, lb=0,
+                                                                    name='NurseUnderAverageWorkTime')
+    # finally the global average work time
+    model.average_nurse_work_time = model.continuous_var(lb=0, name='AverageWorkTime')
+
+
+def setup_constraints(model):
+    all_nurses = model.nurses
+    all_shifts = model.shifts
+    nurse_assigned = model.nurse_assignment_vars
+    nurse_work_time = model.nurse_work_time_vars
+    shift_activities = model.shift_activities
+    nurses_by_id = model.nurses_by_id
+    max_work_time = model.work_rules.work_time_max
+
+    # define average
+    model.add_constraint(
+        len(all_nurses) * model.average_nurse_work_time == model.sum(nurse_work_time[n] for n in all_nurses), "average")
+
+    # compute nurse work time , average and under, over
+    for n in all_nurses:
+        work_time_var = nurse_work_time[n]
+        model.add_constraint(
+            work_time_var == model.sum(nurse_assigned[n, s] * shift_activities[s].duration for s in all_shifts),
+            "work_time_{0!s}".format(n))
+
+        # relate over/under average worktime variables to the worktime variables
+        # the trick here is that variables have zero lower bound
+        # however, thse variables are not completely defined by this constraint,
+        # only their difference is.
+        # if these variables are part of the objective, CPLEX wil naturally minimize their value,
+        # as expected
+        model.add_constraint(
+            work_time_var == model.average_nurse_work_time
+            + model.nurse_over_average_time_vars[n]
+            - model.nurse_under_average_time_vars[n],
+            "average_work_time_{0!s}".format(n))
+
+        # state the maximum work time as a constraint, so that is can be relaxed,
+        # should the problem become infeasible.
+        model.add_constraint(work_time_var <= max_work_time, "max_time_{0!s}".format(n))
+
+    # vacations
+    v = 0
+    for vac_nurse_id, vac_day in model.vacations:
+        vac_n = nurses_by_id[vac_nurse_id]
+        for shift in (s for s in all_shifts if s.day == vac_day):
+            v += 1
+            model.add_constraint(nurse_assigned[vac_n, shift] == 0,
+                                 "medium_vacations_{0!s}_{1!s}_{2!s}".format(vac_n, vac_day, shift))
+    #print('#vacation cts: {0}'.format(v))
+
+    # a nurse cannot be assigned overlapping shifts
+    # post only one constraint per couple(s1, s2)
+    number_of_overlaps = 0
+    nb_shifts = len(all_shifts)
+    for i1 in range(nb_shifts):
+        for i2 in range(i1 + 1, nb_shifts):
+            s1 = all_shifts[i1]
+            s2 = all_shifts[i2]
+            if shift_activities[s1].overlaps(shift_activities[s2]):
+                number_of_overlaps += 1
+                for n in all_nurses:
+                    model.add_constraint(nurse_assigned[n, s1] + nurse_assigned[n, s2] <= 1,
+                                         "high_overlapping_{0!s}_{1!s}_{2!s}".format(s1, s2, n))
+    #print('# overlapping cts: {0}'.format(number_of_overlaps))
+
+    for s in all_shifts:
+        demand_min = s.min_requirement
+        demand_max = s.max_requirement
+        total_assigned = model.sum(nurse_assigned[n, s] for n in model.nurses)
+        model.add_constraint(total_assigned >= demand_min,
+                             "high_req_min_{0!s}_{1}".format(s, demand_min))
+        model.add_constraint(total_assigned <= demand_max,
+                             "medium_req_max_{0!s}_{1}".format(s, demand_max))
+
+    for (dept, skill, required) in model.skill_requirements:
+        if required > 0:
+            for dsh in (s for s in all_shifts if dept == s.department):
+                model.add_constraint(model.sum(nurse_assigned[skilled_nurse, dsh] for skilled_nurse in
+                                               (n for n in all_nurses if
+                                                n.name in model.nurse_skills.keys() and skill in model.nurse_skills[
+                                                    n.name])) >= required,
+                                     "high_required_{0!s}_{1!s}_{2!s}_{3!s}".format(dept, skill, required, dsh))
+
+    # nurse-nurse associations
+    # for each pair of associated nurses, their assignment variables are equal
+    # over all shifts.
+    c = 0
+    for (nurse_id1, nurse_id2) in model.nurse_associations:
+        if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id:
+            nurse1 = nurses_by_id[nurse_id1]
+            nurse2 = nurses_by_id[nurse_id2]
+            for s in all_shifts:
+                c += 1
+                ctname = 'medium_ct_nurse_assoc_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c)
+                model.add_constraint(nurse_assigned[nurse1, s] == nurse_assigned[nurse2, s], ctname)
+
+    # nurse-nurse incompatibilities
+    # for each pair of incompatible nurses, the sum of assigned variables is less than one
+    # in other terms, both nurses can never be assigned to the same shift
+    c = 0
+    for (nurse_id1, nurse_id2) in model.nurse_incompatibilities:
+        if nurse_id1 in nurses_by_id and nurse_id2 in nurses_by_id:
+            nurse1 = nurses_by_id[nurse_id1]
+            nurse2 = nurses_by_id[nurse_id2]
+            for s in all_shifts:
+                c += 1
+                ctname = 'medium_ct_nurse_incompat_{0!s}_{1!s}_{2:d}'.format(nurse_id1, nurse_id2, c)
+                model.add_constraint(nurse_assigned[nurse1, s] + nurse_assigned[nurse2, s] <= 1, ctname)
+
+    model.total_number_of_assignments = model.sum(nurse_assigned[n, s] for n in all_nurses for s in all_shifts)
+    model.nurse_costs = [model.nurse_assignment_vars[n, s] * n.pay_rate * model.shift_activities[s].duration for n in
+                         model.nurses
+                         for s in model.shifts]
+    model.total_salary_cost = model.sum(model.nurse_costs)
+
+
+def setup_objective(model):
+    model.add_kpi(model.total_salary_cost, "Total salary cost")
+    model.add_kpi(model.total_number_of_assignments, "Total number of assignments")
+    model.add_kpi(model.average_nurse_work_time, "average work time")
+
+    total_over_average_worktime = model.sum(model.nurse_over_average_time_vars[n] for n in model.nurses)
+    total_under_average_worktime = model.sum(model.nurse_under_average_time_vars[n] for n in model.nurses)
+    model.add_kpi(total_over_average_worktime, "Total over-average worktime")
+    model.add_kpi(total_under_average_worktime, "Total under-average worktime")
+    total_fairness = total_over_average_worktime + total_under_average_worktime
+    model.add_kpi(total_fairness, "Total fairness")
+
+    model.minimize_static_lex([model.total_salary_cost, total_fairness, model.total_number_of_assignments])
+
+
+def print_information(model):
+    print("#shifts=%d" % len(model.shifts))
+    print("#nurses=%d" % len(model.nurses))
+    print("#vacations=%d" % len(model.vacations))
+    print("#nurse skills=%d" % len(model.nurse_skills))
+    print("#nurse associations=%d" % len(model.nurse_associations))
+    print("#incompatibilities=%d" % len(model.nurse_incompatibilities))
+    model.print_information()
+    model.report_kpis()
+
+
+def print_solution(model):
+    print("*************************** Solution ***************************")
+    print("Allocation By Department:")
+    for d in model.departments:
+        print("\t{}: {}".format(d, sum(
+            model.nurse_assignment_vars[n, s].solution_value for n in model.nurses for s in model.shifts if
+            s.department == d)))
+    print("Cost By Department:")
+    for d in model.departments:
+        cost = sum(
+            model.nurse_assignment_vars[n, s].solution_value * n.pay_rate * model.shift_activities[s].duration for n in
+            model.nurses for s in model.shifts if s.department == d)
+        print("\t{}: {}".format(d, cost))
+    print("Nurses Assignments")
+    for n in sorted(model.nurses):
+        total_hours = sum(
+            model.nurse_assignment_vars[n, s].solution_value * model.shift_activities[s].duration for s in model.shifts)
+        print("\t{}: total hours:{}".format(n.name, total_hours))
+        for s in model.shifts:
+            if model.nurse_assignment_vars[n, s].solution_value == 1:
+                print("\t\t{}: {} {}-{}".format(s.day, s.department, s.start_time, s.end_time))
+
+
+# ----------------------------------------------------------------------------
+# Build the model
+# ----------------------------------------------------------------------------
+
+def build(context=None, **kwargs):
+    mdl = Model("Nurses", context=context, **kwargs)
+    load_data(mdl, SHIFTS, NURSES, NURSE_SKILLS, VACATIONS, NURSE_ASSOCIATIONS,
+              NURSE_INCOMPATIBILITIES)
+    setup_data(mdl)
+    setup_variables(mdl)
+    setup_constraints(mdl)
+    setup_objective(mdl)
+    return mdl
+
+
+# ----------------------------------------------------------------------------
+# Solve the model and display the result
+# ----------------------------------------------------------------------------
+
+if __name__ == '__main__':
+    # Build model
+    model = build()
+
+    # Solve the model and print solution
+    solve(model)
+
+    print(model.solve_details)
+
+    # Save the CPLEX solution as "solution.json" program output
+    with get_environment().get_output_stream("solution.json") as fp:
+        model.solution.export(fp, "json")
+
+    model.end()
+
+    model = build()
+    paramsets = model.build_multiobj_paramsets(timelimits=[70,60,50] , mipgaps=[0.000003, 0.000002, 0.000001])
+    solve(model, clean_before_solve=True, parameter_sets=paramsets)
+    print(model.solve_details)
+
+    model = build()
+    paramsets = model.create_parameter_sets()
+    cplex = model.get_cplex()
+    for i,p in enumerate(paramsets):
+        p.add(cplex.parameters.timelimit, 70+i)
+        p.add(cplex.parameters.mip.tolerances.mipgap, 0.000001*i)
+        p.add(cplex.parameters.threads, 2+i)
+    solve(model, clean_before_solve=True, parameter_sets=paramsets)
+    print(model.solve_details)
+    model.end()
+
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/nurses_pandas.html b/docs/2.24.232/mp/nurses_pandas.html new file mode 100644 index 0000000..f08b86c --- /dev/null +++ b/docs/2.24.232/mp/nurses_pandas.html @@ -0,0 +1,3418 @@ + + + + + + + + + The Nurse Assignment Problem — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

The Nurse Assignment Problem

+

This tutorial includes everything you need to set up IBM Decision +Optimization CPLEX Modeling for Python (DOcplex), build a Mathematical +Programming model, and get its solution by solving the model on the +cloud with IBM ILOG CPLEX Optimizer.

+

When you finish this tutorial, you’ll have a foundational knowledge of +Prescriptive Analytics.

+
+

This notebook is part of Prescriptive Analytics for +Python

+

It requires either an installation of CPLEX +Optimizers +or it can be run on IBM Watson Studio +Cloud (Sign up for a +free IBM Cloud +account +and you can start using Watson Studio Cloud right away).

+
+

Table of contents:

+ +
+
+

Describe the business problem

+

This notebook describes how to use CPLEX Modeling for Python together +with pandas to manage the assignment of nurses to shifts in a +hospital.

+

Nurses must be assigned to hospital shifts in accordance with various +skill and staffing constraints.

+

The goal of the model is to find an efficient balance between the +different objectives:

+
    +
  • minimize the overall cost of the plan and
  • +
  • assign shifts as fairly as possible.
  • +
+
+
+

How decision optimization can help

+
    +
  • Prescriptive analytics (decision optimization) technology recommends +actions that are based on desired outcomes. It takes into account +specific scenarios, resources, and knowledge of past and current +events. With this insight, your organization can make better +decisions and have greater control of business outcomes.

    +
  • +
  • Prescriptive analytics is the next step on the path to insight-based +actions. It creates value through synergy with predictive analytics, +which analyzes data to predict future outcomes.

    +
  • +
  • +
    Prescriptive analytics takes that insight to the next level by +suggesting the optimal way to handle that future situation. +Organizations that can act fast in dynamic conditions and make +superior decisions in uncertain environments gain a strong +competitive advantage.
    +

    +
    +
  • +
+

With prescriptive analytics, you can:

+
    +
  • Automate the complex decisions and trade-offs to better manage your +limited resources.
  • +
  • Take advantage of a future opportunity or mitigate a future risk.
  • +
  • Proactively update recommendations based on changing events.
  • +
  • Meet operational goals, increase customer loyalty, prevent threats +and fraud, and optimize business processes.
  • +
+
+
+

Checking minimum requirements

+

This notebook uses some features of pandas that are available in version +0.17.1 or above.

+
+
+

Use decision optimization

+
+

Step 1: Import the library

+

Run the following code to import the Decision Optimization CPLEX +Modeling library. The DOcplex library contains the two modeling +packages, Mathematical Programming (docplex.mp) and Constraint +Programming (docplex.cp).

+
+
+

Step 2: Model the data

+

The input data consists of several tables:

+
    +
  • The Departments table lists all departments in the scope of the +assignment.
  • +
  • The Skills table list all skills.
  • +
  • The Shifts table lists all shifts to be staffed. A shift contains a +department, a day in the week, plus the start and end times.
  • +
  • The Nurses table lists all nurses, identified by their names.
  • +
  • The NurseSkills table gives the skills of each nurse.
  • +
  • The SkillRequirements table lists the minimum number of persons +required for a given department and skill.
  • +
  • The NurseVacations table lists days off for each nurse.
  • +
  • The NurseAssociations table lists pairs of nurses who wish to work +together.
  • +
  • The NurseIncompatibilities table lists pairs of nurses who do not +want to work together.
  • +
+
+

Loading data from Excel with pandas

+

We load the data from an Excel file using pandas. Each sheet is read +into a separate pandas DataFrame.

+
#nurses = 32
+#shifts = 41
+#vacations = 59
+
+
+

In addition, we introduce some extra global data:

+
    +
  • The maximum work time for each nurse.
  • +
  • The maximum and minimum number of shifts worked by a nurse in a week.
  • +
+

Shifts are stored in a separate DataFrame.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
departmentdaystart_timeend_timemin_reqmax_req
shiftId
0EmergencyMonday2835
1EmergencyMonday81247
2EmergencyMonday121825
3EmergencyMonday18237
4ConsultationMonday8121013
5ConsultationMonday1218812
6Cardiac CareMonday8121013
7Cardiac CareMonday1218812
8EmergencyTuesday81247
9EmergencyTuesday121825
10EmergencyTuesday18237
11ConsultationTuesday8121013
12ConsultationTuesday1218812
13Cardiac CareTuesday81247
14Cardiac CareTuesday121825
15Cardiac CareTuesday18237
16EmergencyWednesday2835
17EmergencyWednesday81247
18EmergencyWednesday121825
19EmergencyWednesday18237
20ConsultationWednesday8121013
21ConsultationWednesday1218812
22EmergencyThursday2835
23EmergencyThursday81247
24EmergencyThursday121825
25EmergencyThursday18237
26ConsultationThursday8121013
27ConsultationThursday1218812
28EmergencyFriday2835
29EmergencyFriday81247
30EmergencyFriday121825
31EmergencyFriday18237
32ConsultationFriday8121013
33ConsultationFriday1218812
34EmergencySaturday21257
35EmergencySaturday122079
36EmergencySaturday2021212
37EmergencySunday21257
38EmergencySunday122079
39EmergencySunday202812
40GeriatricsSunday81025
+
+
+
+

Step 3: Prepare the data

+

We need to precompute additional data for shifts. For each shift, we +need the start time and end time expressed in hours, counting from the +beginning of the week: Monday 8am is converted to 8, Tuesday 8am is +converted to 24+8 = 32, and so on.

+
+

Sub-step #1

+

We start by adding an extra column dow (day of week) which converts +the string “day” into an integer in 0..6 (Monday is 0, Sunday is 6).

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
departmentdaystart_timeend_timemin_reqmax_reqdow
shiftId
0EmergencyMonday28350
1EmergencyMonday812470
2EmergencyMonday1218250
3EmergencyMonday182370
4ConsultationMonday81210130
5ConsultationMonday12188120
6Cardiac CareMonday81210130
7Cardiac CareMonday12188120
8EmergencyTuesday812471
9EmergencyTuesday1218251
10EmergencyTuesday182371
11ConsultationTuesday81210131
12ConsultationTuesday12188121
13Cardiac CareTuesday812471
14Cardiac CareTuesday1218251
15Cardiac CareTuesday182371
16EmergencyWednesday28352
17EmergencyWednesday812472
18EmergencyWednesday1218252
19EmergencyWednesday182372
20ConsultationWednesday81210132
21ConsultationWednesday12188122
22EmergencyThursday28353
23EmergencyThursday812473
24EmergencyThursday1218253
25EmergencyThursday182373
26ConsultationThursday81210133
27ConsultationThursday12188123
28EmergencyFriday28354
29EmergencyFriday812474
30EmergencyFriday1218254
31EmergencyFriday182374
32ConsultationFriday81210134
33ConsultationFriday12188124
34EmergencySaturday212575
35EmergencySaturday1220795
36EmergencySaturday20212125
37EmergencySunday212576
38EmergencySunday1220796
39EmergencySunday2028126
40GeriatricsSunday810256
+
+
+

Sub-step #2 : Compute the absolute start time of each shift.

+

Computing the start time in the week is easy: just add 24*dow to +column start_time. The result is stored in a new column wstart.

+
+
+

Sub-Step #3 : Compute the absolute end time of each shift.

+

Computing the absolute end time is a little more complicated as certain +shifts span across midnight. For example, Shift #3 starts on Monday at +18:00 and ends Tuesday at 2:00 AM. The absolute end time of Shift #3 is +26, not 2. The general rule for computing absolute end time is:

+

abs_end_time = end_time + 24 * dow + (start_time>= end_time ? 24 : 0)

+

Again, we use pandas to add a new calculated column wend. This is +done by using the pandas apply method with an anonymous lambda +function over rows. The raw=True parameter prevents the creation of +a pandas Series for each row, which improves the performance +significantly on large data sets.

+
+
+

Sub-step #4 : Compute the duration of each shift.

+

Computing the duration of each shift is now a straightforward difference +of columns. The result is stored in column duration.

+
+
+

Sub-step #5 : Compute the minimum demand for each shift.

+

Minimum demand is the product of duration (in hours) by the minimum +required number of nurses. Thus, in number of nurse-hours, this demand +is stored in another new column min_demand.

+

Finally, we display the updated shifts DataFrame with all calculated +columns.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
departmentdaystart_timeend_timemin_reqmax_reqdowwstartwenddurationmin_demand
shiftId
0EmergencyMonday2835028618
1EmergencyMonday812470812416
2EmergencyMonday12182501218612
3EmergencyMonday1823701826824
4ConsultationMonday81210130812440
5ConsultationMonday121881201218648
6Cardiac CareMonday81210130812440
7Cardiac CareMonday121881201218648
8EmergencyTuesday8124713236416
9EmergencyTuesday12182513642612
10EmergencyTuesday1823714250824
11ConsultationTuesday812101313236440
12ConsultationTuesday121881213642648
13Cardiac CareTuesday8124713236416
14Cardiac CareTuesday12182513642612
15Cardiac CareTuesday1823714250824
16EmergencyWednesday283525056618
17EmergencyWednesday8124725660416
18EmergencyWednesday12182526066612
19EmergencyWednesday1823726674824
20ConsultationWednesday812101325660440
21ConsultationWednesday121881226066648
22EmergencyThursday283537480618
23EmergencyThursday8124738084416
24EmergencyThursday12182538490612
25EmergencyThursday1823739098824
26ConsultationThursday812101338084440
27ConsultationThursday121881238490648
28EmergencyFriday2835498104618
29EmergencyFriday812474104108416
30EmergencyFriday1218254108114612
31EmergencyFriday182374114122824
32ConsultationFriday81210134104108440
33ConsultationFriday12188124108114648
34EmergencySaturday2125751221321050
35EmergencySaturday1220795132140856
36EmergencySaturday20212125140146672
37EmergencySunday2125761461561050
38EmergencySunday1220796156164856
39EmergencySunday2028126164170648
40GeriatricsSunday81025615215424
+
+
+
+

Step 4: Set up the prescriptive model

+
+* system is: Windows 64bit
+* Python version 3.7.3, located at: c:\local\python373\python.exe
+* docplex is present, version is (2, 11, 0)
+* pandas is present, version is 0.25.1
+
+
+

Create the DOcplex model

+

The model contains all the business constraints and defines the +objective.

+

We now use CPLEX Modeling for Python to build a Mixed Integer +Programming (MIP) model for this problem.

+
+
+

Define the decision variables

+

For each (nurse, shift) pair, we create one binary variable that is +equal to 1 when the nurse is assigned to the shift.

+

We use the binary_var_matrix method of class Model, as each +binary variable is indexed by two objects: one nurse and one shift.

+
+
+

Express the business constraints

+
+
Overlapping shifts
+

Some shifts overlap in time, and thus cannot be assigned to the same +nurse. To check whether two shifts overlap in time, we start by ordering +all shifts with respect to their wstart and duration properties. +Then, for each shift, we iterate over the subsequent shifts in this +ordered list to easily compute the subset of overlapping shifts.

+

We use pandas operations to implement this algorithm. But first, we +organize all decision variables in a DataFrame.

+

For convenience, we also organize the decision variables in a pivot +table with nurses as row index and shifts as columns. The pandas +unstack operation does this.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
assigned
all_shifts0123456789...31323334353637383940
all_nurses
Anneassign_Anne_0assign_Anne_1assign_Anne_2assign_Anne_3assign_Anne_4assign_Anne_5assign_Anne_6assign_Anne_7assign_Anne_8assign_Anne_9...assign_Anne_31assign_Anne_32assign_Anne_33assign_Anne_34assign_Anne_35assign_Anne_36assign_Anne_37assign_Anne_38assign_Anne_39assign_Anne_40
Bethanieassign_Bethanie_0assign_Bethanie_1assign_Bethanie_2assign_Bethanie_3assign_Bethanie_4assign_Bethanie_5assign_Bethanie_6assign_Bethanie_7assign_Bethanie_8assign_Bethanie_9...assign_Bethanie_31assign_Bethanie_32assign_Bethanie_33assign_Bethanie_34assign_Bethanie_35assign_Bethanie_36assign_Bethanie_37assign_Bethanie_38assign_Bethanie_39assign_Bethanie_40
Betsyassign_Betsy_0assign_Betsy_1assign_Betsy_2assign_Betsy_3assign_Betsy_4assign_Betsy_5assign_Betsy_6assign_Betsy_7assign_Betsy_8assign_Betsy_9...assign_Betsy_31assign_Betsy_32assign_Betsy_33assign_Betsy_34assign_Betsy_35assign_Betsy_36assign_Betsy_37assign_Betsy_38assign_Betsy_39assign_Betsy_40
Cathyassign_Cathy_0assign_Cathy_1assign_Cathy_2assign_Cathy_3assign_Cathy_4assign_Cathy_5assign_Cathy_6assign_Cathy_7assign_Cathy_8assign_Cathy_9...assign_Cathy_31assign_Cathy_32assign_Cathy_33assign_Cathy_34assign_Cathy_35assign_Cathy_36assign_Cathy_37assign_Cathy_38assign_Cathy_39assign_Cathy_40
Ceciliaassign_Cecilia_0assign_Cecilia_1assign_Cecilia_2assign_Cecilia_3assign_Cecilia_4assign_Cecilia_5assign_Cecilia_6assign_Cecilia_7assign_Cecilia_8assign_Cecilia_9...assign_Cecilia_31assign_Cecilia_32assign_Cecilia_33assign_Cecilia_34assign_Cecilia_35assign_Cecilia_36assign_Cecilia_37assign_Cecilia_38assign_Cecilia_39assign_Cecilia_40
+

5 rows � 41 columns

+

We create a DataFrame representing a list of shifts sorted by “wstart” +and “duration”. This sorted list will be used to easily detect +overlapping shifts.

+

Note that indices are reset after sorting so that the DataFrame can be +indexed with respect to the index in the sorted list and not the +original unsorted list. This is the purpose of the reset_index() +operation which also adds a new column named “shiftId” with the +original index.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
shiftIdwstartwend
0028
11812
24812
36812
421218
+

Next, we state that for any pair of shifts that overlap in time, a nurse +can be assigned to only one of the two.

+
#incompatible shift constraints: 640
+
+
+
+
+
Vacations
+

When the nurse is on vacation, he cannot be assigned to any shift +starting that day.

+

We use the pandas merge operation to create a join between the +“df_vacations”, “df_shifts”, and “df_assigned” DataFrames. Each +row of the resulting DataFrame contains the assignment decision variable +corresponding to the matching (nurse, shift) pair.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nursedaydowshiftIdall_nursesall_shiftsassigned
0AnneFriday428Anne28assign_Anne_28
1AnneFriday429Anne29assign_Anne_29
2AnneFriday430Anne30assign_Anne_30
3AnneFriday431Anne31assign_Anne_31
4AnneFriday432Anne32assign_Anne_32
+
# vacation forbids: 342 assignments
+
+
+
+
+
Associations
+

Some pairs of nurses get along particularly well, so we wish to assign +them together as a team. In other words, for every such couple and for +each shift, both assignment variables should always be equal. Either +both nurses work the shift, or both do not.

+

In the same way we modeled vacations, we use the pandas merge +operation to create a DataFrame for which each row contains the pair of +nurse-shift assignment decision variables matching each association.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nurse1nurse2all_nurses_1all_shiftsassigned_1all_nurses_2assigned_2
0IsabelleDeeIsabelle0assign_Isabelle_0Deeassign_Dee_0
1IsabelleDeeIsabelle1assign_Isabelle_1Deeassign_Dee_1
2IsabelleDeeIsabelle2assign_Isabelle_2Deeassign_Dee_2
3IsabelleDeeIsabelle3assign_Isabelle_3Deeassign_Dee_3
4IsabelleDeeIsabelle4assign_Isabelle_4Deeassign_Dee_4
+

The associations constraint can now easily be formulated by iterating on +the rows of the “df_preferred_assign” DataFrame.

+
+
+
Incompatibilities
+

Similarly, certain pairs of nurses do not get along well, and we want to +avoid having them together on a shift. In other terms, for each shift, +both nurses of an incompatible pair cannot be assigned together to the +sift. Again, we state a logical OR between the two assignments: at most +one nurse from the pair can work the shift.

+

We first create a DataFrame whose rows contain pairs of invalid +assignment decision variables, using the same pandas merge +operations as in the previous step.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nurse1nurse2all_nurses_1all_shiftsassigned_1all_nurses_2assigned_2
0PatriciaPatrickPatricia0assign_Patricia_0Patrickassign_Patrick_0
1PatriciaPatrickPatricia1assign_Patricia_1Patrickassign_Patrick_1
2PatriciaPatrickPatricia2assign_Patricia_2Patrickassign_Patrick_2
3PatriciaPatrickPatricia3assign_Patricia_3Patrickassign_Patrick_3
4PatriciaPatrickPatricia4assign_Patricia_4Patrickassign_Patrick_4
+

The incompatibilities constraint can now easily be formulated, by +iterating on the rows of the “df_incompatible_assign” DataFrame.

+
+
+
Constraints on work time
+

Regulations force constraints on the total work time over a week; and we +compute this total work time in a new variable. We store the variable in +an extra column in the nurse DataFrame.

+

The variable is declared as continuous though it contains only integer +values. This is done to avoid adding unnecessary integer variables for +the branch and bound algorithm. These variables are not true decision +variables; they are used to express work constraints.

+

From a pandas perspective, we apply a function over the rows of the +nurse DataFrame to create this variable and store it into a new column +of the DataFrame.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
seniorityqualificationpay_rateworktime
name
Anne11125worktime_Anne
Bethanie4528worktime_Bethanie
Betsy2217worktime_Betsy
Cathy2217worktime_Cathy
Cecilia9538worktime_Cecilia
Chris11438worktime_Chris
Cindy5221worktime_Cindy
David1215worktime_David
Debbie7224worktime_Debbie
Dee3321worktime_Dee
Gloria8225worktime_Gloria
Isabelle3116worktime_Isabelle
Jane3423worktime_Jane
Janelle4322worktime_Janelle
Janice2217worktime_Janice
Jemma2422worktime_Jemma
Joan5324worktime_Joan
Joyce8329worktime_Joyce
Jude4322worktime_Jude
Julie6222worktime_Julie
Juliet7431worktime_Juliet
Kate5324worktime_Kate
Nancy8432worktime_Nancy
Nathalie9538worktime_Nathalie
Nicole0214worktime_Nicole
Patricia1113worktime_Patricia
Patrick6119worktime_Patrick
Roberta3526worktime_Roberta
Suzanne5118worktime_Suzanne
Vickie7120worktime_Vickie
Wendie5221worktime_Wendie
Zoe8329worktime_Zoe
+

Define total work time

+

Work time variables must be constrained to be equal to the sum of hours +actually worked.

+

We use the pandas groupby operation to collect all assignment +decision variables for each nurse in a separate series. Then, we iterate +over nurses to post a constraint calculating the actual worktime for +each nurse as the dot product of the series of nurse-shift assignments +with the series of shift durations.

+
Model: nurses
+ - number of variables: 1344
+   - binary=1312, integer=0, continuous=32
+ - number of constraints: 1547
+   - linear=1547
+ - parameters: defaults
+ - problem type is: MILP
+
+
+

Maximum work time

+

For each nurse, we add a constraint to enforce the maximum work time for +a week. Again we use the apply method, this time with an anonymous +lambda function.

+
name
+Anne            worktime_Anne <= 40
+Bethanie    worktime_Bethanie <= 40
+Betsy          worktime_Betsy <= 40
+Cathy          worktime_Cathy <= 40
+Cecilia      worktime_Cecilia <= 40
+Chris          worktime_Chris <= 40
+Cindy          worktime_Cindy <= 40
+David          worktime_David <= 40
+Debbie        worktime_Debbie <= 40
+Dee              worktime_Dee <= 40
+Gloria        worktime_Gloria <= 40
+Isabelle    worktime_Isabelle <= 40
+Jane            worktime_Jane <= 40
+Janelle      worktime_Janelle <= 40
+Janice        worktime_Janice <= 40
+Jemma          worktime_Jemma <= 40
+Joan            worktime_Joan <= 40
+Joyce          worktime_Joyce <= 40
+Jude            worktime_Jude <= 40
+Julie          worktime_Julie <= 40
+Juliet        worktime_Juliet <= 40
+Kate            worktime_Kate <= 40
+Nancy          worktime_Nancy <= 40
+Nathalie    worktime_Nathalie <= 40
+Nicole        worktime_Nicole <= 40
+Patricia    worktime_Patricia <= 40
+Patrick      worktime_Patrick <= 40
+Roberta      worktime_Roberta <= 40
+Suzanne      worktime_Suzanne <= 40
+Vickie        worktime_Vickie <= 40
+Wendie        worktime_Wendie <= 40
+Zoe              worktime_Zoe <= 40
+Name: worktime, dtype: object
+
+
+
+
+
Minimum requirement for shifts
+

Each shift requires a minimum number of nurses. For each shift, the sum +over all nurses of assignments to this shift must be greater than the +minimum requirement.

+

The pandas groupby operation is invoked to collect all assignment +decision variables for each shift in a separate series. Then, we iterate +over shifts to post the constraint enforcing the minimum number of nurse +assignments for each shift.

+
+
+
+

Express the objective

+

The objective mixes different (and contradictory) KPIs.

+

The first KPI is the total salary cost, computed as the sum of work +times over all nurses, weighted by pay rate.

+

We compute this KPI as an expression from the variables we previously +defined by using the panda summation over the DOcplex objects.

+
DecisionKPI(name=Total salary cost,expr=25worktime_Anne+28worktime_Bethanie+17worktime_Betsy+17worktime_..)
+
+
+
+
Minimizing salary cost
+

In a preliminary version of the model, we minimize the total salary +cost. This is accomplished using the Model.minimize() method.

+
Model: nurses
+ - number of variables: 1344
+   - binary=1312, integer=0, continuous=32
+ - number of constraints: 1588
+   - linear=1588
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+
+

Solve with Decision Optimization

+

Now we have everything we need to solve the model, using +Model.solve(). The following cell solves using your local CPLEX (if +any, and provided you have added it to your PYTHONPATH variable).

+
CPXPARAM_Read_DataCheck                          1
+CPXPARAM_MIP_Tolerances_MIPGap                   1.0000000000000001e-05
+Tried aggregator 2 times.
+MIP Presolve eliminated 997 rows and 379 columns.
+MIP Presolve modified 90 coefficients.
+Aggregator did 41 substitutions.
+Reduced MIP has 550 rows, 922 columns, and 2862 nonzeros.
+Reduced MIP has 892 binaries, 0 generals, 0 SOSs, and 0 indicators.
+Presolve time = 0.00 sec. (3.68 ticks)
+Probing time = 0.00 sec. (0.50 ticks)
+Tried aggregator 1 time.
+Reduced MIP has 550 rows, 922 columns, and 2862 nonzeros.
+Reduced MIP has 892 binaries, 30 generals, 0 SOSs, and 0 indicators.
+Presolve time = 0.00 sec. (2.03 ticks)
+Probing time = 0.00 sec. (0.50 ticks)
+Clique table members: 479.
+MIP emphasis: balance optimality and feasibility.
+MIP search method: dynamic search.
+Parallel mode: deterministic, using up to 12 threads.
+Root relaxation solution time = 0.00 sec. (4.56 ticks)
+
+        Nodes                                         Cuts/
+   Node  Left     Objective  IInf  Best Integer    Best Bound    ItCnt     Gap
+
+      0     0    28824.0000    48                  28824.0000      473
+      0     0    28824.0000    62                    Cuts: 98      649
+      0     0    28824.0000    35                    Cuts: 44      716
+      0     0    28824.0000    41                    Cuts: 55      795
+*     0+    0                        29100.0000    28824.0000             0.95%
+*     0+    0                        29068.0000    28824.0000             0.84%
+      0     2    28824.0000    10    29068.0000    28824.0000      795    0.84%
+Elapsed time = 0.24 sec. (139.30 ticks, tree = 0.02 MB, solutions = 2)
+*    10+    4                        29014.0000    28824.0000             0.65%
+*    11+    2                        29010.0000    28824.0000             0.64%
+*    15+    2                        28982.0000    28824.0000             0.55%
+*    26+    1                        28944.0000    28824.0000             0.41%
+*    47+   10                        28936.0000    28824.0000             0.39%
+*  1611+ 1392                        28888.0000    28824.0000             0.22%
+*  4006+ 3397                        28842.0000    28824.0000             0.06%
+*  4152+ 3293                        28824.0000    28824.0000             0.00%
+   4253  3414    28824.0000     6    28824.0000    28824.0000    73849    0.00%
+
+GUB cover cuts applied:  12
+Cover cuts applied:  76
+Flow cuts applied:  5
+Mixed integer rounding cuts applied:  11
+Zero-half cuts applied:  13
+Lift and project cuts applied:  5
+
+Root node processing (before b&c):
+  Real time             =    0.24 sec. (139.27 ticks)
+Parallel b&c, 12 threads:
+  Real time             =    0.47 sec. (270.80 ticks)
+  Sync time (average)   =    0.08 sec.
+  Wait time (average)   =    0.00 sec.
+                          ------------
+Total (root+branch&cut) =    0.70 sec. (410.07 ticks)
+* model nurses solved with objective = 28824.000
+*  KPI: Total salary cost = 28824.000
+
+
+
+
+
+

Step 5: Investigate the solution and then run an example analysis

+

We take advantage of pandas to analyze the results. First we store the +solution values of the assignment variables into a new pandas Series.

+

Calling solution_value on a DOcplex variable returns its value in +the solution (provided the model has been successfully solved).

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
all_shifts0123456789...31323334353637383940
all_nurses
Anne0.01.00.00.00.01.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
Bethanie0.01.00.00.00.00.00.01.00.00.0...0.00.01.01.00.00.00.00.00.00.0
Betsy0.00.00.00.00.00.00.00.00.00.0...1.01.00.00.00.00.00.00.00.00.0
Cathy0.00.00.00.00.00.01.01.00.00.0...0.01.01.00.00.01.01.00.00.00.0
Cecilia0.00.00.01.00.01.01.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
+

5 rows � 41 columns

+
+

Analyzing how worktime is distributed

+

Let’s analyze how worktime is distributed among nurses.

+

First, we compute the global average work time as the total minimum +requirement in hours, divided by number of nurses.

+
* theoretical average work time is 39 h
+
+
+

Let’s analyze the series of deviations to the average, stored in a +pandas Series.

+
* the sum of absolute deviations from mean is 58.0
+
+
+

To see how work time is distributed among nurses, print a histogram of +work time values. Note that, as all time data are integers, work times +in the solution can take only integer values.

+
Text(0.5, 0, 'worktime')
+
+
+_images/nurses_pandas_72_1.png +
+
+

How shifts are distributed

+

Let’s now analyze the solution from the number of shifts perspective. +How many shifts does each nurse work? Are these shifts fairly +distributed amongst nurses?

+

We compute a new column in our result DataFrame for the number of shifts +worked, by summing rows (the “axis=1” argument in the sum() call +indicates to pandas that each sum is performed by row instead of +column):

+
Text(0, 0.5, '#shifts worked')
+
+
+_images/nurses_pandas_74_1.png +

We see that one nurse works significantly fewer shifts than others do. +What is the average number of shifts worked by a nurse? This is equal to +the total demand divided by the number of nurses.

+

Of course, this yields a fractional number of shifts that is not +practical, but nonetheless will help us quantify the fairness in shift +distribution.

+
-- expected avg #shifts worked is 6.875
+-- total absolute deviation to mean #shifts is 16.25
+
+
+
+
+
+

Introducing a fairness goal

+

As the above diagram suggests, the distribution of shifts could be +improved. We implement this by adding one extra objective, fairness, +which balances the shifts assigned over nurses.

+

Note that we can edit the model, that is add (or remove) constraints, +even after it has been solved.

+
+
+

Step #1 : Introduce three new variables per nurse to model the

+

number of shifts worked and positive and negative deviations to the +average.

+
+
+

Step #2 : Post the constraint that links these variables together.

+
+
+

Step #3 : Define KPIs to measure the result after solve.

+
DecisionKPI(name=Total under-worked,expr=underw_Anne+underw_Bethanie+underw_Betsy+underw_Cathy+underw_Cec..)
+
+
+

Finally, let’s modify the objective by adding the sum of +over_worked and under_worked to the previous objective.

+

Note: The definitions of over_worked and under_worked as +described above are not sufficient to give them an unambiguous value. +However, as all these variables are minimized, CPLEX ensures that these +variables take the minimum possible values in the solution.

+

Our modified model is ready to solve.

+

The log_output=True parameter tells CPLEX to print the log on the +standard output.

+
CPXPARAM_Read_DataCheck                          1
+CPXPARAM_MIP_Tolerances_MIPGap                   1.0000000000000001e-05
+1 of 31 MIP starts provided solutions.
+MIP start 'm1' defined initial solution with objective 28840.2500.
+Tried aggregator 2 times.
+MIP Presolve eliminated 997 rows and 379 columns.
+MIP Presolve modified 90 coefficients.
+Aggregator did 73 substitutions.
+Reduced MIP has 582 rows, 986 columns, and 3859 nonzeros.
+Reduced MIP has 892 binaries, 0 generals, 0 SOSs, and 0 indicators.
+Presolve time = 0.02 sec. (4.35 ticks)
+Probing time = 0.00 sec. (0.59 ticks)
+Tried aggregator 1 time.
+MIP Presolve eliminated 2 rows and 4 columns.
+Reduced MIP has 580 rows, 982 columns, and 3814 nonzeros.
+Reduced MIP has 892 binaries, 30 generals, 0 SOSs, and 0 indicators.
+Presolve time = 0.00 sec. (2.41 ticks)
+Probing time = 0.00 sec. (0.58 ticks)
+Clique table members: 479.
+MIP emphasis: balance optimality and feasibility.
+MIP search method: dynamic search.
+Parallel mode: deterministic, using up to 12 threads.
+Root relaxation solution time = 0.01 sec. (11.48 ticks)
+
+        Nodes                                         Cuts/
+   Node  Left     Objective  IInf  Best Integer    Best Bound    ItCnt     Gap
+
+*     0+    0                        28840.2500        0.0000           100.00%
+      0     0    28827.9167    76    28840.2500    28827.9167      885    0.04%
+      0     0    28829.2500    51    28840.2500      Cuts: 89     1053    0.04%
+      0     0    28829.9063    59    28840.2500     Cuts: 107     1359    0.04%
+      0     0    28831.0000    33    28840.2500      Cuts: 55     1530    0.03%
+      0     0    28831.0000    45    28840.2500      Cuts: 36     1649    0.03%
+      0     0    28831.0000    35    28840.2500       Cuts: 8     1715    0.03%
+      0     0    28831.0000    34    28840.2500      Cuts: 29     1783    0.03%
+*     0+    0                        28831.2500    28831.0000             0.00%
+
+GUB cover cuts applied:  19
+Cover cuts applied:  4
+Flow cuts applied:  21
+Mixed integer rounding cuts applied:  69
+Zero-half cuts applied:  14
+Gomory fractional cuts applied:  4
+
+Root node processing (before b&c):
+  Real time             =    0.36 sec. (211.92 ticks)
+Parallel b&c, 12 threads:
+  Real time             =    0.00 sec. (0.00 ticks)
+  Sync time (average)   =    0.00 sec.
+  Wait time (average)   =    0.00 sec.
+                          ------------
+Total (root+branch&cut) =    0.36 sec. (211.92 ticks)
+* model nurses solved with objective = 28831.250
+*  KPI: Total salary cost  = 28824.000
+*  KPI: Total over-worked  = 3.625
+*  KPI: Total under-worked = 3.625
+
+
+
+
+

Analyzing new results

+

Let’s recompute the new total deviation from average on this new +solution.

+
-- total absolute deviation to mean #shifts is now 7.25 down from 16.25
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
all_shifts0123456789...323334353637383940worked
all_nurses
Anne0.01.00.01.00.00.00.00.00.01.0...0.00.00.00.00.00.00.00.00.07.0
Bethanie0.00.00.00.01.00.00.01.00.00.0...0.01.01.00.00.00.00.00.00.07.0
Betsy0.00.00.00.00.00.00.00.01.00.0...1.01.01.00.00.00.00.00.00.07.0
Cathy0.00.00.00.00.00.01.00.00.00.0...0.01.00.00.01.01.00.01.00.07.0
Cecilia0.00.00.00.00.01.01.00.00.00.0...0.00.00.00.01.00.00.00.00.07.0
+

5 rows � 42 columns

+

Let’s print the new histogram of shifts worked.

+
<matplotlib.axes._subplots.AxesSubplot at 0x279df51e710>
+
+
+_images/nurses_pandas_90_1.png +

The breakdown of shifts over nurses is much closer to the average than +it was in the previous version.

+
+
+

But what would be the minimal fairness level?

+

But what is the absolute minimum for the deviation to the ideal average +number of shifts? CPLEX can tell us: simply minimize only the the total +deviation from average, ignoring the salary cost. Of course this is +unrealistic, but it will help us quantify how far our fairness result is +to the absolute optimal fairness.

+

We modify the objective and solve for the third time.

+
* model nurses solved with objective = 4.000
+*  KPI: Total salary cost  = 29606.000
+*  KPI: Total over-worked  = 4.000
+*  KPI: Total under-worked = 0.000
+
+
+

In the fairness-optimal solution, we have zero under-average shifts and +4 over-average. Salary cost is now higher than the previous value of +28884 but this was expected as salary cost was not part of the +objective.

+

To summarize, the absolute minimum for this measure of fairness is 4, +and we have found a balance with fairness=7.

+

Finally, we display the histogram for this optimal-fairness solution.

+
<matplotlib.axes._subplots.AxesSubplot at 0x279e05915f8>
+
+
+_images/nurses_pandas_95_1.png +

In the above figure, all nurses but one are assigned the average of 7 +shifts, which is what we expected.

+
+
+
+

Summary

+

You learned how to set up and use IBM Decision Optimization CPLEX +Modeling for Python to formulate a Mathematical Programming model and +solve it with IBM Decision Optimization on Cloud.

+
+
+

References

+ +

Copyright � 2017-2019 IBM. IPLA licensed Sample Materials.

+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/nurses_scheduling.html b/docs/2.24.232/mp/nurses_scheduling.html new file mode 100644 index 0000000..c44c00d --- /dev/null +++ b/docs/2.24.232/mp/nurses_scheduling.html @@ -0,0 +1,729 @@ + + + + + + + + + The Nurses Model — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

The Nurses Model

+

This tutorial includes everything you need to set up IBM Decision +Optimization CPLEX Modeling for Python (DOcplex), build a Mathematical +Programming model, and get its solution by solving the model on the +cloud with IBM ILOG CPLEX Optimizer.

+

When you finish this tutorial, you’ll have a foundational knowledge of +Prescriptive Analytics.

+
+

This notebook is part of Prescriptive Analytics for +Python

+

It requires either an installation of CPLEX +Optimizers +or it can be run on IBM Watson Studio +Cloud (Sign up for a +free IBM Cloud +account +and you can start using Watson Studio Cloud right away).

+
+

Table of contents:

+ +
+
+

Describe the business problem

+

This model deals with nurse scheduling. Nurses must be assigned to +hospital shifts in accordance with various skill and staffing +constraints.

+

The goal of the model is to find an efficient balance between the +different objectives:

+
    +
  • minimize the overall cost of the plan and
  • +
  • assign shifts as fairly as possible.
  • +
+
+
+

How decision optimization can help

+
    +
  • Prescriptive analytics (decision optimization) technology recommends +actions that are based on desired outcomes. It takes into account +specific scenarios, resources, and knowledge of past and current +events. With this insight, your organization can make better +decisions and have greater control of business outcomes.

    +
  • +
  • Prescriptive analytics is the next step on the path to insight-based +actions. It creates value through synergy with predictive analytics, +which analyzes data to predict future outcomes.

    +
  • +
  • +
    Prescriptive analytics takes that insight to the next level by +suggesting the optimal way to handle that future situation. +Organizations that can act fast in dynamic conditions and make +superior decisions in uncertain environments gain a strong +competitive advantage.
    +

    +
    +
  • +
+

With prescriptive analytics, you can:

+
    +
  • Automate the complex decisions and trade-offs to better manage your +limited resources.
  • +
  • Take advantage of a future opportunity or mitigate a future risk.
  • +
  • Proactively update recommendations based on changing events.
  • +
  • Meet operational goals, increase customer loyalty, prevent threats +and fraud, and optimize business processes.
  • +
+
+
+

Use decision optimization

+
+

Step 1: Import the library

+

Run the following code to import Decision Optimization CPLEX Modeling +library. The DOcplex library contains the two modeling packages, +Mathematical Programming and Constraint Programming.

+
+
+

Step 2: Model the data

+

Input data consists of several tables:

+
    +
  • The Departments table lists all departments in the scope of the +assignment.
  • +
  • The Skills table list all skills.
  • +
  • The Shifts table lists all shifts to be staffed. A shift contains a +department, a day in the week, plus the start and end times.
  • +
  • The Nurses table lists all nurses, identified by their names.
  • +
  • The NurseSkills table gives the skills of each nurse.
  • +
  • The SkillRequirements table lists the minimum number of persons +required for a given department and skill.
  • +
  • The NurseVacations table lists days off for each nurse.
  • +
  • The NurseAssociations table lists pairs of nurses who wish to work +together.
  • +
  • The NurseIncompatibilities table lists pairs of nurses who do not +want to work together . In addition, the plan has to satisfy a +maximum worktime for all nurses, for example 40 hours a week.
  • +
+
+
+

Step 3: Prepare the data

+

Now we need some basic data structures to store information.

+
+

Loading data from Excel with pandas

+

We load the data from an Excel file using pandas. Each sheet is read +into a separate pandas DataFrame.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nameseniorityqualificationpay_rate
0Anne11125
1Bethanie4528
2Betsy2217
3Cathy2217
4Cecilia9538
5Chris11438
6Cindy5221
7David1215
8Debbie7224
9Dee3321
10Gloria8225
11Isabelle3116
12Jane3423
13Janelle4322
14Janice2217
15Jemma2422
16Joan5324
17Joyce8329
18Jude4322
19Julie6222
20Juliet7431
21Kate5324
22Nancy8432
23Nathalie9538
24Nicole0214
25Patricia1113
26Patrick6119
27Roberta3526
28Suzanne5118
29Vickie7120
30Wendie5221
31Zoe8329
+

Now, we create some additional data structures to be used for building +the prescriptive model. The goal is to not depend on pandas when +defining decision variables and constraints. The ‘nurses_pandas’ +notebook illustrates how to benefit from pandas to build the +prescriptive model.

+
+
+
+

Step 4: Set up the prescriptive model

+
+* system is: Windows 64bit
+* Python version 3.7.3, located at: c:\local\python373\python.exe
+* docplex is present, version is (2, 11, 0)
+* pandas is present, version is 0.25.1
+
+
+

Create the DOcplex model

+

This model contains all the business constraints and defines the +objective.

+
+
+

Define the decision variables

+

The basic decisions are “which nurse works which shift”, which is +modeled by binary variables for each (nurse, shift) pair.

+

The output of the model is, for each shift, the list of nurses that work +the shift.

+
+
+

Express the business constraints

+
+
First constraint: define average work time
+

The average work time over all nurses will be used in particular to +calculate the over/under average work time for each nurse, and to +formulate a fairness rule.

+
docplex.mp.LinearConstraint[average](18AverageWorkTime,EQ,NurseWorkTime_Betsy+NurseWorkTime_Cathy+NurseWorkTime_Cindy+NurseWorkTime_Debbie+NurseWorkTime_Dee+NurseWorkTime_Isabelle+NurseWorkTime_Jane+NurseWorkTime_Janelle+NurseWorkTime_Janice+NurseWorkTime_Jemma+NurseWorkTime_Joan+NurseWorkTime_Jude+NurseWorkTime_Julie+NurseWorkTime_Kate+NurseWorkTime_Patrick+NurseWorkTime_Suzanne+NurseWorkTime_Vickie+NurseWorkTime_Wendie)
+
+
+
+
+
Second constraint: compute nurse work time, average and under/over time
+
+
+
Third constraint: vacations
+

When a nurse is on vacation, he or she cannot be assigned to any shift +starting that day.

+
+
+
Fourth constraint: a nurse cannot be assigned overlapping shifts
+

Some shifts overlap in time and thus cannot be assigned to the same +nurse.

+
# overlapping shifts: 20
+
+
+
+
+
Fifth constraint: enforce minimum and maximum requirements for shifts
+

Each shift requires a minimum and a maximum number of nurses. For each +shift, the sum over all nurses of assignments to this shift must be +greater than or equal to the minimum requirement and lesser than or +equal to the maximum requirement.

+
+
+
Sixth constraint: enforce skill requirements for selected shifts
+

Some shifts require at least x nurses with a specified skill.

+
+
+
Seventh constraint: associations
+

Some pairs of nurses get along particularly well, so we wish to assign +them together as a team. In other words, for every such pair and for +each shift, both assignment variables should always be equal. Either +both nurses work the shift, or both do not.

+
+
+
Eighth constraint: incompatibilities
+

Similarly, certain pairs of nurses do not get along well, and we want to +avoid having them together on a shift. In other words, for each shift, +both nurses of an incompatible pair cannot be assigned together to the +shift.

+
+
+
+

Express the objective

+

The objective mixes different (and contradictory) KPIs.

+

The first KPI is the total salary cost, computed as the sum of work +times over all nurses, weighted by pay rate. The second KPI is the total +number of assignments (nurse, shift). The third KPI is the average total +work time over all nurses. The fourth KPI represents the total number of +hours that is above the average work time (summed over all nurses), +while the fifth KPI represents the total number of hours that is below +this average. Finally, the last KPI is a measure of fairness, which is +evaluated as the total deviation from the average work time.

+
Model: nurses
+ - number of variables: 793
+   - binary=738, integer=0, continuous=55
+ - number of constraints: 961
+   - linear=961
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
Minimizing objective
+

The goal is to minimize the non-weighted sum of the total salary cost, +fairness and total number of assignment. This is accomplished using +the Model.minimize() method.

+

This definition is arbitrary and could be revised. For instance, one +could emphasize minimizing salary cost by adding a weight on this term +in the objective.

+
+
+
+

Solve with Decision Optimization

+

Now we have everything we need to solve the model, using +Model.solve(). The following cell solves using your local CPLEX (if +any, and provided you have added it to your PYTHONPATH variable).

+
CPXPARAM_Read_DataCheck                          1
+CPXPARAM_MIP_Tolerances_MIPGap                   1.0000000000000001e-05
+Bound infeasibility column 'NurseWorkTime_Betsy'.
+Presolve time = 0.00 sec. (0.29 ticks)
+
+Root node processing (before b&c):
+  Real time             =    0.00 sec. (0.45 ticks)
+Parallel b&c, 12 threads:
+  Real time             =    0.00 sec. (0.00 ticks)
+  Sync time (average)   =    0.00 sec.
+  Wait time (average)   =    0.00 sec.
+                          ------------
+Total (root+branch&cut) =    0.00 sec. (0.45 ticks)
+Warning: 55 constraint(s) will not be relaxed (e.g.: average: 18AverageWorkTime == NurseWorkTime_Betsy+NurseWorkTime_Cathy+NurseWorkTime_Cindy+NurseWorkTime_Debbie+NurseWorkTime_Dee+NurseWorkTime_Isabelle+NurseWorkTime_Jane+NurseWorkTime_Janelle+NurseWorkTime_Janice+NurseWorkTime_Jemma+NurseWorkTime_Joan+NurseWorkTime_Jude+NurseWorkTime_Julie+NurseWorkTime_Kate+NurseWorkTime_Patrick+NurseWorkTime_Suzanne+NurseWorkTime_Vickie+NurseWorkTime_Wendie)
+* number of relaxations: 35
+ - relaxed: high_req_min_EMER_Mon_18_3, with relaxation: -3.0
+ - relaxed: high_req_min_CONS_Mon_08_10, with relaxation: -5.0
+ - relaxed: high_req_min_CONS_Mon_12_8, with relaxation: -3.0
+ - relaxed: high_req_min_CARD_Mon_08_10, with relaxation: -6.0
+ - relaxed: high_req_min_CARD_Mon_12_8, with relaxation: -6.0
+ - relaxed: high_req_min_EMER_Tue_08_4, with relaxation: -2.0
+ - relaxed: high_req_min_EMER_Tue_18_3, with relaxation: -3.0
+ - relaxed: high_req_min_CONS_Tue_08_10, with relaxation: -4.0
+ - relaxed: high_req_min_CONS_Tue_12_8, with relaxation: -5.0
+ - relaxed: high_req_min_CARD_Tue_08_4, with relaxation: -1.0
+ - relaxed: high_req_min_CARD_Tue_18_3, with relaxation: -3.0
+ - relaxed: high_req_min_EMER_Wed_18_3, with relaxation: -1.0
+ - relaxed: high_req_min_EMER_Thu_02_3, with relaxation: -1.0
+ - relaxed: high_req_min_EMER_Thu_18_3, with relaxation: -3.0
+ - relaxed: high_req_min_CONS_Thu_08_10, with relaxation: -2.0
+ - relaxed: high_req_min_CONS_Thu_12_8, with relaxation: -1.0
+ - relaxed: high_req_min_EMER_Fri_18_3, with relaxation: -3.0
+ - relaxed: high_req_min_CONS_Fri_08_10, with relaxation: -3.0
+ - relaxed: high_req_min_CONS_Fri_12_8, with relaxation: -3.0
+ - relaxed: high_req_min_EMER_Sat_02_5, with relaxation: -5.0
+ - relaxed: high_req_min_EMER_Sat_12_7, with relaxation: -7.0
+ - relaxed: high_req_min_EMER_Sat_20_12, with relaxation: -4.0
+ - relaxed: high_req_min_EMER_Sun_02_5, with relaxation: -5.0
+ - relaxed: high_req_min_EMER_Sun_12_7, with relaxation: -7.0
+ - relaxed: high_required_Emergency_Cardiac Care_1_EMER_Mon_02, with relaxation: -1.0
+ - relaxed: high_required_Emergency_Cardiac Care_1_EMER_Mon_18, with relaxation: -1.0
+ - relaxed: high_required_Emergency_Cardiac Care_1_EMER_Tue_18, with relaxation: -1.0
+ - relaxed: high_required_Emergency_Cardiac Care_1_EMER_Wed_12, with relaxation: -1.0
+ - relaxed: high_required_Emergency_Cardiac Care_1_EMER_Wed_18, with relaxation: -1.0
+ - relaxed: high_required_Emergency_Cardiac Care_1_EMER_Thu_18, with relaxation: -1.0
+ - relaxed: high_required_Emergency_Cardiac Care_1_EMER_Fri_18, with relaxation: -1.0
+ - relaxed: high_required_Emergency_Cardiac Care_1_EMER_Sat_02, with relaxation: -1.0
+ - relaxed: high_required_Emergency_Cardiac Care_1_EMER_Sat_12, with relaxation: -1.0
+ - relaxed: high_required_Emergency_Cardiac Care_1_EMER_Sun_02, with relaxation: -1.0
+ - relaxed: high_required_Emergency_Cardiac Care_1_EMER_Sun_12, with relaxation: -1.0
+* total absolute relaxation: 97.0
+* model nurses solved with objective = 14097.333
+*  KPI: Total salary cost            = 13940.000
+*  KPI: Total number of assignments  = 134.000
+*  KPI: AverageWorkTime              = 37.667
+*  KPI: Total over-average worktime  = 11.667
+*  KPI: Total under-average worktime = 11.667
+*  KPI: Total fairness               = 23.333
+
+
+
+
+
+

Step 5: Investigate the solution and then run an example analysis

+

Let’s display some charts to visualize the results: a Gantt chart +displaying the assignment of nurses to shifts in a Gantt chart, and +another chart showing the number of assigned nurses to each department +over time.

+
-24
+
+
+_images/nurses_scheduling_47_0.png +
+
+
+

Summary

+

You learned how to set up and use IBM Decision Optimization CPLEX +Modeling for Python to formulate a Mathematical Programming model and +solve it with IBM Decision Optimization on Cloud.

+
+
+

References

+ +

Copyright � 2017-2019 IBM. IPLA licensed Sample Materials.

+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/objects.inv b/docs/2.24.232/mp/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..01b27961cd537014c827775d64d4c2fdb0be060b GIT binary patch literal 6925 zcmV+o8}j5MAX9K?X>NERX>N99Zgg*Qc_4OWa&u{KZXhxWBOp+6Z)#;@bUGkJPh)Uw zWq2-4P&yz@VRUF^ZDDk2V_|F{P;zf)a$#+4X>MmAO>bmnY-w(1AZBlJAW(U9Xm4%` zBOq2~a&u{KZaN@VGA=SS3L_v?Xk{RBWo=<;Ze(S0Aa78b#rNMXCQiPX<{x4c-p<4%aYu6Eel#dcaGRjbprXkSxJuYjCW)W}#e6I8W0Bz(N%(U39jEoia+*+N zPEWYN6{>Ltl(N03VoW6r)pbPgv9*`@GmVu~m|p!AXN(?gPz7gD&C#XP1@b68^EvVzx@Pz3dD8~C}CXK4a1 zahv7rPq1ZPQFJ3Bsn0&?_-6V7N;6ILY0isi{Os?)Q(i^Hd(!Y9IO z(WYC;KArqJ(j!!W>L76&*<8e)NPU~sEKZSH1PUF1Q6<{26smqJ#qwIT?lih^)tFP3 zi(l14tA3SykNVN;OVmC0wOa1tXt7;bS&0ss$N;T@j!luOBHhGi#gP)x7At%d?Xq$X zBa?XEL1Y^Dw*ByRkYVf9RuNaS_Zi_OMVF+D$QL>nh*15#m+5R^Ai_iwP7bUoGTs+t zG+|#cZ)hE}!nB00+M<@+SX3e#1%C~JL=K&!_842@uuvL_b*Umi$yzy);>fBTc@;gN zP#u~jhf9oHwo@YhkBT)VbXOR`_uUW`p<{pRif#}JURb}P>eFH1|5l;01XlM##CLEJ zLK75i$FvW9I>N%3*G3WxJHj7?ZfUqXStig)x)swjR1waKZTe1w1?a+t$-4aXkh0)x-yMY}sz2c;JsAri>18{4!VkFM7!)S=jdJuta`FfE=*z8S zQp6zhvnbePVFzO)`w3SE0P4RRL`i-5j0GhPtg)|{q8Bxy$;o~o?BUA;K$m`C^a;Q1 zizp2Cz06RJ@1Af`2a<$}ekZ4=06F}v5_6}(&Rco)dbOz3tNq)99Brl}rzQV{<59?jufc60OWfLQoDK-A8YTSz(#IGPG@kin#3<2J8S^g zOsqu2toa1VY`dOMe|m{`KN|9cC@xYAQ2QT`-_E#*0D>sIk3~jO*-s$SH)rihAqtX6 z<+?qMpVXEiKmw;Ab;Q8cQn=wkEoM%}MTRS(+=_zL{ZbH)pNYX;bV%A@SnfzG0bZU!fG)dffT?T z1(Cq}XNa_oDEr`kIL1LuU&hq(nI`i10WhJLAeLD1WtH6P2dEDV3%a}5M1-qLD-}p% z+StJg?kg%z5cA9l2X1mR1H(3h8NNT=0Yd`KmZsQXEj0x~>8UA~9zc@`)t#o?W_vPg z{8D?iI%M#|UWbYz?UHT>n!O(mFb4&U(ErWlW>>XGjTo=X=0FKQ?;lIiy;)P2O`RMG zrrC<^$Vh@$G=(c9m%crl^&A?6k@QjTEj_f|8voelf3iG<)1IiTb2P8&YDzOMl)UqfqHYv zr5Vi%)Kk#U#W-|e+B9KXoit%}BL2UyvUM_W@kA!T}{(2xabZwRw!4eusjJ|Nq@q59WQqDJVc27|)4HTE=N{e{;`{XQ&m%1&dN0Gc; zeH8J%USO#t{&SFE>-2L0Uh>D{$b2>x%RDi6cjZ^+RjZkLb2i8vQ;jgJTq27Pn&DCE_g|DvYkz+7wj%ZUt{->YgU~V>pye1X24&gyT#h-3sFfT9mQq$=7*boqo>|4U zJb=b*05kH`ZVCmX{96tiP>ET3!At_rTiRB*jwh9hY{D=_n2YHgDd`0SR^Sr?$|k6q zGq@#fU_d__Ruc@=NGhDOCk&^VR_~WwM{N#jBnL@iP4bW`{mcrM6GVds7!v}dGe=0! zM*P3HVnMm>06;nIaWJ_Y=FXORs|xf@TXlrb9m7&S2EzT@Jvunkz7mjS#@;O?D}jW8 z=8^*>4G*^6pfK1xJHW8{2S_J5CiNK?0HDrx91sOtC01+#$OTAaw>1?6q3DkbsA`!2 z0rHuXQl4>E0EsIrc9L5l36)M>3JlNye{`!{*i2W_S}GjF83%#90ipaHPEp>j(xwJ& zvleJzvFX!i-&E*Yj!54DgTXS7>yE2ovNM6fawL`%FHpFJQ_Yn)D$OIZgmpJ}^#X{8 zgBwDHev^x3!Pe$$9EY~O^elCUt{L`>O%Ey{&_=o;fPqpR+CdZouHR~$v}}Xmz>bBQ z*^h=WG}-osh7Ez7jjR`YzkkH@Q2+d2AmkHttM^9lOG2jORet^wT?b`!^ z%ok}MB8!TaaHNIXh1UY}k^`cE6BpjZOcZ?LPJe*F%8Iax)R%-+nNT-JLNN$eUq!_z z52GDPkx7l8o<->;ox4$}v3*RBu+kq3@Hy&WsBxOi8gVu;7cmzAaDKTyE(eGp* zE{Sq*3k~)8*1m$-jsJm|CbZ2-O3~y%q+w0$b3(J8H_U95gFupnBW?W_u(9ZiL&GJE zFq%BT6nZQt0DVJwPlaf|(y~_FH3`RnE;vCBB*>{?LS@QQ3g6KoxfpW ziDHBhqj`8f1!XYgc3+2FOH-Fy!dJfr2w>@J(!$rhCM|r?YtkTAyao?sxofz23{>HU zTgPw`D{Byv9H81=X0XYi2@RjOt-bGc9nIJkfgnaXQdi^%=fW+w?JZgXDYwf)`;Ks<`UJ8JNj&PT;kMqyqQD;Nyjk@&*R; zGXMtutYh8PgLN$F6ai#$B0*~J7!1ed7Fzg(kHYPA%#<-OE-^g`gw||J58n@<+QVLg zLO0_(P!W36(SbrO*J!E6*F?;GPW3r>vOU^)435e`m?*Q?w>mDHXeEkF7#3zd(cs>KBy@AF3 z42aV!rYJ4JwHO1!!`1vS0r1_Wzm~P&JoQ@KlH=fOc}o`>*YQ@lpInI|IK|I|2hKNp zj4}$LtPEY-m8@*2=Qi$jl;OeB(&PPVC3Ed-eAnP$&hf;+E4pT$fE=tWJ}qeoR` ze5aiJ@?fNVf-1??8&u(aU^*~|rA|3p)!Q85MobC@wVAD8;C5D~m%QKv%6sGx48Yo) z;6Sn%ex}`e1Yok@3r{1gnah+2Q$5h; zE)ynD#mIO5USLh!6ttaZK-l1^*Ak%AGE$pOqjItu8Ns>#Ds#eak$#)>4U&V#2L$|4=m)IjpiV(hL0jC~Wi{&xE` zHsdV%it6I2)P3!_F2_+L>`I>6F6g1o{BA}M?q*{ND78B}c>I!&>+8Vd!ssmcHW6iG z=l%w!qAa&B9Zlu=VKg#0o0tJ!PYgJ44;HxPyewGg=(_G}h}(2(A1wFuy5csO4gg5K z@-#G9B7NOrz25X7SmdTd_+Y75AI%3#+#VFf^ zBf(kxMpj`Svr?kMwO1C8Bbs0l|EO3~#!`n0?)S(BB;zEH;iU!B(0E(-jjtv6RiugD zSus|}mZGjnzD9mxcm7t&U=`hG>a`^D=5UgqUO}h`!D4oQ>UwlsX>$!8v7N-#;aWz` zYI!Bi(pEr)GEU@yWW*)GSD_)I>i4;T!8=(FU0_~eNwfjdA~urpwMZJ4>C1lEVv2p8 za*eA=9_lJqM>Ek;SAHWwP*u|!xH>m(+HmiFO&90hfY}Oo#F|00HKjWtI^3U-HqP*3 zh@yL|V}^seWoO{FH0#UYWa0Mt84i6Ww48ID9BS$@Ic zv(^#}mMR_bks_5-Pw(rH8_@Dn#0izK&6w;q*oJs^Q;jcJV_c*{hU+3%TYH-3QTyVr94O_Z!1U0}tu?ZV@H zJq!%nq~pwZob>R{0y;&FJ27?A;YtBSM^9m$126rR!=^M&5l7IscM_UCy>q?)Cq_V&2gDEq{M>m8XeZ%i2GSB{@!5Q9rgIDLCCwIMQg&)mCMgud%A|e~bTk9%mjHz90LW*;<7kZ7LQZNqvL$8xLHqdwQ7I&ZRx zs812aeTV)Upe(LXP;1~?wj*L#gR>d2_KCa}U15q|S_52H*EZw$y7(xbe#8E&{N$X* z%)zUPU6HC7UQ5h%La;ygcsb)D%CO7b?&p&~{CUUT6zxBwvLwX`3Rb7*Ur>(Y;q-$i z=DCQgJ{I7To57?LhO(j^uOp};TG?n<66LX~`ez10)BAB<0yzZJFn}L}9op&|w)z_) zk-&Lwt0YP44Z&8=#pyYnKj%xth`Vk1)3J z;i3Mcp$$MeS3i^5?fr5r-V7-046Y3Kga z=_6wcSuUl;&{|~kWtMz8XK5p1t4QBFvs;-f9*p0LUV0jLE7vk?K;l=qd)*3`?zZJE zf(Wg{?|67K)%zOnR%(=r)BO*2tTMPBe~w{fpI^ryGHA8Q<>8`JzAa$F6P%Z@{Xx2> z#)uQ4q!(>GHu6OYlPnIreH$)9C9ZpRZVP$!+i_dqz3(WvnfLXNG;I;Ga7%I%(sVfz zC)E+wWd>c}RRW$w^0@N?sKum-9K4BwrgWt{&&}*ibZ}s9Q)V8UkOd-_t|r?nrq&&r zHiCbPri=9O<>A*>Jw9ltb$LEwu7vFma={?oVipCf(Sk#ZjWX6M_)TyTu(+AHjl6g> z&d)XcPTSf*8|-TVU6gmqN1sUAx0YwA{t<2sN4ftXJgu<1m9K%`_U^R86F6f*CGw+o zy%nCqF$gMo4>%`~oFNChbMxNwAV4HT>d zr@#gZ2wM+(ewKTI3)ALnfQuuG52jp?OMkV_qxK<&Z*(zYY4?1ig|yuv?v%WQ96R)b zVX6t=k+cRr+nB6+lrN~)9(x5djO41$|vco3w!_ZPJd9~3!SZ$-P zjPvE*wg?E};^K)^tjIf)Tx-^iNdC;55#JuTvhDuFQbg~@Vx}t5E@cy5?NXs9u}V=T zl**Tj(#{xY)=!}YlS1B{@VMuI7TDg^ctwe&={n=}r19wD_)wf;d_d_r$8fC%z5|#^ zSNBvCkso^K_UdgzT%eTLJ!>ip7bjBSgtAjY@e`)8M7wkJ<_SQVDPS)Gz{IGKW`^eb~;I)iVH^*qiFse`f=3vI_Q)5=$V6B@Yitn zsl1VzBG3L^s06j66AzEOl|>x9bgvzcF2dr$pHWq~&OkQ`(ByAS2%@aX09)mR3|%rE z{)2fl1Qxp~$zSi@zW;o;&$At`v%ia9?0$N)>p!Y^BG!SuU~a}xA9Uy!b*jvFq|je) z(zEE#Cnn}C$0HUx^vuf9Z*Ack^+7&3DH;qD3-ny51j^F(iY+_t3jDlNTPQ+;7_DlV z8Ev9#vPU7BdfR^6p2rviM4Jnna;&11n=0;>p~G$P_LmN7o+F7heyQCw8EV6+U(I=$^rK~m z$)FiY#h+u$8NHo0olrQ!Y-~Bg^-CQG-HbEmj|g)Kk0o(%MaYaMW6v3_Qztvd@P{=2 zY=O-bi}gM7*;f~-d!yU~G+&{11v>HilCY0HJk99{v#{rncG@M_ACU0Iuu1FgFFJ&n zZ$j7ttjN~urweI#<+KIZkgc;$2hnhRY58vBJ-eD&=aq2s&B9QA3hj{I5!&>l1vB!E zX2;E(i@5?_c4gy3b>f2iMchzLhVe? z?M&0S(DL2FGfWQc$ceq3J;(+&4e=c4sU4mxaEhPU=2_?#BR5BwmAx>gT9@h0P^*OT z65b_aK6UqmcXCcxti3Y)qNo%d#NZ^j-B(TdDE}#9)@sVEoFd?nCYc23Up?rLyQbWs zLeJI7ANppAHp-b2lky@~B!vgu#+dVW_xh(sLEa46YGFo>KARK#dt-vHUbdG1IEux3 z3;oOcFS-;z-~ZPQMJM@OIYEW#ug;KSji9C4gTw-WGL$6;L{X7S*(Pli(qhKn#P4$? zn!hTj6y+r%r%{ULp_=ng_PndvPVN%!MEQ!c7PTrDTVmW5-_u4cVRG@u|J>yIRgO|j z%+RR)jRM+ro~~%MT2i5+>N^)mH=@y5T3dPfsS9YUNAFPSc2_h)yN#skFFI=bZ&3*P zUx~Up$qv8If6xb+zZ^aehsjUW>0T5*pceS(q$~zegv0!?q_MhEfzTXO6`;01lW(^-r TVET^(<+naw*Z=+>NE+Ih{?Jk( literal 0 HcmV?d00001 diff --git a/docs/2.24.232/mp/oil_blending.html b/docs/2.24.232/mp/oil_blending.html new file mode 100644 index 0000000..e0bfa79 --- /dev/null +++ b/docs/2.24.232/mp/oil_blending.html @@ -0,0 +1,621 @@ + + + + + + + + + Maximizing the profit of an oil company — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

Maximizing the profit of an oil company

+

This tutorial includes everything you need to set up the decision +optimization engines and build mathematical programming models.

+

When you finish this tutorial, you’ll have a foundational knowledge of +Prescriptive Analytics.

+
+

This notebook is part of Prescriptive Analytics for +Python

+

It requires either an installation of CPLEX +Optimizers +or it can be run on IBM Watson Studio +Cloud (Sign up for a +free IBM Cloud +account +and you can start using Watson Studio Cloud right away).

+
+

Table of contents:

+ +
+

Describe the business problem

+
    +
  • An oil company manufactures different types of gasoline and diesel. +Each type of gasoline is produced by blending different types of +crude oils that must be purchased. The company must decide how much +crude oil to buy in order to maximize its profit while respecting +processing capacities and quality levels as well as satisfying +customer demand.
  • +
  • Blending problems are a typical industry application of Linear +Programming (LP). LP represents real life problems mathematically +using an objective function to represent the goal that is to be +minimized or maximized, together with a set of linear constraints +which define the conditions to be satisfied and the limitations of +the real life problem. The function and constraints are expressed in +terms of decision variables and the solution, obtained from +optimization engines such as IBM� ILOG� CPLEX�, provides the best +values for these variables so that the objective function is +optimized.
  • +
  • The oil-blending problem consists of calculating different blends of +gasoline according to specific quality criteria.
  • +
  • Three types of gasoline are manufactured: super, regular, and diesel.
  • +
  • Each type of gasoline is produced by blending three types of crude +oil: crude1, crude2, and crude3.
  • +
  • The gasoline must satisfy some quality criteria with respect to their +lead content and their octane ratings, thus constraining the possible +blendings.
  • +
  • The company must also satisfy its customer demand, which is 3,000 +barrels a day of super, 2,000 of regular, and 1,000 of diesel.
  • +
  • The company can purchase 5,000 barrels of each type of crude oil per +day and can process at most 14,000 barrels a day.
  • +
  • In addition, the company has the option of advertising a gasoline, in +which case the demand for this type of gasoline increases by ten +barrels for every dollar spent.
  • +
  • Finally, it costs four dollars to transform a barrel of oil into a +barrel of gasoline.
  • +
+
+
+

How decision optimization can help

+
    +
  • Prescriptive analytics technology recommends actions based on desired +outcomes, taking into account specific scenarios, resources, and +knowledge of past and current events. This insight can help your +organization make better decisions and have greater control of +business outcomes.

    +
  • +
  • Prescriptive analytics is the next step on the path to insight-based +actions. It creates value through synergy with predictive analytics, +which analyzes data to predict future outcomes.

    +
  • +
  • +
    Prescriptive analytics takes that insight to the next level by +suggesting the optimal way to handle that future situation. +Organizations that can act fast in dynamic conditions and make +superior decisions in uncertain environments gain a strong +competitive advantage.
    +

    +
    +
  • +
  • For example:

    +
      +
    • Automate complex decisions and trade-offs to better manage limited +resources.
    • +
    • Take advantage of a future opportunity or mitigate a future risk.
    • +
    • Proactively update recommendations based on changing events.
    • +
    • Meet operational goals, increase customer loyalty, prevent threats +and fraud, and optimize business processes.
    • +
    +
  • +
+
+
+

Use decision optimization

+
+

Step 1: Import the library

+

Run the following code to import the Decision Optimization CPLEX +Modeling library. The DOcplex library contains the two modeling +packages, Mathematical Programming (docplex.mp) and Constraint +Programming (docplex.cp).

+

If CPLEX is not installed, install CPLEX Community edition.

+
+
+

Step 2: Model the data

+
    +
  • For each type of crude oil, there are capacities of what can be +bought, the buying price, the octane level, and the lead level.
  • +
  • For each type of gasoline or diesel, there is customer demand, +selling prices, and octane and lead levels.
  • +
  • There is a maximum level of production imposed by the factory’s limit +as well as a fixed production cost.
  • +
  • There are inventory costs for each type of final product and blending +proportions. All of these have actual values in the model.
  • +
  • The maginal production cost and maximum production are assumed to be +identical for all oil types.
  • +
+

Input data comes as NumPy arrays with two dimensions. +NumPy is the fundamental package for +scientific computing with Python.

+

The first dimension of the NumPy array is the number of gasoline +types; and for each gasoline type, we have a NumPy array containing +capacity, price, octane and lead level, in that order.

+
Number of gasoline types = 3
+Number of crude types = 3
+
+
+
+
+

Step 3: Prepare the data

+

Pandas is another Python library that we +use to store data. pandas contains data structures and data analysis +tools for the Python programming language.

+

Use basic HTML and a stylesheet to format the data.

+

Let’s display the data we just prepared.

+
Gas data:
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namedemandpriceoctanelead
0super300070101
1regular20006082
2diesel10005061
+
Oil data:
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
namecapacitypriceoctanelead
0crude1500045120.5
1crude250003562.0
2crude350002583.0
+
+
+

Step 4: Set up the prescriptive model

+
+

Create the DOcplex model

+

A model is needed to store all the variables and constraints needed to +formulate the business problem and submit the problem to the solve +service.

+
+
+

Define the decision variables

+

For each combination of oil and gas, we have to decide the quantity of +oil to use to produce a gasoline. A decision variable will be needed to +represent that amount. A matrix of continuous variables, indexed by the +set of oils and the set of gasolines needs to be created.

+

We also have to decide how much should be spent in advertising for each +time of gasoline. To do so, we will create a list of continuous +variables, indexed by the gasolines.

+
+
+

Express the business constraints

+

The business constraints are the following:

+
    +
  • The demand for each gasoline type must be satisfied. The total demand +includes the initial demand as stored in the data,plus a variable +demand caused by the advertising. This increase is assumed to be +proportional to the advertising cost.
  • +
  • The capacity constraint on each oil type must also be satisfied.
  • +
  • For each gasoline type, the octane level must be above a minimum +level, and the lead level must be below a maximum level.
  • +
+
+
Demand
+
    +
  • For each gasoline type, the total quantity produced must equal the +raw demand plus the demand increase created by the advertising.
  • +
+
Model: oil_blending
+ - number of variables: 12
+   - binary=0, integer=0, continuous=12
+ - number of constraints: 3
+   - linear=3
+ - parameters: defaults
+ - problem type is: LP
+
+
+
+
+
Maximum capacity
+
    +
  • For each type of oil, the total quantity used in all types of +gasolines must not exceed the maximum capacity for this oil.
  • +
+
Model: oil_blending
+ - number of variables: 12
+   - binary=0, integer=0, continuous=12
+ - number of constraints: 6
+   - linear=6
+ - parameters: defaults
+ - problem type is: LP
+
+
+
+
+
Octane and Lead levels
+
    +
  • For each gasoline type, the octane level must be above a minimum +level, and the lead level must be below a maximum level.
  • +
+
Model: oil_blending
+ - number of variables: 12
+   - binary=0, integer=0, continuous=12
+ - number of constraints: 12
+   - linear=12
+ - parameters: defaults
+ - problem type is: LP
+
+
+
+
+
Maximum total production
+
    +
  • The total production must not exceed the maximum (here 14000).
  • +
+
Model: oil_blending
+ - number of variables: 12
+   - binary=0, integer=0, continuous=12
+ - number of constraints: 13
+   - linear=13
+ - parameters: defaults
+ - problem type is: LP
+
+
+
+
+
+

Express the objective

+
    +
  • The objective or goal is to maximize profit, which is made from sales +of the final products minus total costs. The costs consist of the +purchase cost of the crude oils, production costs, and inventory +costs.
  • +
  • The model maximizes the net revenue, that is revenue minus oil cost +and production cost, to which we subtract the total advertising cost.
  • +
  • To define business objective, let’s define a few KPIs :
      +
    • Total advertising cost
    • +
    • Total Oil cost
    • +
    • Total production cost
    • +
    • Total revenue
    • +
    +
  • +
+
+
+

Solve with Decision Optimization

+

If you’re using a Community Edition of CPLEX runtimes, depending on the +size of the problem, the solve stage may fail and will need a paying +subscription or product installation.

+

We display the objective and KPI values after the solve by calling the +method report() on the model.

+
* model oil_blending solved with objective = 287750.000
+*  KPI: Total advertising cost = 750.000
+*  KPI: Total Oil cost         = 487500.000
+*  KPI: Total production cost  = 54000.000
+*  KPI: Total revenue          = 830000.000
+
+
+
+
+
+

Step 5: Investigate the solution and then run an example analysis

+
+

Displaying the solution

+

First, get the KPIs values and store them in a pandas DataFrame.

+

Let’s display some KPIs in pie charts using the Python package +matplotlib.

+_images/oil_blending_41_0.png +
+
Production
+_images/oil_blending_43_0.png +

We see that the most produced gasoline type is by far regular.

+

Now, let’s plot the breakdown of oil blend quantities per gasoline type. +We are using a multiple bar chart diagram, displaying all blend values +for each couple of oil and gasoline type.

+_images/oil_blending_46_0.png +

Notice the missing bar for (crude2, diesel) which is expected since +blend[crude2, diesel] is zero in the solution.

+

We can check the solution value of blends for crude2 and diesel, +remembering that crude2 has offset 1 and diesel has offset 2. Note how +the decision variable is automatically converted to a float here. This +would raise an exception if called before submitting a solve, as no +solution value would be present.

+
* value of blend[crude2, diesel] is 0
+
+
+
+
+
+
+
+

Summary

+

You learned how to set up and use IBM Decision Optimization CPLEX +Modeling for Python to formulate a Mathematical Programming model and +solve it with CPLEX.

+
+
+

References

+ +

Copyright � 2017-2019 IBM. IPLA licensed Sample Materials.

+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/production.html b/docs/2.24.232/mp/production.html new file mode 100644 index 0000000..33e5f70 --- /dev/null +++ b/docs/2.24.232/mp/production.html @@ -0,0 +1,334 @@ + + + + + + + + + production.py — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

production.py

+

To meet the demands of its customers, a company manufactures its products in its own factories (inside production) or buys them from other companies (outside production). +The inside production is subject to some resource constraints: each product consumes a certain amount of each resource. +In contrast, outside production is theoretically unlimited. The problem is to determine how much of each product should be +produced inside and outside the company while minimizing the overall production cost, meeting the demand, and satisfying the resource constraints.

+

The model aims at minimizing the production cost for a number of products while satisfying customer demand. +Each product can be produced either inside the company or outside, at a higher cost. +The inside production is constrained by the company’s resources, while outside production is considered unlimited. +The model first declares the products and the resources. The data consists of the description of the products +(the demand, the inside and outside costs, and the resource consumption) and the capacity of the various resources.

+

The variables for this problem are the inside and outside production for each product.

+
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2018
+# --------------------------------------------------------------------------
+
+"""The model aims at minimizing the production cost for a number of products
+while satisfying customer demand. Each product can be produced either inside
+the company or outside, at a higher cost.
+
+The inside production is constrained by the company's resources, while outside
+production is considered unlimited.
+
+The model first declares the products and the resources.
+The data consists of the description of the products (the demand, the inside
+and outside costs, and the resource consumption) and the capacity of the
+various resources.
+
+The variables for this problem are the inside and outside production for each
+product.
+"""
+
+from docplex.mp.model import Model
+from docplex.util.environment import get_environment
+
+
+# ----------------------------------------------------------------------------
+# Initialize the problem data
+# ----------------------------------------------------------------------------
+PRODUCTS = [("kluski", 100, 0.6, 0.8),
+            ("capellini", 200, 0.8, 0.9),
+            ("fettucine", 300, 0.3, 0.4)]
+
+# resources are a list of simple tuples (name, capacity)
+RESOURCES = [("flour", 20),
+             ("eggs", 40)]
+
+CONSUMPTIONS = {("kluski", "flour"): 0.5,
+                ("kluski", "eggs"): 0.2,
+                ("capellini", "flour"): 0.4,
+                ("capellini", "eggs"): 0.4,
+                ("fettucine", "flour"): 0.3,
+                ("fettucine", "eggs"): 0.6}
+
+
+# ----------------------------------------------------------------------------
+# Build the model
+# ----------------------------------------------------------------------------
+def build_production_problem(mdl, products, resources, consumptions, **kwargs):
+    """ Takes as input:
+        - a list of product tuples (name, demand, inside, outside)
+        - a list of resource tuples (name, capacity)
+        - a list of consumption tuples (product_name, resource_named, consumed)
+    """
+      # --- decision variables ---
+    mdl.inside_vars  = mdl.continuous_var_dict(products, name=lambda p: 'inside_%s' % p[0])
+    mdl.outside_vars = mdl.continuous_var_dict(products, name=lambda p: 'outside_%s' % p[0])
+
+    # --- constraints ---
+    # demand satisfaction
+    mdl.add_constraints((mdl.inside_vars[prod] + mdl.outside_vars[prod] >= prod[1], 'ct_demand_%s' % prod[0]) for prod in products)
+
+    # --- resource capacity ---
+    mdl.add_constraints((mdl.sum(mdl.inside_vars[p] * consumptions[p[0], res[0]] for p in products) <= res[1],
+                         'ct_res_%s' % res[0]) for res in resources)
+
+    # --- objective ---
+    mdl.total_inside_cost = mdl.sum(mdl.inside_vars[p] * p[2] for p in products)
+    mdl.add_kpi(mdl.total_inside_cost, "inside cost")
+    mdl.total_outside_cost = mdl.sum(mdl.outside_vars[p] * p[3] for p in products)
+    mdl.add_kpi(mdl.total_outside_cost, "outside cost")
+    mdl.minimize(mdl.total_inside_cost + mdl.total_outside_cost)
+    return mdl
+
+
+def print_production_solution(mdl, products):
+    obj = mdl.objective_value
+    print("* Production model solved with objective: {:g}".format(obj))
+    print("* Total inside cost=%g" % mdl.total_inside_cost.solution_value)
+    for p in products:
+        print("Inside production of {product}: {ins_var}".format
+              (product=p[0], ins_var=mdl.inside_vars[p].solution_value))
+    print("* Total outside cost=%g" % mdl.total_outside_cost.solution_value)
+    for p in products:
+        print("Outside production of {product}: {out_var}".format
+              (product=p[0], out_var=mdl.outside_vars[p].solution_value))
+
+def build_default_production_problem(**kwargs):
+    mdl = Model( **kwargs)
+    return build_production_problem(mdl, PRODUCTS, RESOURCES, CONSUMPTIONS)
+# ----------------------------------------------------------------------------
+# Solve the model and display the result
+# ----------------------------------------------------------------------------
+if __name__ == '__main__':
+    # Build the model
+    with Model(name='production') as model:
+        model = build_production_problem(model, PRODUCTS, RESOURCES, CONSUMPTIONS)
+        model.print_information()
+        # Solve the model.
+        if model.solve():
+            print_production_solution(model, PRODUCTS)
+            # Save the CPLEX solution as "solution.json" program output
+            with get_environment().get_output_stream("solution.json") as fp:
+                model.solution.export(fp, "json")
+        else:
+            print("Problem has no solution")
+
+
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/py-modindex.html b/docs/2.24.232/mp/py-modindex.html new file mode 100644 index 0000000..6264d17 --- /dev/null +++ b/docs/2.24.232/mp/py-modindex.html @@ -0,0 +1,260 @@ + + + + + + + + + Python Module Index — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + +

Python Module Index

+ +
+ d +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ d
+ docplex +
    + docplex.mp.basic +
    + docplex.mp.callbacks.cb_mixin +
    + docplex.mp.conflict_refiner +
    + docplex.mp.constants +
    + docplex.mp.constr +
    + docplex.mp.context +
    + docplex.mp.dvar +
    + docplex.mp.environment +
    + docplex.mp.error_handler +
    + docplex.mp.kpi +
    + docplex.mp.linear +
    + docplex.mp.model +
    + docplex.mp.model_reader +
    + docplex.mp.model_stats +
    + docplex.mp.params.parameters +
    + docplex.mp.priority +
    + docplex.mp.progress +
    + docplex.mp.publish +
    + docplex.mp.pwl +
    + docplex.mp.quad +
    + docplex.mp.relax_linear +
    + docplex.mp.relaxer +
    + docplex.mp.sdetails +
    + docplex.mp.solution +
    + docplex.mp.sosvarset +
    + docplex.mp.vartype +
    + docplex.mp.with_funcs +
    + docplex.mp.worker_utils +
    + docplex.util.csv_utils +
    + docplex.util.environment +
    + docplex.util.logging_utils +
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/refman.html b/docs/2.24.232/mp/refman.html new file mode 100644 index 0000000..4360c11 --- /dev/null +++ b/docs/2.24.232/mp/refman.html @@ -0,0 +1,220 @@ + + + + + + + + + docplex.mp reference manual — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/samples.html b/docs/2.24.232/mp/samples.html new file mode 100644 index 0000000..8422fac --- /dev/null +++ b/docs/2.24.232/mp/samples.html @@ -0,0 +1,231 @@ + + + + + + + + + Examples of mathematical programming — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

Examples of mathematical programming

+
+

Downloading the examples

+ + + +The examples can be downloaded from GitHub: + + +docplex examples + + +
+
+
+
+

Examples content and source

+

The sample archive docplex_examples.zip contains +subdirectories for different sample categories:

+
    +
  • examples/mp/modeling contains examples that target modeling.

    +
    +
      +
    • diet: This example solves a simple variation of the well-known +diet problem that was posed by George Stigler and George Dantzig. How does a planner +choose foods that satisfy nutritional requirements while minimizing costs or +maximizing satiety?
    • +
    • production: This example solves a production planning problem. +How does a company decide what proportion of its products to produce +inside the company and what to buy from outside the company?
    • +
    • sport_scheduling: How can a sports league schedule matches between +teams in different divisions such that the teams play each other the +appropriate number of times and maximize the objective of scheduling +intradivision matches as late as possible in the season?
    • +
    • nurses: This example solves the problem of finding an optimal +assignment of nurses to shifts.
    • +
    • nurses with multiobjective: This example solves the problem of finding an optimal +assignment of nurses to shifts using multi objective.
    • +
    +
    +
  • +
  • examples/mp/workflow contains examples that target optimization workflow.

    +
    +
      +
    • cutstock: The cutting stock problem in this example is sometimes known in math programming terms as a knapsack problem with reduced +cost in the objective function. Generally, a cutting stock problem begins with a supply of rolls of material of fixed length (the stock). +Strips are cut from these rolls. All the strips that are cut from one roll are known together as a pattern. +The point of this example is to use as few rolls of stock as possible to satisfy some specified demand of strips.
    • +
    • lagrangian_relaxation: This example solves the generalized +assignment problem, with or without Lagrangian relaxation.
    • +
    • load_balancing: This example looks at cloud load balancing to keep a service running +in the cloud at reasonable cost by reducing the expense of running cloud servers while +minimizing risk and human time due to rebalancing and doing balance sleeping models +across servers.
    • +
    • +
      populate.py: This sample shows how to run the populate algorithm,
      +
      either on a model file or a model instance.
      +
      +
    • +
    +
    +
  • +
  • examples/mp/jupyter contains Jupyter Notebooks, working on http://datascience.ibm.com.

    +
    +
      +
    • boxes : This example illustrates assigning objects to boxes in a manner that +minimizes the total distance between each object and its assigned box.
    • +
    • chicago_coffee_shops : A K-Median model is used to determine where N +coffee shops should be located to minimize the distance from local libraries.
    • +
    • marketing_campaign : A marketing department wants to achieve more profitable results in future +campaigns by matching the right offer of financial services to each customer.
    • +
    • mining_pandas : This mining operations optimization problem is an +implementation of Problem 7 from “Model Building in Mathematical Programming” by H.P. Williams. The operational decisions that need +to be made are which mines should be operated each year and how much each mine should produce.
    • +
    • nurses_pandas : This is the same example as the nurses example, but describes how to use pandas in the model.
    • +
    • nurses_scheduling : This example solves the problem of finding an optimal assignment of nurses to shifts.
    • +
    • oil_blending : The oil-blending problem consists of calculating different blends of gasoline according to specific quality criteria. +Blending problems are a typical industry application on Linear Programming.
    • +
    • sports_scheduling : How can a sports league schedule matches between +teams in different divisions such that the teams play each other the +appropriate number of times and maximize the objective of scheduling +intradivision matches as late as possible in the season?
    • +
    • ucp_pandas : The Unit Commitment Problem answers the question, “Which power generators should I run at which times and at what level +in order to satisfy the demand for electricity?”
    • +
    +
    +
  • +
+
+
+

Running the examples

+

Examples files are Python scripts that contain problem data. +Debug traces and warnings are included in the examples to help you diagnose the cloud or local solve and +to give a brief overview of your installation (Is CPLEX® in the path?, Is matplotlib installed?…). +Running a sample submits the problem to the optimization engine and displays the result.

+

For instance, to run diet, type the following command at a command prompt:

+
c:\docplex\examples\mp\modeling> python diet.py
+* model solved as function with objective: 2.69041
+1> Servings of ({Spaghetti W/ Sauce}) = 2.155
+2> Servings of ({Chocolate Chip Cookies}) = 10.000
+3> Servings of ({2% Lowfat Milk}) = 1.831
+4> Servings of ({Hotdog}) = 0.930
+* KPI: Total Calories=2000.000
+* KPI: Total Calcium=800.000
+* KPI: Total Iron=11.278
+* KPI: Total Vit_A=8518.433
+* KPI: Total Dietary_Fiber=25.000
+* KPI: Total Carbohydrates=256.806
+* KPI: Total Protein=51.174
+
+
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/search.html b/docs/2.24.232/mp/search.html new file mode 100644 index 0000000..6f8aa00 --- /dev/null +++ b/docs/2.24.232/mp/search.html @@ -0,0 +1,103 @@ + + + + + + + + + Search — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Search

+
+ +

+ Please activate JavaScript to enable the search + functionality. +

+
+

+ From here you can search these documents. Enter your search + words into the box below and click "search". Note that the search + function will automatically search for all of the words. Pages + containing fewer words won't appear in the result list. +

+
+ + + +
+ +
+ +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/searchindex.js b/docs/2.24.232/mp/searchindex.js new file mode 100644 index 0000000..e18b3dd --- /dev/null +++ b/docs/2.24.232/mp/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({docnames:["README.md","boxes","chicago_coffee_shops","creating_model","cutstock","diet","docplex.mp.basic","docplex.mp.callbacks.cb_mixin","docplex.mp.conflict_refiner","docplex.mp.constants","docplex.mp.constr","docplex.mp.context","docplex.mp.dvar","docplex.mp.environment","docplex.mp.error_handler","docplex.mp.kpi","docplex.mp.linear","docplex.mp.model","docplex.mp.model_reader","docplex.mp.model_stats","docplex.mp.params.parameters","docplex.mp.priority","docplex.mp.progress","docplex.mp.publish","docplex.mp.pwl","docplex.mp.quad","docplex.mp.relax_linear","docplex.mp.relaxer","docplex.mp.sdetails","docplex.mp.solution","docplex.mp.sosvarset","docplex.mp.vartype","docplex.mp.with_funcs","docplex.mp.worker_utils","docplex.util.csv_utils","docplex.util.environment","docplex.util.logging_utils","getting_started","getting_started_python","index","lagrangian_relaxation","load_balancing","marketing_campaign","mining_pandas","nurses","nurses_multiobj","nurses_pandas","nurses_scheduling","oil_blending","production","refman","samples","sport_scheduling","sports_scheduling","support","troubleshooting","ucp_pandas","warehouse"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.todo":1,"sphinx.ext.viewcode":1,sphinx:55},filenames:["README.md.rst","boxes.rst","chicago_coffee_shops.rst","creating_model.rst","cutstock.rst","diet.rst","docplex.mp.basic.rst","docplex.mp.callbacks.cb_mixin.rst","docplex.mp.conflict_refiner.rst","docplex.mp.constants.rst","docplex.mp.constr.rst","docplex.mp.context.rst","docplex.mp.dvar.rst","docplex.mp.environment.rst","docplex.mp.error_handler.rst","docplex.mp.kpi.rst","docplex.mp.linear.rst","docplex.mp.model.rst","docplex.mp.model_reader.rst","docplex.mp.model_stats.rst","docplex.mp.params.parameters.rst","docplex.mp.priority.rst","docplex.mp.progress.rst","docplex.mp.publish.rst","docplex.mp.pwl.rst","docplex.mp.quad.rst","docplex.mp.relax_linear.rst","docplex.mp.relaxer.rst","docplex.mp.sdetails.rst","docplex.mp.solution.rst","docplex.mp.sosvarset.rst","docplex.mp.vartype.rst","docplex.mp.with_funcs.rst","docplex.mp.worker_utils.rst","docplex.util.csv_utils.rst","docplex.util.environment.rst","docplex.util.logging_utils.rst","getting_started.rst","getting_started_python.rst","index.rst","lagrangian_relaxation.rst","load_balancing.rst","marketing_campaign.rst","mining_pandas.rst","nurses.rst","nurses_multiobj.rst","nurses_pandas.rst","nurses_scheduling.rst","oil_blending.rst","production.rst","refman.rst","samples.rst","sport_scheduling.rst","sports_scheduling.rst","support.rst","troubleshooting.rst","ucp_pandas.rst","warehouse.rst"],objects:{"docplex.mp":{basic:[6,0,0,"-"],conflict_refiner:[8,0,0,"-"],constants:[9,0,0,"-"],constr:[10,0,0,"-"],context:[11,0,0,"-"],dvar:[12,0,0,"-"],environment:[13,0,0,"-"],error_handler:[14,0,0,"-"],kpi:[15,0,0,"-"],linear:[16,0,0,"-"],model:[17,0,0,"-"],model_reader:[18,0,0,"-"],model_stats:[19,0,0,"-"],priority:[21,0,0,"-"],progress:[22,0,0,"-"],publish:[23,0,0,"-"],pwl:[24,0,0,"-"],quad:[25,0,0,"-"],relax_linear:[26,0,0,"-"],relaxer:[27,0,0,"-"],sdetails:[28,0,0,"-"],solution:[29,0,0,"-"],sosvarset:[30,0,0,"-"],vartype:[31,0,0,"-"],with_funcs:[32,0,0,"-"],worker_utils:[33,0,0,"-"]},"docplex.mp.basic":{Expr:[6,1,1,""],IndexableObject:[6,1,1,""],ModelObject:[6,1,1,""],ModelingObjectBase:[6,1,1,""]},"docplex.mp.basic.Expr":{contains_var:[6,2,1,""],is_quad_expr:[6,2,1,""],number_of_variables:[6,2,1,""]},"docplex.mp.basic.IndexableObject":{is_generated:[6,2,1,""],model:[6,3,1,""]},"docplex.mp.basic.ModelingObjectBase":{has_name:[6,2,1,""],has_user_name:[6,2,1,""],model:[6,3,1,""],name:[6,3,1,""]},"docplex.mp.callbacks":{cb_mixin:[7,0,0,"-"]},"docplex.mp.callbacks.cb_mixin":{ConstraintCallbackMixin:[7,1,1,""],ModelCallbackMixin:[7,1,1,""],print_called:[7,5,1,""]},"docplex.mp.callbacks.cb_mixin.ConstraintCallbackMixin":{get_cpx_unsatisfied_cts:[7,2,1,""],make_solution_from_watched:[7,2,1,""],register_watched_var:[7,2,1,""],register_watched_vars:[7,2,1,""]},"docplex.mp.callbacks.cb_mixin.ModelCallbackMixin":{index_to_var:[7,2,1,""],linear_ct_to_cplex:[7,4,1,""],make_complete_solution:[7,2,1,""],make_solution:[7,2,1,""],make_solution_from_vars:[7,2,1,""],model:[7,3,1,""]},"docplex.mp.conflict_refiner":{ConflictRefiner:[8,1,1,""],ConflictRefinerResult:[8,1,1,""],ConstraintsGroup:[8,1,1,""],TConflictConstraint:[8,3,1,""],VarLbConstraintWrapper:[8,1,1,""],VarUbConstraintWrapper:[8,1,1,""]},"docplex.mp.conflict_refiner.ConflictRefiner":{display_conflicts:[8,4,1,""],refine_conflict:[8,2,1,""],var_bounds:[8,6,1,""]},"docplex.mp.conflict_refiner.ConflictRefinerResult":{display:[8,2,1,""],display_stats:[8,2,1,""],iter_conflicts:[8,2,1,""],number_of_conflicts:[8,3,1,""],print_information:[8,2,1,""],refined_by:[8,3,1,""]},"docplex.mp.conflict_refiner.ConstraintsGroup":{from_var:[8,6,1,""]},"docplex.mp.constants":{BasisStatus:[9,1,1,""],ComparisonType:[9,1,1,""],ConflictStatus:[9,1,1,""],CplexScope:[9,1,1,""],EffortLevel:[9,1,1,""],ObjectiveSense:[9,1,1,""],QualityMetric:[9,1,1,""],RelaxationMode:[9,1,1,""],SOSType:[9,1,1,""],SolveAttribute:[9,1,1,""],UpdateEvent:[9,1,1,""],VarBoundType:[9,1,1,""],WriteLevel:[9,1,1,""]},"docplex.mp.constants.ComparisonType":{operator_symbol:[9,3,1,""]},"docplex.mp.constants.ObjectiveSense":{is_maximize:[9,2,1,""],is_minimize:[9,2,1,""],short_name:[9,3,1,""],verb:[9,3,1,""]},"docplex.mp.constr":{AbstractConstraint:[10,1,1,""],BinaryConstraint:[10,1,1,""],EquivalenceConstraint:[10,1,1,""],IfThenConstraint:[10,1,1,""],IndicatorConstraint:[10,1,1,""],LinearConstraint:[10,1,1,""],LogicalConstraint:[10,1,1,""],NotEqualConstraint:[10,1,1,""],PwlConstraint:[10,1,1,""],QuadraticConstraint:[10,1,1,""],RangeConstraint:[10,1,1,""]},"docplex.mp.constr.AbstractConstraint":{is_added:[10,2,1,""],set_mandatory:[10,2,1,""]},"docplex.mp.constr.BinaryConstraint":{get_left_expr:[10,2,1,""],get_right_expr:[10,2,1,""],iter_variables:[10,2,1,""],to_string:[10,2,1,""],type:[10,3,1,""]},"docplex.mp.constr.IfThenConstraint":{to_string:[10,2,1,""]},"docplex.mp.constr.IndicatorConstraint":{invalidate:[10,2,1,""]},"docplex.mp.constr.LinearConstraint":{basis_status:[10,3,1,""],benders_annotation:[10,3,1,""],dual_value:[10,3,1,""],left_expr:[10,3,1,""],lhs:[10,3,1,""],rhs:[10,3,1,""],right_expr:[10,3,1,""],sense:[10,3,1,""],slack_value:[10,3,1,""],to_string:[10,2,1,""],type:[10,3,1,""]},"docplex.mp.constr.LogicalConstraint":{benders_annotation:[10,3,1,""],to_string:[10,2,1,""]},"docplex.mp.constr.NotEqualConstraint":{to_string:[10,2,1,""]},"docplex.mp.constr.PwlConstraint":{expr:[10,3,1,""],iter_variables:[10,2,1,""],pwl_func:[10,3,1,""],y:[10,3,1,""]},"docplex.mp.constr.QuadraticConstraint":{benders_annotation:[10,3,1,""],sense:[10,3,1,""],slack_value:[10,3,1,""],type:[10,3,1,""]},"docplex.mp.constr.RangeConstraint":{basis_status:[10,3,1,""],benders_annotation:[10,3,1,""],bounds:[10,3,1,""],dual_value:[10,3,1,""],expr:[10,3,1,""],iter_variables:[10,2,1,""],lb:[10,3,1,""],slack_value:[10,3,1,""],ub:[10,3,1,""]},"docplex.mp.context":{BaseContext:[11,1,1,""],Context:[11,1,1,""],ContextOverride:[11,1,1,""],InvalidSettingsFileError:[11,7,1,""],SolverContext:[11,1,1,""]},"docplex.mp.context.Context":{copy:[11,2,1,""],cplex_parameters:[11,3,1,""],make_default_context:[11,4,1,""],read_settings:[11,2,1,""],update:[11,2,1,""]},"docplex.mp.context.Context.context.solver.auto_publish":{kpis_output_field_name:[11,3,1,""],kpis_output_field_value:[11,3,1,""]},"docplex.mp.context.Context.solver":{auto_publish:[11,3,1,""],log_output:[11,3,1,""]},"docplex.mp.context.Context.solver.auto_publish":{kpis_output:[11,3,1,""],result_output:[11,3,1,""],solve_details:[11,3,1,""]},"docplex.mp.context.Context.solver.kpi_reporting":{filter_level:[11,3,1,""]},"docplex.mp.dvar":{Var:[12,1,1,""]},"docplex.mp.dvar.Var":{basis_status:[12,3,1,""],benders_annotation:[12,3,1,""],equals:[12,2,1,""],get_key:[12,2,1,""],is_binary:[12,2,1,""],is_continuous:[12,2,1,""],is_discrete:[12,2,1,""],is_integer:[12,2,1,""],iter_constraints:[12,2,1,""],lb:[12,3,1,""],name:[12,3,1,""],reduced_cost:[12,3,1,""],solution_value:[12,3,1,""],sv:[12,3,1,""],to_bool:[12,2,1,""],ub:[12,3,1,""],vartype:[12,3,1,""]},"docplex.mp.environment":{Environment:[13,1,1,""],UnsupportedPlatformError:[13,7,1,""]},"docplex.mp.environment.Environment":{cplex_location:[13,3,1,""],get_cplex_module:[13,2,1,""],has_cplex:[13,3,1,""],has_matplotlib:[13,3,1,""],has_numpy:[13,3,1,""],has_pandas:[13,3,1,""],is_64bit:[13,2,1,""],python_version:[13,3,1,""]},"docplex.mp.error_handler":{DefaultErrorHandler:[14,1,1,""],InfoLevel:[14,1,1,""]},"docplex.mp.kpi":{DecisionKPI:[15,1,1,""],FunctionalKPI:[15,1,1,""],KPI:[15,1,1,""]},"docplex.mp.kpi.DecisionKPI":{compute:[15,2,1,""],get_model:[15,2,1,""],is_decision_expression:[15,2,1,""]},"docplex.mp.kpi.FunctionalKPI":{get_model:[15,2,1,""],is_decision_expression:[15,2,1,""]},"docplex.mp.kpi.KPI":{get_model:[15,2,1,""],is_decision_expression:[15,2,1,""]},"docplex.mp.linear":{AbstractLinearExpr:[16,1,1,""],ConstantExpr:[16,1,1,""],DOCplexQuadraticArithException:[16,7,1,""],LinearExpr:[16,1,1,""],MonomialExpr:[16,1,1,""],ZeroExpr:[16,1,1,""]},"docplex.mp.linear.AbstractLinearExpr":{get_coef:[16,2,1,""]},"docplex.mp.linear.ConstantExpr":{contains_var:[16,2,1,""],iter_variables:[16,2,1,""],number_of_variables:[16,2,1,""]},"docplex.mp.linear.LinearExpr":{add:[16,2,1,""],add_term:[16,2,1,""],clone:[16,2,1,""],constant:[16,3,1,""],contains_var:[16,2,1,""],divide:[16,2,1,""],equals:[16,2,1,""],equals_constant:[16,2,1,""],is_constant:[16,2,1,""],is_discrete:[16,2,1,""],iter_terms:[16,2,1,""],multiply:[16,2,1,""],negate:[16,2,1,""],number_of_variables:[16,2,1,""],plus:[16,2,1,""],quotient:[16,2,1,""],remove_term:[16,2,1,""],solution_value:[16,3,1,""],subtract:[16,2,1,""],times:[16,2,1,""]},"docplex.mp.linear.MonomialExpr":{contains_var:[16,2,1,""],number_of_variables:[16,2,1,""]},"docplex.mp.linear.ZeroExpr":{contains_var:[16,2,1,""],number_of_variables:[16,2,1,""]},"docplex.mp.model":{Model:[17,1,1,""]},"docplex.mp.model.Model":{"var":[17,2,1,""],abs:[17,2,1,""],add_constraint:[17,2,1,""],add_constraint_:[17,2,1,""],add_constraints:[17,2,1,""],add_constraints_:[17,2,1,""],add_equivalence:[17,2,1,""],add_equivalence_constraints:[17,2,1,""],add_equivalence_constraints_:[17,2,1,""],add_equivalences:[17,2,1,""],add_if_then:[17,2,1,""],add_indicator:[17,2,1,""],add_indicator_constraints:[17,2,1,""],add_indicator_constraints_:[17,2,1,""],add_indicators:[17,2,1,""],add_kpi:[17,2,1,""],add_lazy_constraint:[17,2,1,""],add_lazy_constraints:[17,2,1,""],add_mip_start:[17,2,1,""],add_progress_listener:[17,2,1,""],add_quadratic_constraints:[17,2,1,""],add_range:[17,2,1,""],add_sos1:[17,2,1,""],add_sos2:[17,2,1,""],add_sos:[17,2,1,""],add_user_cut_constraint:[17,2,1,""],add_user_cut_constraints:[17,2,1,""],binary_var:[17,2,1,""],binary_var_cube:[17,2,1,""],binary_var_dict:[17,2,1,""],binary_var_list:[17,2,1,""],binary_var_matrix:[17,2,1,""],binary_vartype:[17,3,1,""],blended_objective_values:[17,3,1,""],build_multiobj_paramsets:[17,2,1,""],change_var_lower_bounds:[17,2,1,""],change_var_upper_bounds:[17,2,1,""],clear:[17,2,1,""],clear_constraints:[17,2,1,""],clear_kpis:[17,2,1,""],clear_lazy_constraints:[17,2,1,""],clear_mip_starts:[17,2,1,""],clear_multi_objective:[17,2,1,""],clear_progress_listeners:[17,2,1,""],clear_sos:[17,2,1,""],clear_user_cut_constraints:[17,2,1,""],clone:[17,2,1,""],continuous_var:[17,2,1,""],continuous_var_cube:[17,2,1,""],continuous_var_dict:[17,2,1,""],continuous_var_list:[17,2,1,""],continuous_var_matrix:[17,2,1,""],continuous_vartype:[17,3,1,""],cplex:[17,3,1,""],create_parameter_sets:[17,2,1,""],dot:[17,2,1,""],dotf:[17,2,1,""],dual_values:[17,2,1,""],dump_as_sav:[17,2,1,""],end:[17,2,1,""],eq_constraint:[17,2,1,""],equivalence_constraint:[17,2,1,""],export_as_lp:[17,2,1,""],export_as_lp_string:[17,2,1,""],export_as_mps:[17,2,1,""],export_as_mps_string:[17,2,1,""],export_as_sav:[17,2,1,""],export_as_sav_string:[17,2,1,""],export_as_savgz:[17,2,1,""],export_priority_order_file:[17,2,1,""],export_to_stream:[17,2,1,""],find_matching_linear_constraints:[17,2,1,""],find_matching_quadratic_constraints:[17,2,1,""],find_matching_vars:[17,2,1,""],find_re_matching_vars:[17,2,1,""],float_precision:[17,3,1,""],ge_constraint:[17,2,1,""],get_constraint_by_index:[17,2,1,""],get_constraint_by_name:[17,2,1,""],get_cplex:[17,2,1,""],get_cuts:[17,2,1,""],get_num_cuts:[17,2,1,""],get_objective_expr:[17,2,1,""],get_parameter_from_id:[17,2,1,""],get_quadratic_constraint_by_index:[17,2,1,""],get_solve_status:[17,2,1,""],get_time_limit:[17,2,1,""],get_var_by_name:[17,2,1,""],has_basis:[17,2,1,""],has_multi_objective:[17,2,1,""],if_then:[17,2,1,""],ignore_names:[17,3,1,""],import_solution:[17,2,1,""],indicator_constraint:[17,2,1,""],infinity:[17,3,1,""],init_numpy:[17,4,1,""],integer_var:[17,2,1,""],integer_var_cube:[17,2,1,""],integer_var_dict:[17,2,1,""],integer_var_list:[17,2,1,""],integer_var_matrix:[17,2,1,""],integer_vartype:[17,3,1,""],is_maximized:[17,2,1,""],is_minimized:[17,2,1,""],is_optimized:[17,2,1,""],iter_binary_constraints:[17,2,1,""],iter_binary_vars:[17,2,1,""],iter_constraints:[17,2,1,""],iter_continuous_vars:[17,2,1,""],iter_equivalence_constraints:[17,2,1,""],iter_indicator_constraints:[17,2,1,""],iter_integer_vars:[17,2,1,""],iter_kpis:[17,2,1,""],iter_lazy_constraints:[17,2,1,""],iter_linear_constraints:[17,2,1,""],iter_mip_starts:[17,2,1,""],iter_progress_listeners:[17,2,1,""],iter_pwl_constraints:[17,2,1,""],iter_pwl_functions:[17,2,1,""],iter_quadratic_constraints:[17,2,1,""],iter_range_constraints:[17,2,1,""],iter_semicontinuous_vars:[17,2,1,""],iter_semiinteger_vars:[17,2,1,""],iter_sos1:[17,2,1,""],iter_sos2:[17,2,1,""],iter_sos:[17,2,1,""],iter_user_cut_constraints:[17,2,1,""],iter_variables:[17,2,1,""],kpi_by_name:[17,2,1,""],kpi_value_by_name:[17,2,1,""],kpis_as_dict:[17,2,1,""],le_constraint:[17,2,1,""],linear_constraint:[17,2,1,""],linear_constraint_basis_statuses:[17,2,1,""],linear_expr:[17,2,1,""],logical_and:[17,2,1,""],logical_not:[17,2,1,""],logical_or:[17,2,1,""],lp_line_length:[17,3,1,""],lp_string:[17,3,1,""],max:[17,2,1,""],maximize:[17,2,1,""],maximize_static_lex:[17,2,1,""],min:[17,2,1,""],minimize:[17,2,1,""],minimize_static_lex:[17,2,1,""],mip_starts:[17,3,1,""],multi_objective_values:[17,3,1,""],name:[17,3,1,""],number_of_binary_variables:[17,3,1,""],number_of_constraints:[17,3,1,""],number_of_continuous_variables:[17,3,1,""],number_of_equivalence_constraints:[17,3,1,""],number_of_indicator_constraints:[17,3,1,""],number_of_integer_variables:[17,3,1,""],number_of_lazy_constraints:[17,3,1,""],number_of_linear_constraints:[17,3,1,""],number_of_mip_starts:[17,3,1,""],number_of_progress_listeners:[17,3,1,""],number_of_pwl_constraints:[17,3,1,""],number_of_quadratic_constraints:[17,3,1,""],number_of_range_constraints:[17,3,1,""],number_of_semicontinuous_variables:[17,3,1,""],number_of_semiinteger_variables:[17,3,1,""],number_of_sos1:[17,3,1,""],number_of_sos2:[17,3,1,""],number_of_sos:[17,3,1,""],number_of_user_constraints:[17,3,1,""],number_of_user_cut_constraints:[17,3,1,""],number_of_variables:[17,3,1,""],objective_coef:[17,2,1,""],objective_expr:[17,3,1,""],objective_sense:[17,3,1,""],objective_value:[17,3,1,""],parameters:[17,3,1,""],piecewise:[17,2,1,""],piecewise_as_slopes:[17,2,1,""],populate:[17,2,1,""],populate_solution_pool:[17,2,1,""],print_information:[17,2,1,""],print_solution:[17,2,1,""],problem_type:[17,3,1,""],quad_expr:[17,2,1,""],quadratic_dual_slacks:[17,2,1,""],quality_metrics:[17,3,1,""],range_constraint:[17,2,1,""],read_basis_file:[17,2,1,""],read_mip_starts:[17,2,1,""],read_priority_order_file:[17,2,1,""],reduced_costs:[17,2,1,""],remove:[17,2,1,""],remove_constraint:[17,2,1,""],remove_constraints:[17,2,1,""],remove_kpi:[17,2,1,""],remove_objective:[17,2,1,""],remove_progress_listener:[17,2,1,""],report:[17,2,1,""],report_kpis:[17,2,1,""],restore_numpy:[17,4,1,""],round_solution:[17,3,1,""],scal_prod:[17,2,1,""],scal_prod_f:[17,2,1,""],scal_prod_vars_all_different:[17,2,1,""],semicontinuous_var:[17,2,1,""],semicontinuous_var_dict:[17,2,1,""],semicontinuous_var_list:[17,2,1,""],semicontinuous_var_matrix:[17,2,1,""],semicontinuous_vartype:[17,3,1,""],semiinteger_var:[17,2,1,""],semiinteger_var_dict:[17,2,1,""],semiinteger_var_list:[17,2,1,""],semiinteger_var_matrix:[17,2,1,""],semiinteger_vartype:[17,3,1,""],set_lex_multi_objective:[17,2,1,""],set_lp_start_basis:[17,2,1,""],set_multi_objective:[17,2,1,""],set_multi_objective_abstols:[17,2,1,""],set_multi_objective_exprs:[17,2,1,""],set_multi_objective_reltols:[17,2,1,""],set_objective:[17,2,1,""],set_time_limit:[17,2,1,""],slack_values:[17,2,1,""],solution:[17,3,1,""],solve:[17,2,1,""],solve_details:[17,3,1,""],solve_status:[17,3,1,""],solve_with_goals:[17,2,1,""],statistics:[17,3,1,""],str_use_space:[17,3,1,""],sum:[17,2,1,""],sum_squares:[17,2,1,""],sum_vars:[17,2,1,""],sum_vars_all_different:[17,2,1,""],sums:[17,2,1,""],sumsq:[17,2,1,""],time_limit:[17,3,1,""],var_basis_statuses:[17,2,1,""],var_hypercube:[17,2,1,""]},"docplex.mp.model_reader":{ModelReader:[18,1,1,""],ModelReaderError:[18,7,1,""],read_model:[18,5,1,""]},"docplex.mp.model_reader.ModelReader":{read:[18,6,1,""],read_model:[18,6,1,""],read_prm:[18,6,1,""]},"docplex.mp.model_stats":{ModelStatistics:[19,1,1,""]},"docplex.mp.model_stats.ModelStatistics":{number_of_binary_variables:[19,3,1,""],number_of_continuous_variables:[19,3,1,""],number_of_eq_constraints:[19,3,1,""],number_of_equivalence_constraints:[19,3,1,""],number_of_ge_constraints:[19,3,1,""],number_of_indicator_constraints:[19,3,1,""],number_of_integer_variables:[19,3,1,""],number_of_le_constraints:[19,3,1,""],number_of_linear_constraints:[19,3,1,""],number_of_quadratic_constraints:[19,3,1,""],number_of_range_constraints:[19,3,1,""],number_of_semicontinuous_variables:[19,3,1,""],number_of_semiinteger_variables:[19,3,1,""],number_of_variables:[19,3,1,""],print_information:[19,2,1,""]},"docplex.mp.params":{parameters:[20,0,0,"-"]},"docplex.mp.params.parameters":{BoolParameter:[20,1,1,""],IntParameter:[20,1,1,""],NumParameter:[20,1,1,""],Parameter:[20,1,1,""],ParameterGroup:[20,1,1,""],PositiveIntParameter:[20,1,1,""],RootParameterGroup:[20,1,1,""],StrParameter:[20,1,1,""]},"docplex.mp.params.parameters.BoolParameter":{accept_value:[20,2,1,""]},"docplex.mp.params.parameters.IntParameter":{accept_value:[20,2,1,""]},"docplex.mp.params.parameters.NumParameter":{accept_value:[20,2,1,""]},"docplex.mp.params.parameters.Parameter":{accept_value:[20,2,1,""],cpx_id:[20,3,1,""],cpx_name:[20,3,1,""],default_value:[20,3,1,""],description:[20,3,1,""],get:[20,2,1,""],is_default:[20,2,1,""],is_nondefault:[20,2,1,""],qualified_name:[20,3,1,""],reset:[20,2,1,""],set:[20,2,1,""],to_string:[20,2,1,""]},"docplex.mp.params.parameters.ParameterGroup":{clone:[20,2,1,""],generate_nondefault_params:[20,2,1,""],generate_params:[20,2,1,""],is_root:[20,2,1,""],iter_params:[20,2,1,""],name:[20,3,1,""],number_of_params:[20,3,1,""],number_of_subgroups:[20,3,1,""],parent_group:[20,3,1,""],qualified_name:[20,2,1,""],reset:[20,2,1,""],total_number_of_params:[20,2,1,""]},"docplex.mp.params.parameters.RootParameterGroup":{export_prm:[20,2,1,""],export_prm_to_string:[20,2,1,""],is_root:[20,2,1,""],print_info_to_stream:[20,2,1,""],print_info_to_string:[20,2,1,""],qualified_name:[20,2,1,""]},"docplex.mp.params.parameters.StrParameter":{accept_value:[20,2,1,""],to_string:[20,2,1,""]},"docplex.mp.priority":{Priority:[21,1,1,""]},"docplex.mp.progress":{FunctionalSolutionListener:[22,1,1,""],KpiListener:[22,1,1,""],KpiPrinter:[22,1,1,""],ProgressClock:[22,1,1,""],ProgressData:[22,1,1,""],ProgressDataRecorder:[22,1,1,""],ProgressListener:[22,1,1,""],SolutionListener:[22,1,1,""],SolutionRecorder:[22,1,1,""],TextProgressListener:[22,1,1,""]},"docplex.mp.progress.FunctionalSolutionListener":{notify_solution:[22,2,1,""]},"docplex.mp.progress.KpiListener":{notify_solution:[22,2,1,""],publish:[22,2,1,""]},"docplex.mp.progress.KpiPrinter":{publish:[22,2,1,""]},"docplex.mp.progress.ProgressData":{best_bound:[22,3,1,""],current_nb_iterations:[22,3,1,""],current_nb_nodes:[22,3,1,""],current_objective:[22,3,1,""],det_time:[22,3,1,""],has_incumbent:[22,3,1,""],mip_gap:[22,3,1,""],remaining_nb_nodes:[22,3,1,""],time:[22,3,1,""]},"docplex.mp.progress.ProgressDataRecorder":{iter_recorded:[22,3,1,""],notify_progress:[22,2,1,""],notify_start:[22,2,1,""],recorded:[22,3,1,""]},"docplex.mp.progress.ProgressListener":{abort:[22,2,1,""],clock:[22,3,1,""],current_progress_data:[22,3,1,""],notify_end:[22,2,1,""],notify_progress:[22,2,1,""],notify_solution:[22,2,1,""],notify_start:[22,2,1,""]},"docplex.mp.progress.SolutionListener":{notify_solution:[22,2,1,""],notify_start:[22,2,1,""]},"docplex.mp.progress.SolutionRecorder":{iter_solutions:[22,2,1,""],notify_solution:[22,2,1,""],notify_start:[22,2,1,""],number_of_solutions:[22,3,1,""]},"docplex.mp.progress.TextProgressListener":{notify_progress:[22,2,1,""],notify_start:[22,2,1,""]},"docplex.mp.publish":{PublishResultAsDf:[23,1,1,""]},"docplex.mp.publish.PublishResultAsDf":{write_output_table:[23,2,1,""]},"docplex.mp.pwl":{PwlFunction:[24,1,1,""]},"docplex.mp.pwl.PwlFunction":{add:[24,2,1,""],clone:[24,2,1,""],divide:[24,2,1,""],evaluate:[24,2,1,""],multiply:[24,2,1,""],plot:[24,2,1,""],subtract:[24,2,1,""],translate:[24,2,1,""]},"docplex.mp.quad":{QuadExpr:[25,1,1,""]},"docplex.mp.quad.QuadExpr":{clone:[25,2,1,""],constant:[25,3,1,""],contains_var:[25,2,1,""],get_quadratic_coefficient:[25,2,1,""],has_quadratic_term:[25,2,1,""],is_quad_expr:[25,2,1,""],is_separable:[25,2,1,""],iter_quad_triplets:[25,2,1,""],iter_terms:[25,2,1,""],linear_part:[25,3,1,""],number_of_quadratic_terms:[25,3,1,""]},"docplex.mp.relax_linear":{LinearRelaxer:[26,1,1,""]},"docplex.mp.relax_linear.LinearRelaxer":{linear_relaxation:[26,2,1,""]},"docplex.mp.relaxer":{FunctionalPrioritizer:[27,1,1,""],MappingPrioritizer:[27,1,1,""],MatchNamePrioritizer:[27,1,1,""],NamedPrioritizer:[27,1,1,""],Prioritizer:[27,1,1,""],Relaxer:[27,1,1,""],TOutputTables:[27,1,1,""],UniformPrioritizer:[27,1,1,""]},"docplex.mp.relaxer.MatchNamePrioritizer":{get_priority:[27,2,1,""]},"docplex.mp.relaxer.Relaxer":{get_relaxation:[27,2,1,""],is_relaxed:[27,2,1,""],iter_relaxations:[27,2,1,""],number_of_relaxations:[27,3,1,""],relax:[27,2,1,""],relaxations:[27,2,1,""],relaxed_objective_value:[27,3,1,""]},"docplex.mp.relaxer.TOutputTables":{Amount:[27,3,1,""],Constraint:[27,3,1,""],Priority:[27,3,1,""]},"docplex.mp.sdetails":{SolveDetails:[28,1,1,""]},"docplex.mp.sdetails.SolveDetails":{best_bound:[28,3,1,""],columns:[28,3,1,""],deterministic_time:[28,3,1,""],dettime:[28,3,1,""],gap:[28,3,1,""],has_hit_limit:[28,2,1,""],mip_relative_gap:[28,3,1,""],nb_iterations:[28,3,1,""],nb_linear_nonzeros:[28,3,1,""],nb_nodes_processed:[28,3,1,""],problem_type:[28,3,1,""],status:[28,3,1,""],status_code:[28,3,1,""],time:[28,3,1,""]},"docplex.mp.solution":{SolutionPool:[29,1,1,""],SolveSolution:[29,1,1,""]},"docplex.mp.solution.SolutionPool":{describe_objectives:[29,2,1,""],export_as_sol:[29,2,1,""],mean_objective_value:[29,3,1,""],size:[29,3,1,""],stats:[29,3,1,""]},"docplex.mp.solution.SolveSolution":{"export":[29,2,1,""],add_var_value:[29,2,1,""],as_df:[29,2,1,""],check_as_mip_start:[29,2,1,""],clear:[29,2,1,""],contains:[29,2,1,""],export_as_json_string:[29,2,1,""],export_as_mst:[29,2,1,""],export_as_sol:[29,2,1,""],from_file:[29,6,1,""],get_blended_objective_value_by_priority:[29,2,1,""],get_cuts:[29,2,1,""],get_dual_values:[29,2,1,""],get_num_cuts:[29,2,1,""],get_objective_value:[29,2,1,""],get_reduced_costs:[29,2,1,""],get_sensitivity:[29,2,1,""],get_slacks:[29,2,1,""],get_status:[29,2,1,""],get_value:[29,2,1,""],get_value_df:[29,2,1,""],get_value_dict:[29,2,1,""],get_value_list:[29,2,1,""],get_values:[29,2,1,""],has_objective:[29,2,1,""],is_empty:[29,2,1,""],is_feasible_solution:[29,2,1,""],is_valid_solution:[29,2,1,""],iter_var_values:[29,2,1,""],iter_variables:[29,2,1,""],kpi_value_by_name:[29,2,1,""],model:[29,3,1,""],multi_objective_values:[29,3,1,""],name:[29,3,1,""],number_of_var_values:[29,3,1,""],objective_value:[29,3,1,""],print_mst:[29,2,1,""],set_objective_value:[29,2,1,""],size:[29,3,1,""],slack_value:[29,2,1,""],solve_details:[29,3,1,""],solved_by:[29,3,1,""],update:[29,2,1,""]},"docplex.mp.sosvarset":{SOSVariableSet:[30,1,1,""]},"docplex.mp.sosvarset.SOSVariableSet":{benders_annotation:[30,3,1,""],iter_variables:[30,2,1,""],sos_type:[30,3,1,""],to_string:[30,2,1,""]},"docplex.mp.vartype":{BinaryVarType:[31,1,1,""],ContinuousVarType:[31,1,1,""],IntegerVarType:[31,1,1,""],SemiContinuousVarType:[31,1,1,""],SemiIntegerVarType:[31,1,1,""],VarType:[31,1,1,""]},"docplex.mp.vartype.BinaryVarType":{is_discrete:[31,2,1,""]},"docplex.mp.vartype.ContinuousVarType":{is_discrete:[31,2,1,""]},"docplex.mp.vartype.IntegerVarType":{is_discrete:[31,2,1,""]},"docplex.mp.vartype.SemiContinuousVarType":{default_lb:[31,3,1,""],is_discrete:[31,2,1,""]},"docplex.mp.vartype.SemiIntegerVarType":{default_lb:[31,3,1,""],is_discrete:[31,2,1,""]},"docplex.mp.vartype.VarType":{cplex_typecode:[31,3,1,""],default_lb:[31,3,1,""],default_ub:[31,3,1,""],is_discrete:[31,2,1,""],short_name:[31,3,1,""],to_string:[31,2,1,""]},"docplex.mp.with_funcs":{model_objective:[32,5,1,""],model_parameters:[32,5,1,""],model_solvefixed:[32,5,1,""]},"docplex.util":{csv_utils:[34,0,0,"-"],environment:[35,0,0,"-"],logging_utils:[36,0,0,"-"]},"docplex.util.csv_utils":{encode_csv_string:[34,5,1,""],write_table_as_csv:[34,5,1,""]},"docplex.util.environment":{AbstractLocalEnvironment:[35,1,1,""],Environment:[35,1,1,""],LocalEnvironment:[35,1,1,""],NotAvailableError:[35,7,1,""],OverrideEnvironment:[35,1,1,""],SolveDetailsFilter:[35,1,1,""],WSNotebookEnvironment:[35,1,1,""],WorkerEnvironment:[35,1,1,""],add_abort_callback:[35,5,1,""],default_solution_storage_handler:[35,5,1,""],get_available_core_count:[35,5,1,""],get_environment:[35,5,1,""],get_input_stream:[35,5,1,""],get_output_stream:[35,5,1,""],get_parameter:[35,5,1,""],make_attachment_name:[35,5,1,""],maketrans:[35,5,1,""],read_df:[35,5,1,""],remove_abort_callback:[35,5,1,""],set_output_attachment:[35,5,1,""],update_solve_details:[35,5,1,""],write_df:[35,5,1,""]},"docplex.util.environment.AbstractLocalEnvironment":{get_available_core_count:[35,2,1,""],get_input_stream:[35,2,1,""],get_output_stream:[35,2,1,""],get_parameter:[35,2,1,""],get_parameters:[35,2,1,""],set_output_attachment:[35,2,1,""]},"docplex.util.environment.Environment":{abort_callbacks:[35,3,1,""],get_available_core_count:[35,2,1,""],get_engine_log_level:[35,2,1,""],get_input_stream:[35,2,1,""],get_output_stream:[35,2,1,""],get_parameter:[35,2,1,""],get_parameters:[35,2,1,""],get_stop_callback:[35,2,1,""],is_debug_mode:[35,2,1,""],is_dods:[35,2,1,""],publish_solve_details:[35,2,1,""],read_df:[35,2,1,""],record_history_fields:[35,3,1,""],record_history_size:[35,3,1,""],record_interval:[35,3,1,""],set_output_attachment:[35,2,1,""],set_stop_callback:[35,2,1,""],solution_storage_handler:[35,3,1,""],stop_callback:[35,3,1,""],store_solution:[35,2,1,""],update_solve_details:[35,2,1,""],write_df:[35,2,1,""]},"docplex.util.environment.SolveDetailsFilter":{filter:[35,2,1,""]},"docplex.util.environment.WorkerEnvironment":{get_available_core_count:[35,2,1,""],get_input_stream:[35,2,1,""],get_output_stream:[35,2,1,""],get_parameter:[35,2,1,""],get_parameters:[35,2,1,""],get_stop_callback:[35,2,1,""],publish_solve_details:[35,2,1,""],set_output_attachment:[35,2,1,""],set_stop_callback:[35,2,1,""]},"docplex.util.logging_utils":{DocplexLogger:[36,1,1,""],LoggerToDocloud:[36,1,1,""],LoggerToFile:[36,1,1,""]},"docplex.util.logging_utils.DocplexLogger":{critical:[36,2,1,""],debug:[36,2,1,""],error:[36,2,1,""],info:[36,2,1,""],log:[36,2,1,""],warning:[36,2,1,""]},"docplex.util.logging_utils.LoggerToDocloud":{log:[36,2,1,""]},"docplex.util.logging_utils.LoggerToFile":{log:[36,2,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","attribute","Python attribute"],"4":["py","staticmethod","Python static method"],"5":["py","function","Python function"],"6":["py","classmethod","Python class method"],"7":["py","exception","Python exception"]},objtypes:{"0":"py:module","1":"py:class","2":"py:method","3":"py:attribute","4":"py:staticmethod","5":"py:function","6":"py:classmethod","7":"py:exception"},terms:{"0000000000000001e":[46,47],"02d":[44,45],"0x23fbad3d588":56,"0x23fd903bf60":56,"0x23fda312710":56,"0x23fda37f9b0":56,"0x23fda3c7e80":56,"0x279df51e710":46,"0x279e05915f8":46,"10am":2,"10g":4,"12pm":2,"13bfa4c7":2,"17worktime_":46,"17worktime_betsi":46,"18averageworktim":47,"24x7":54,"25worktime_ann":46,"28worktime_bethani":46,"2min":[44,45],"2x2x4":17,"49er":[52,53],"4d83":2,"5pm":2,"5th":38,"64_linux":13,"64bit":[1,2,13,43,46,47,53,56,57],"6pm":2,"78ce":2,"8am":46,"8pm":2,"9am":2,"abstract":[8,15,17,22,27,31],"boolean":[6,8,12,16,17,20,22,25,28,29,31],"break":[4,40],"byte":[17,35],"case":[1,10,12,15,17,22,27,28,29,35,43,48,57],"class":[3,4,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,35,36,41,44,45,46,53,57],"default":[1,2,3,4,8,10,11,12,14,17,18,20,22,24,26,27,29,31,32,35,37,42,43,46,47,48,53,56,57],"enum":[9,11,14,21,22],"export":[5,17,18,20,29,44,45,49,52],"final":[17,44,45,46,47,48,57],"float":[3,4,7,8,12,15,16,17,20,22,28,29,40,43,48],"function":[3,4,5,7,10,13,16,17,18,20,22,24,30,32,35,41,43,46,48,51,53,57],"import":[4,5,13,17,24,35,40,41,44,45,49,52,57],"int":[5,35],"new":[1,3,4,7,8,9,10,11,16,17,20,22,24,26,29,32,38,40,43,52,53,56],"null":[2,17,35],"public":2,"return":[3,4,5,6,7,8,9,10,12,13,15,16,17,18,19,20,22,23,24,25,26,27,28,29,30,31,32,34,35,40,41,43,44,45,46,49,52,57],"short":[9,17,31],"static":[7,8,11,17],"super":[48,53],"switch":56,"true":[4,5,6,8,9,10,11,12,13,15,16,17,18,20,23,25,26,27,28,29,31,35,40,41,44,45,46,57],"try":[13,17,57],"var":[3,4,6,7,9,12,16,17,25,29,41,57],"while":[4,5,9,11,17,18,40,43,47,48,49,51,53,55],AND:17,Are:46,But:[56,57],COS:45,For:[1,3,7,8,10,12,17,22,25,27,28,29,35,37,38,42,43,46,47,48,51,53,55,56,57],Gas:48,MPS:[17,18],One:17,SOS:[9,17,26,29,30],Such:42,That:56,The:[1,2,3,5,6,8,9,10,11,12,13,14,15,16,17,18,19,20,22,23,24,25,27,28,29,30,32,34,35,37,38,40,41,42,44,45,48,49,51,52,55],Then:[1,17,42,43,46,56],There:[2,3,9,17,48,53,57],These:[0,20,42,43,46,55],Use:[7,10,12,17,18,35,37,38],Used:[17,41],Using:17,Wes:38,Will:42,With:[0,1,2,17,39,43,46,47,53,56],Yes:2,__call_:7,__call__:7,__contains_:29,__eq__:[12,16],__getitem__:29,__init__:[4,7,23,44,45],__main__:[4,5,40,41,44,45,49,52],__name_0__:17,__name_1__:17,__name_:17,__name__:[4,5,40,41,44,45,49,52],__str__:[4,20,30,41,44,45,57],_abstractbendersannot:[10,12,30],_abstractmodelobject:6,_abstractnam:[6,15],_abstractprogresslisten:22,_abstractvalu:[6,12,15],_all_dai:[44,45],_end_time_of_dai:[44,45],_get_supply_cost:57,_is_migr:41,_name__2__:17,_origin:6,_start_time_of_dai:[44,45],_subplot:[46,56],_subscriptionmixin:[16,25],_tconflictconstraint:8,_tprogressdata:22,_weekdai:[44,45],abdul:42,abort:[22,35],abort_callback:35,about:[8,13,17,22,29,37,38,39,55],abov:[2,14,40,43,46,47,48,52,56],abs:[4,17],abs_end_tim:46,absdiff:22,absolut:[17,22,44,45,47,56,57],abstol:17,abstractconstraint:10,abstracterrorhandl:14,abstractlinearexpr:16,abstractlocalenviron:35,academ:[0,37],accapt:17,accept:[8,12,17,18,20,22,27,29,32,35,42],accept_valu:20,access:[0,3,15,17,22,27,29,35,37,38,54],accessor:[3,17],accomplish:[46,47],accord:[3,9,35,46,47,48,51],account:[0,1,2,12,37,38,42,43,46,47,48,53,56],accredit:[0,37],accuraci:2,achiev:[4,41,42,51,57],acquir:2,across:[41,46,51,57],act:[1,2,17,27,42,43,46,47,48,53,56],action:[1,2,42,43,46,47,48,53,56],activ:[3,10,17,41,44,45],active_serv:41,active_valu:[10,17],active_var_by_serv:41,actual:[29,32,35,46,48,57],adapt:35,add:[0,1,4,5,10,16,17,24,29,35,39,43,45,46,56],add_abort_callback:35,add_constraint:[3,4,17,40,41,44,45,49,52,57],add_constraint_:17,add_constraints_:17,add_equival:17,add_equivalence_constraint:17,add_equivalence_constraints_:17,add_if_then:17,add_ind:[10,17],add_indicator_constraint:17,add_indicator_constraints_:17,add_kpi:[5,17,40,41,44,45,49],add_lazy_constraint:17,add_mip_start:17,add_pattern_to_master_model:4,add_progress_listen:[17,22],add_quadratic_constraint:17,add_rang:[5,10,17],add_so:[17,30],add_sos1:[17,30],add_sos2:[17,30],add_term:16,add_user_cut_constraint:17,add_var_valu:29,added:[3,10,16,17,24,46,47],adding:[3,17,22,46,47,56,57],addit:[20,22,24,29,35,46,47,48,54,55],addition:37,adjust:3,advantag:[1,2,42,43,46,47,48,53,54,56],adventur:40,advertis:48,advis:11,afc:53,after:[2,17,24,29,35,38,48,53],again:[1,17,40,43,46,56],agent:11,aggreg:[43,46,56],aim:[3,49],aix:38,albani:2,alekseeva:42,alex:38,algorithm:[3,5,8,9,22,27,28,46,51],alia:[8,27],align:1,all:[1,2,3,6,7,8,9,10,11,12,14,16,17,18,19,20,22,24,25,27,29,31,32,35,41,43,44,45,46,47,48,51,52,53,56,57],all_item:4,all_nurs:[44,45,46],all_nurses_1:46,all_nurses_2:46,all_pattern:4,all_shift:[44,45,46],allexpress:17,alloc:[17,44,45],allow:[3,17,20,29,35,37],allvar:[9,17,29],alon:56,along:[24,46,47],alreadi:[11,35,37,38],also:[3,7,9,12,17,19,20,22,29,35,38,39,43,46,48,53,56,57],altern:[17,38],although:10,alwai:[2,8,17,20,22,27,46,47],amn:7,among:[7,17,46],amongst:46,amount:[5,24,27,43,48,49],anaconda:[37,38],anacondacloud:37,anaesthesiolog:[44,45],analog:17,analysi:[38,42],analyt:[1,2,42,43,46,47,48,53,56],analyz:[1,2,4,8,38,43,47,48,53,56],anchor:17,ani:[2,10,12,16,17,18,20,22,25,29,43,46,47,53,56,57],ann:[44,45,46,47],annot:[10,12,30],anonym:[17,46],anoth:[2,6,17,46,47,48],answer:[51,56],anumb:17,anymor:17,anyth:[17,38],anywai:26,apach:[0,4,5,39,40,41,44,45,49,52],api:[7,29,39],append:[4,17,22,29,35,41,43,57],appl:5,appli:[5,17,27,43,46,57],applic:[0,2,24,39,42,48,51,54],appropri:[35,42,51,52],approxim:5,arbitrari:[17,47],archiv:[0,51],arg:[4,11,17,18,24,29],arg_demand:4,arg_id:4,arg_siz:4,argument:[3,5,7,8,12,15,17,18,20,22,24,26,27,29,35,43,46,57],arithmet:[3,17,53],arizona:[52,53],arm:5,armi:5,arrai:[4,17,48,53],arrang:1,arriv:[8,53],artefact:[41,57],articl:40,as_df:29,ascii:35,asingl:17,ask:55,asrgument:17,assert:[4,12,40,44,45,52,57],assign:[1,8,17,27,40,41,44,45,51,52,53,57],assign_anne_0:46,assign_anne_1:46,assign_anne_28:46,assign_anne_29:46,assign_anne_2:46,assign_anne_30:46,assign_anne_31:46,assign_anne_32:46,assign_anne_33:46,assign_anne_34:46,assign_anne_35:46,assign_anne_36:46,assign_anne_37:46,assign_anne_38:46,assign_anne_39:46,assign_anne_3:46,assign_anne_40:46,assign_anne_4:46,assign_anne_5:46,assign_anne_6:46,assign_anne_7:46,assign_anne_8:46,assign_anne_9:46,assign_bethanie_0:46,assign_bethanie_1:46,assign_bethanie_2:46,assign_bethanie_31:46,assign_bethanie_32:46,assign_bethanie_33:46,assign_bethanie_34:46,assign_bethanie_35:46,assign_bethanie_36:46,assign_bethanie_37:46,assign_bethanie_38:46,assign_bethanie_39:46,assign_bethanie_3:46,assign_bethanie_40:46,assign_bethanie_4:46,assign_bethanie_5:46,assign_bethanie_6:46,assign_bethanie_7:46,assign_bethanie_8:46,assign_bethanie_9:46,assign_betsy_0:46,assign_betsy_1:46,assign_betsy_2:46,assign_betsy_31:46,assign_betsy_32:46,assign_betsy_33:46,assign_betsy_34:46,assign_betsy_35:46,assign_betsy_36:46,assign_betsy_37:46,assign_betsy_38:46,assign_betsy_39:46,assign_betsy_3:46,assign_betsy_40:46,assign_betsy_4:46,assign_betsy_5:46,assign_betsy_6:46,assign_betsy_7:46,assign_betsy_8:46,assign_betsy_9:46,assign_cathy_0:46,assign_cathy_1:46,assign_cathy_2:46,assign_cathy_31:46,assign_cathy_32:46,assign_cathy_33:46,assign_cathy_34:46,assign_cathy_35:46,assign_cathy_36:46,assign_cathy_37:46,assign_cathy_38:46,assign_cathy_39:46,assign_cathy_3:46,assign_cathy_40:46,assign_cathy_4:46,assign_cathy_5:46,assign_cathy_6:46,assign_cathy_7:46,assign_cathy_8:46,assign_cathy_9:46,assign_cecilia_0:46,assign_cecilia_1:46,assign_cecilia_2:46,assign_cecilia_31:46,assign_cecilia_32:46,assign_cecilia_33:46,assign_cecilia_34:46,assign_cecilia_35:46,assign_cecilia_36:46,assign_cecilia_37:46,assign_cecilia_38:46,assign_cecilia_39:46,assign_cecilia_3:46,assign_cecilia_40:46,assign_cecilia_4:46,assign_cecilia_5:46,assign_cecilia_6:46,assign_cecilia_7:46,assign_cecilia_8:46,assign_cecilia_9:46,assign_dee_0:46,assign_dee_1:46,assign_dee_2:46,assign_dee_3:46,assign_dee_4:46,assign_isabelle_0:46,assign_isabelle_1:46,assign_isabelle_2:46,assign_isabelle_3:46,assign_isabelle_4:46,assign_patricia_0:46,assign_patricia_1:46,assign_patricia_2:46,assign_patricia_3:46,assign_patricia_4:46,assign_patrick_0:46,assign_patrick_1:46,assign_patrick_2:46,assign_patrick_3:46,assign_patrick_4:46,assign_user_to_server_var:41,assigned_1:46,assigned_2:46,assignment_cost_f:44,assist:42,associ:[7,8,10,12,16,17,29,43,44,45],assum:[7,12,17,25,29,43,48,52,53],assume_alldiffer:17,atlanta:[52,53],atlow:9,attach:[15,17,22,34,35,41],attempt:[15,57],attribut:[3,6,11,17],atupp:9,austerlitz:42,auto:[9,17,29],auto_publish:[11,23,35],autom:[1,2,42,43,46,47,48,53,56],automat:[0,9,11,17,35,37,38,43,48,57],autopct:57,autopublish:23,auxiliari:[17,43],avail:[3,10,12,13,17,22,29,35,37,38,43,46,54,55],avenu:2,averag:[44,45,46],average_nurse_work_tim:[44,45],average_work_time_:[44,45],averageworktim:[44,45,47],avg:46,avoid:[9,46,47,56],awai:[1,2,42,43,46,47,48,53,56],awar:17,axes:[46,56],axessubplot:[46,56],axi:[1,24,46,56,57],azevedo:42,b53d:2,b57c60b701cf:2,b_1_1:17,b_1_2:17,b_2_1:17,b_2_2:17,b_i:1,bai:[52,53],balanc:[41,46,47,51],baltimor:[52,53],bank:42,bar:[17,43,48],barrel:48,bas:17,bas_path:17,base:[1,2,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,35,36,38,42,43,46,47,48,53,56],basecontext:11,basenam:[17,18,29],basi:[9,10,12,17],basic:[9,10,12,15,16,17,22,24,25,30,47,48,50,53],basis_statu:[10,12],basisstatu:[9,10,12,17],batch:17,bear:[52,53],beazlei:38,becaus:[5,12,16,28,29,31,43,57],becom:[17,44,45],been:[2,3,6,7,10,12,15,16,17,22,24,27,28,29,32,35,40,46,52],befor:[3,7,17,22,24,28,29,35,37,43,46,47,48,56],begin:[4,22,46,51,57],beginn:38,behavior:[8,11,17,22],behaviour:[11,37,43],being:[2,8,11,16,17,22,35,42,53,57],belong:[6,9,20],below:[22,47,48],bender:[10,12,30],benders_annot:[10,12,30],benefici:42,benefit:47,bengal:[52,53],best:[3,4,22,28,40,42,46,48,53,57],best_bound:[22,28],bestbound:22,bethani:[44,45,46,47],betsi:[44,45,46,47],better:[1,2,9,42,43,46,47,48,53,56],between:[1,3,7,9,10,16,17,22,24,35,43,46,47,51,52,57],big:38,bill:[38,52,53],bin:13,binari:[1,2,3,8,9,10,12,17,19,29,31,35,42,43,44,45,46,47,48,53,56,57],binary_var:[3,10,17],binary_var_cub:17,binary_var_dict:[3,17,41,57],binary_var_list:[3,17,40],binary_var_matrix:[3,17,41,43,44,45,46,52,57],binary_vartyp:17,binaryconstraint:10,binaryvartyp:[17,31],bit:[13,35,38],biz:0,blend:[17,29,48,51],blended_obj_by_prior:29,blended_objective_valu:17,block:[17,32],blog:[40,41],blomqvist:42,blue:43,bonn:57,book:[2,38],bool:17,boolparamet:20,bordeaux:57,borrow:2,both:[1,7,10,17,25,38,43,44,45,46,47,56,57],bought:48,bound:[8,9,10,12,17,22,28,29,31,44,45,46,47,56],bound_typ:8,bowl:53,box:51,bracket:3,branch:[2,43,46,47],branchcallback:7,breakdown:[17,46,48,57],breakpoint:[17,24],breaksxi:17,breakx:17,bressert:38,brewer:2,brian:38,bridg:7,brief:51,brn:5,bronco:[52,53],brown:[52,53],browser:[38,56],brussel:57,buccan:[52,53],budget:[5,42],budgetsp:42,buffalo:[52,53],bug:[1,2,39,42,43,46,47,48,53,54,56],bui:[5,42,48,49,51],build:[1,2,4,5,7,8,17,29,40,41,42,43,44,45,46,47,48,49,51,52,53,54,56],build_default_production_problem:49,build_diet_model:5,build_load_balancing_model:41,build_multiobj_paramset:[17,45],build_production_problem:49,build_sport:52,built:[3,15,16,17,18,25,27,53],bureau:5,busi:[3,17],bvar:17,c_i:40,c_rang:40,cal_prod:17,calcium:[5,51],calcul:[5,24,43,46,47,48,51,52,53,56],call:[3,7,12,17,20,22,25,27,28,29,35,42,46,48],callabl:28,callback:[22,35,50],calori:[5,51],campaign:[42,51],can:[0,3,5,8,9,11,12,13,15,16,17,18,20,22,23,24,27,29,32,35,37,38,39,40,44,45,49,51,52,54,57],candid:[20,57],cannot:[1,10,12,16,17,29,30,35,43,44,45,46,52,56],capabl:[0,37],capac:[49,57],capellini:49,car:42,carbohydr:[5,51],cardiac:46,cardiac_car:[44,45],cardin:[52,53],care:[1,46,57],care_1_emer_fri_18:47,care_1_emer_mon_02:47,care_1_emer_mon_18:47,care_1_emer_sat_02:47,care_1_emer_sat_12:47,care_1_emer_sun_02:47,care_1_emer_sun_12:47,care_1_emer_thu_18:47,care_1_emer_tue_18:47,care_1_emer_wed_12:47,care_1_emer_wed_18:47,carolina:[52,53],cartesian:17,case_sensit:27,cassio:42,categori:51,cathi:[44,45,46,47],caught:22,caus:[8,29,48,55],caution:17,cb_mixin:50,cecilia:[44,45,46,47],ceil:12,cell:[46,47],cent:5,center:42,certain:[5,20,28,35,43,46,47,49],certainli:42,certifi:0,chang:[1,2,4,9,11,16,17,20,22,24,32,37,42,43,46,47,48,53,56],change_var_lower_bound:17,change_var_upper_bound:17,channel:[37,38,42],channelvar:42,charact:[35,44,45],characterist:42,chardet:0,charg:37,charger:[52,53],chart:[47,48,57],check:[1,6,8,10,12,16,17,20,25,27,28,29,31,37,38,48,55,57],check_as_mip_start:29,check_bound:17,checker:17,chicago:[2,52,53],chicago_coffee_shop:51,chicken:5,chief:[52,53],child:11,chip:[5,51],chipublib:2,chocol:[5,51],choos:[5,38,51],chose:[22,57],chosen:[22,57],chri:[44,45,46,47],christian:42,cilumn:29,cincinnati:[52,53],cindi:[44,45,46,47],citi:[2,52,53],cityofchicago:2,cla:57,claim:2,classmethod:[4,8,18,29],clean:[38,43],clean_before_solv:[17,45],cleans:43,clear:[17,22,29,57],clear_constraint:17,clear_kpi:17,clear_lazy_constraint:17,clear_mip_start:17,clear_multi_object:17,clear_progress_listen:17,clear_so:17,clear_user_cut_constraint:17,clearli:[56,57],cleveland:[52,53],cliqu:46,clock:22,clock_arg:22,clone:[16,17,20,24,25],clone_kwarg:17,close:[2,5],closer:46,cloud:[1,2,37,38,40,41,42,43,46,47,48,51,52,53,56],cls:4,co2:17,co2_cost:56,co2_ub:56,coal1:56,coal2:56,coal:56,code:[1,2,3,7,11,17,20,22,28,31,35,42,43,46,47,48,53,55,56,57],coef:17,coef_fn:17,coeff:16,coeffici:[7,10,12,16,17,25,43,46],coffe:[2,51],collect:[2,3,4,5,17,22,41,43,44,45,46,52,53,56,57],color:57,colt:[52,53],column:[1,4,23,28,29,43,46,47,53,56],com:[0,1,2,9,28,29,39,41,42,43,46,47,48,51,53,54,56],combin:[3,17,48,57],come:[20,48],command:[13,37,38,51,53],comment:3,commit:51,common:[43,56],commun:[0,1,37,39,42,43,48,52,53,56],compact:[17,43,57],compani:[2,42,49,51,56],companion:17,compar:3,comparison:3,comparisontyp:[9,10,17],compat:[18,29],competit:[1,2,42,43,46,47,48,53,56],complet:[2,29,44,45,57],complete_var:17,complex:[1,2,42,43,46,47,48,53,56],compli:7,complic:46,compon:[29,57],compos:[0,2],comprehens:17,compress:17,compris:[19,53],comput:[1,8,9,15,16,17,20,22,24,27,29,38,39,42,43,44,45,48,56,57],concaten:17,concret:[17,22],condens:3,condit:[1,2,42,43,46,47,48,53,56],confer:38,confid:42,confidence1:42,confidence2:42,config:[11,35],configur:[11,37,56],conflict:[8,9],conflict_refin:50,conflictrefin:8,conflictrefinerresult:8,conflictrefinerresut:8,conflictstatu:[8,9],conform:35,connect:[7,24],consecut:[17,53,56],consequ:1,consid:[8,10,11,16,17,35,49],consist:[12,16,17,22,29,46,47,48,49,51,52,57],constant:[2,8,10,12,16,17,25,27,29,30,32,43,45,50],constantexpr:16,constr:[17,19,50],constrain:[46,48,49,56],constraint:[4,5,6,7,8,9,10,12,17,19,26,27,29,37,40,41,44,45,49],constraintcallbackmixin:7,constraintgroup:8,constraintpriorit:27,constraintsgroup:8,constrait:17,construct:[3,17,26,32,43],constructor:[7,18],consttraint:17,consult:[44,45,46],consum:49,consumpt:49,contact:[1,2,42,43,46,47,48,53,56],contain:[1,2,3,4,7,8,11,13,16,17,19,22,26,28,29,31,35,42,43,46,47,48,51,53,56],contains_var:[6,16,25],content:[1,2,5,35,42,43,46,47,48,53,56],context:[17,23,35,44,45,50],contextoverrid:11,contextu:32,continu:[1,2,3,12,17,19,26,31,40,42,43,46,47,48,53,56,57],continuous_var:[3,4,17,44,45],continuous_var_cub:17,continuous_var_dict:[3,4,12,17,44,45,49],continuous_var_list:[3,17,40],continuous_var_matrix:[3,17],continuous_vartyp:[5,17],continuousvartyp:[17,31],continuum:[37,38],contradictori:[8,46,47],contraint:17,contrast:49,contribut:[17,43],control:[1,2,9,11,17,22,23,29,42,43,46,47,48,53,56],contructor:18,conveni:[27,43,46,56,57],convers:[10,17,44,45,56,57],converst:7,convert:[7,12,17,20,29,30,35,43,44,45,46,48,57],cookbook:38,cooki:[5,51],coordin:[17,24],copi:[11,16,17,20,22,24,25,26,35],copy_paramet:26,copyright:[1,2,4,5,40,41,42,43,44,45,46,47,48,49,52,53,56],core:35,corp:[4,5,40,41,44,45,49,52],correct:35,correct_nb_games_:52,correspond:[3,8,17,28,29,35,43,46],cost:[1,4,5,12,17,29,41,42,44,45,47,48,49,51,54,57],cost_expr:4,could:[13,17,46,47,52,56,57],count:[5,17,25,43,46,57],counter:19,coupl:[17,43,44,45,46,48,57],cours:[38,43,46,57],cover:[20,46],cowboi:[52,53],cplex:[0,1,2,3,5,7,8,9,11,13,17,18,20,22,28,29,31,35,39,41,43,44,45,46,47,48,49,51,52,53,54,55,56],cplex_cloud:[8,29],cplex_config:11,cplex_config_:11,cplex_loc:[8,13,29],cplex_paramet:[11,17],cplex_studio1263:55,cplex_studio129:13,cplex_studio_dir1210:13,cplex_studio_dir128:13,cplex_studio_dir129:13,cplex_studio_dir20101:13,cplex_studio_dir201:13,cplex_studio_dirxxx:37,cplex_typecod:31,cplex_vers:20,cplexcloud:22,cplexscop:9,cpo:37,cpx_id:20,cpx_name:20,cpx_param_epgap:20,cpxapi:[9,28],cpxparam_mip_tolerances_mipgap:[46,47],cpxparam_read_datacheck:[43,46,47],cpxxgetstatstr:28,creat:[4,5,7,8,10,11,12,16,17,18,22,24,29,30,35,37,38,39,57],create_missing_nod:11,create_parameter_set:[17,45],creation:[3,46],criteria:[22,48,51],criterion:3,critic:[36,54],cross:1,crude1:48,crude2:48,crude3:48,crude:48,cst:16,csv:[11,23,34,35],csv_util:50,ct1:17,ct2:17,ct_arg:17,ct_assign_to_active_:41,ct_define_max_sleeping_:41,ct_demand_:49,ct_fill_:4,ct_name:41,ct_res_:49,ct_unique_server_:41,ctlh:4,ctname:[17,44,45],cts:[7,8,17,29,44,45],cts_by_nam:17,ctsens:[10,17],cube:17,curr:4,current:[1,2,11,17,20,22,35,38,41,42,43,46,47,48,53,56],current_nb_iter:22,current_nb_nod:22,current_object:22,current_progress_data:22,current_serv:41,currweek:52,custock_mast:4,custom:[1,2,3,7,17,22,23,43,46,47,48,49,51,53,56],cut:[4,7,17,29,43,46,47,51],cut_:4,cut_ct:17,cut_typ:[17,29],cut_var:4,cut_var_valu:4,cutstock:51,cutstock_generate_pattern:4,cutstock_model:4,cutstock_print_solut:4,cutstock_save_as_json:4,cutstock_solv:4,cutstock_solve_default:4,cutstock_update_du:4,d_w:52,dai:[44,45,46,47,48],daili:5,dalla:[52,53],damag:5,dantzig:[5,51],data:[3,4,5,7,17,22,23,29,34,35,38,39,40,41,44,45,49,51,52],databas:53,datafram:[23,29,34,35,42,43,46,47,48,53],datasci:[42,51],dataset:57,david:[38,44,45,46,47],day_index:[44,45],day_map:[44,45],day_to_day_week:[44,45],daynam:[44,45],deal:47,debbi:[44,45,46,47],debug:[35,36,51],debugg:38,decid:[48,51,57],decim:17,decis:[0,4,5,6,12,15,16,17,19,25,29,30,31,35,37,38,40,41,49,51,52,54],decisionkpi:[15,17,46],declar:[42,46,49],decor:7,decreas:[9,17,22,43],dedic:2,dee:[44,45,46,47],deep:[17,20],def:[4,5,7,17,23,40,41,44,45,49,52,57],defaul:22,default_environ:35,default_item:4,default_lb:31,default_loc:13,default_max_processes_per_serv:41,default_output_table_nam:23,default_pattern:4,default_pattern_item_fil:4,default_prior:27,default_roll_width:4,default_solution_storage_handl:35,default_ub:31,default_valu:20,default_work_rul:[44,45],defaulterrorhandl:14,defaut:[7,8],defect:54,defin:[7,8,9,10,11,15,17,21,22,23,24,29,31,44,45],definit:[8,17,46,47],deliv:[0,54,55],demand:[4,49,51],demand_max:[44,45],demand_min:[44,45],demonstr:[3,5,56],denot:[9,10,17],denver:[52,53],depart:[42,44,45,46,47,51],depend:[0,12,17,20,28,34,35,37,38,42,47,48,52,53,56],deploi:37,deploy:37,deprec:35,dept2:[44,45],dept:[44,45],deriv:[7,22,56],descend:22,describ:[3,9,17,20,28,51],describe_object:29,descript:[20,29,49],design:53,desir:[1,2,42,43,46,47,48,53,56],desk:5,destroi:17,det_tim:22,detail:[3,4,11,17,28,29,35,37,38,42,54,55],detailt:17,detect:[13,17,46,55],determin:[2,4,7,9,17,22,42,49,51,53,56],determinist:[22,28,46],deterministic_tim:28,detroit:[52,53],dettim:28,develop:[37,54],deviat:[29,46,47],df_assign:46,df_decision_var:43,df_energi:56,df_incompatible_assign:46,df_mine:43,df_preferred_assign:46,df_shift:46,df_unit:56,df_vacat:46,diagnos:[38,51],diagram:[46,48],dicitonari:[17,22,29],dict:[11,17,18,29,35,44,45],dictionai:[17,22],dictionari:[3,4,8,12,17,20,27,29,32,35,43,57],did:[5,17,43,46],diego:[52,53],diesel1:56,diesel2:56,diesel3:56,diesel4:56,diesel:[48,56],diet:51,dietary_fib:[5,51],differ:[8,9,16,17,20,22,29,35,41,43,44,45,46,47,48,51,52,53,56],digit:17,dimens:[17,48],dimension:[17,53],din:17,direct:[17,35,56],directli:[3,6,7,17,28,29,30],directori:[3,17,29,35,38],directparamet:20,disabl:[17,20],disc_royalti:43,discard:18,disclaim:2,discontinu:[17,24],discrep:9,discret:[9,12,16,17,29,31,32,53],discretenonzerovar:9,discretevar:[9,29],discuss:56,displai:[1,4,5,8,10,13,17,24,40,41,42,44,45,46,47,49,51,52,53,56],display_conflict:8,display_cost:57,display_pareto:57,display_stat:8,distanc:[1,51],distinguish:29,distribut:[37,38],divid:[16,24,43,46,56],divis:[16,43,51,52,53],division:52,do_rais:17,do_stop:40,doc:[0,17],docloudlogg:36,docplex:[0,3,4,5,37,40,41,42,44,45,49,51,52,55],docplex_:17,docplex_exampl:51,docplex_foo:18,docplex_model1:57,docplex_mymodel:17,docplexcloud:[8,29,35,36],docplexexcept:[12,16,18,24,27],docplexlogg:36,docplexquadraticarithexcept:16,document:[0,1,2,9,17,28,37,38,42,43,46,47,48,53,56],dod:35,doe:[16,17,20,23,24,29,35,38,41,43,46,51,56,57],dofeedback:[1,2,39,42,43,46,47,48,53,54,56],doing:[41,51],dollar:48,dolphin:[52,53],domain:17,don:[17,55],done:[11,17,46,57],doplex:17,dot:[4,17,46],dotf:17,doubl:34,doug:38,dow:46,down:[46,57],download:[37,38],draw:57,dsh:[44,45],dtype:[43,46],dual:[4,10,17,29],dual_valu:[4,10,17],due:[17,41,51,53],dummi:[17,29],dump:[4,35,40,41],dump_as_sav:17,duplic:17,durat:[44,45],dure:[22,28],dvar:[6,7,8,16,17,25,29,50],dvar_stat:17,dynam:[1,2,4,42,43,46,47,48,53,56],each:[1,2,3,4,7,8,15,16,17,22,27,28,29,31,35,40,41,42,44,45,47,48,49,51,52,56,57],eagl:[52,53],earl:42,earlier:[1,2,43],eas:3,easi:[37,38,46,57],easiest:38,easili:[0,2,39,43,46],edit:[0,1,4,37,38,39,42,43,46,48,52,53,56],editor:38,effect:[4,20,22,29],effici:[43,46,47,54],effort:9,effort_level:17,effortlevel:[9,17],egg:49,eight:53,eimag:17,either:[1,2,8,15,16,17,20,22,24,25,27,29,32,42,43,46,47,48,49,51,53,56],ekei:29,elaps:[17,22,46],eldar:42,electr:[51,56],element:[2,3,7,8,17,35,56,57],elementari:3,eli:38,elimin:[43,46,57],els:[3,4,5,6,12,15,16,17,18,20,22,25,26,27,29,40,41,44,45,49,52,57],emb:17,emerg:[44,45,46],emodel:15,empahsi:32,emphas:47,emphasi:[11,32,46],empti:[17,29,30],enabl:[14,17,42],encod:[4,17,34,40,41],encode_csv_str:34,encount:26,end:[4,8,17,28,34,41,43,44,45,47,52],end_day_index:[44,45],end_tim:[44,45,46],end_time_of_dai:[44,45],enforc:[1,43,46,56],engin:[11,17,35,38,39,48,51,53],engineloglevel:35,england:[52,53],enjoi:2,enlarg:4,ensur:[46,57],entri:[40,57],enumer:[4,8,9,10,12,14,17,21,22,27,29,30,32,40,44,45,57],env:[7,34,35,57],environ:[1,2,4,5,39,40,41,42,43,44,45,46,47,48,49,50,52,53,54,56,57],eobject:17,eps:40,eps_zero:17,eq_constraint:[3,17],eqct:17,equal:[1,3,10,12,16,17,20,35,43,44,45,46,47,48,56,57],equals_const:16,equival:[10,12,16,17,19,25,35],equivalence_constraint:17,equivalenceconstraint:[10,19],error:[7,11,14,17,18,29,35,36,55],error_handl:50,especi:17,essenti:38,estim:[56,57],etc:[4,43],eth:12,ethem:[44,45],euclidean:1,evalu:[9,11,15,17,24,29,47,57],even:[9,17,46],event:[1,2,22,42,43,46,47,48,53,56],everi:[46,47,48,52,53],everybodi:38,everyth:[1,2,17,42,43,46,47,48,53,56],everywher:17,evolut:56,exact:[17,29],exactli:[1,2,17,52,57],examin:[1,8],exampl:[0,3,5,7,9,10,12,14,16,17,18,20,22,23,25,26,27,28,29,32,37,39,40,41,42,44,45,52,55,57],exce:[48,52],exceed:28,excel:56,exceot:18,except:[7,10,11,12,13,14,15,16,17,18,20,22,24,28,29,32,35,42,43,48,57],exclud:[2,8,17],exe:[1,2,43,46,47,53,56],execut:[7,35,38],exist:[11,13,17,35,37,38],exit:32,expect:[1,7,17,29,36,42,44,45,46,48,56,57],expens:[9,41,51],experi:[37,38,39],expert:54,explicit:[3,17],explicitli:[15,17],export_as_json_str:29,export_as_lp:[3,17],export_as_lp_str:17,export_as_mp:17,export_as_mps_str:17,export_as_mst:29,export_as_sav:17,export_as_sav_str:17,export_as_savgz:17,export_as_sol:29,export_priority_order_fil:17,export_prm:20,export_prm_to_str:20,export_to_stream:17,expr1:[10,17],expr2:[10,17],expr:[4,6,10,16,17,25,46],express:[4,6,7,9,10,12,15,16,17,19,24,25,26,29,32,57],extens:[17,34,35],extra:[17,43,46,56,57],extract:38,extrem:[1,17,56],fabien:42,facil:36,fact:[17,43],factor:24,factori:[3,10,17,48,49],faculti:[0,37],fail:[4,10,12,17,28,40,42,48,56],fair:[44,45,47],fairli:[46,47],falcon:[52,53],fals:[2,5,6,8,10,11,12,16,17,18,20,23,25,26,27,29,31,40,44,45],famili:17,far:[46,48],fashion:17,fast:[1,2,42,43,46,47,48,53,56],faster:[3,12,17],fastli:17,fatal:14,favor:[42,56],feasibl:[3,9,17,28,29,43,46,56],featur:[17,35,38,39,43,46,56],februari:53,fetch:17,fettucin:49,few:[4,38,48,51],fewer:46,fiction:[2,42],field:[4,5,8,11,17,27,29,35,57],field_nam:34,figur:[46,56],file:[2,4,5,9,11,17,18,20,29,34,35,36,40,41,44,45,46,47,49,51,52],file_list:11,file_or_filenam:29,fileformat:29,filenam:[11,18,20,23,29,35],fill:[4,17],fill_tabl:4,filter:[11,17,22,29,35],filter_level:11,filterobject:11,filterobjectiveandbound:11,financi:[42,51],find:[1,9,17,22,37,43,44,45,46,47,51,56],find_matching_linear_constraint:17,find_matching_quadratic_constraint:17,find_matching_var:[5,17],find_re_matching_var:17,fine:35,finer:35,finest:35,finish:[1,2,17,22,35,42,43,46,47,48,53,56],first:[1,2,7,9,17,24,25,29,35,37,38,42,43,44,45,46,48,49,52,56,57],first_generation_du:4,first_half_week:52,firstnurs:[44,45],fix:[2,32,48,51,54,56,57],fixed_cost:[56,57],flag:[8,17,18,20,29],flat:43,float64:43,float_precis:[5,17],floor:12,flour:49,flow:46,fly:17,focu:42,folder:3,folium:2,follow:[1,2,3,4,10,11,13,17,28,35,37,38,42,43,46,47,48,51,53,56,57],fontsiz:57,foo:[17,18],food:[5,51],food_nam:5,food_nutri:5,forbid:[43,46],forc:[3,5,11,20,46,53],forced_stop:43,forecast:42,forget:17,form:[3,10,17,19,20,25,27,29],format:[2,4,5,7,8,17,18,19,20,22,29,35,40,41,44,45,48,49,52,53],format_spec:17,formid:0,formul:[1,2,42,43,46,47,48,53,56],forum:55,foster:2,found:[3,5,9,13,17,24,27,29,37,40,46,53,57],foundat:[1,2,42,43,46,47,48,53,56],four:[17,48],fourth:2,fraction:46,fragment:35,frame:[23,35,53],franca:42,francisco:[52,53],fraud:[1,2,42,43,46,47,48,53,56],free:[0,1,2,17,37,38,39,42,43,46,47,48,53,56],freenonbas:9,frequenc:22,fresh:17,fridai:[44,45,46],friendli:[0,39],from:[1,2,3,4,5,7,8,10,11,12,13,14,15,16,17,18,19,20,22,24,28,29,32,35,37,38,40,41,42,43,44,45,48,49,51,52,53,54,57],from_fil:29,from_var:8,fromon:8,ftype:5,full:[17,20,29,37],fulli:[29,35],func:[10,29],functionalkpi:15,functionalpriorit:27,functionalsolutionlisten:22,functor:27,fundament:48,futur:[0,1,2,42,43,46,47,48,51,53,56],gabrielli:42,gain:[1,2,42,43,46,47,48,53,54,56],game:52,gantt:47,gap:[3,17,22,28,40,46],gap_best_obj:40,gap_fmt:22,gas1:56,gas2:56,gas3:56,gas4:56,gas:[48,56],gasolin:[48,51],gather:[19,38],ge_constraint:17,gen_model:4,gener:[4,6,17,20,22,40,46,51,53,56,57],generate_nondefault_param:20,generate_param:20,geograph:2,geopi:2,georg:[5,42,51],geq:1,gerg:42,geriatr:[44,45,46],get:[0,1,2,4,6,7,10,12,16,17,20,22,25,28,29,30,35,42,43,46,47,48,53,55,56,57],get_available_core_count:35,get_blended_objective_value_by_prior:29,get_coef:16,get_constraint_by_index:17,get_constraint_by_nam:17,get_cplex:[17,45],get_cplex_modul:13,get_cpx_unsatisfied_ct:7,get_cut:[17,29],get_dual_valu:29,get_engine_log_level:35,get_environ:[4,5,35,40,41,44,45,49,52],get_input_stream:35,get_kei:12,get_left_expr:10,get_model:15,get_num_cut:[17,29],get_objective_expr:17,get_objective_valu:29,get_output_stream:[4,5,35,40,41,44,45,49,52],get_paramet:35,get_parameter_from_id:17,get_prior:27,get_quadratic_coeffici:25,get_quadratic_constraint_by_index:17,get_reduced_cost:29,get_relax:27,get_right_expr:10,get_sensit:29,get_slack:29,get_solve_statu:[3,17],get_statist:19,get_statu:[29,52],get_stop_callback:35,get_time_limit:17,get_valu:[4,7,29],get_value_df:29,get_value_dict:29,get_value_list:29,get_var_by_nam:17,getbas:9,getstatstr:28,gettempdir:[17,29],ghani:42,giant:[52,53],gift:42,github:[0,38,51],give:[24,43,46,47,51,54,56],given:[6,7,8,12,16,17,24,29,43,46,47,53,56,57],global:[2,3,18,37,38,44,45,46,56],gloria:[44,45,46,47],gmodel:4,goal:[1,2,5,17,42,43,47,48,53,56,57],goal_nam:17,gold:57,gomori:46,good:[38,43,53,56],gorelick:38,govern:17,grape:5,graphic:57,grater:57,greater:[1,2,17,42,43,46,47,48,53,56,57],green:[52,53],ground:17,group:[8,17,18,20,53,56],groupbi:[43,46,56],gru:38,guadalup:42,guarante:[20,35],gub:46,guid:[5,38],had:17,hakimi:42,half:[17,46,52],halv:[52,53],hand:[5,7],handl:[1,2,17,22,35,42,43,46,47,48,53,56],handler:[14,35],happen:[17,22,35,56,57],hard:38,has:[2,3,5,6,7,10,12,15,16,17,20,22,27,28,29,30,32,35,40,42,46,47,48,49,52,57],has_basi:[10,12,17],has_cplex:13,has_hit_limit:28,has_incumb:22,has_matplotlib:[13,57],has_multi_object:17,has_nam:6,has_numpi:13,has_object:[17,29],has_panda:13,has_quadratic_term:25,has_user_nam:6,hat:57,have:[1,2,7,9,10,11,12,15,16,17,22,24,26,29,32,37,38,42,43,44,45,46,47,48,53,55,56,57],header:17,health:5,held:[52,53],hellmann:38,help:[9,22,28,29,38,39,51,54],henderson:42,here:[1,2,3,17,23,29,35,37,38,39,40,42,43,44,45,46,47,48,53,54,55,56,57],heurist:5,hide_user_nam:17,hierarch:[3,20],hierarchi:[3,17,20],high:[21,27,38],high_overlapping_:[44,45],high_req_min_:[44,45],high_req_min_card_mon_08_10:47,high_req_min_card_mon_12_8:47,high_req_min_card_tue_08_4:47,high_req_min_card_tue_18_3:47,high_req_min_cons_fri_08_10:47,high_req_min_cons_fri_12_8:47,high_req_min_cons_mon_08_10:47,high_req_min_cons_mon_12_8:47,high_req_min_cons_thu_08_10:47,high_req_min_cons_thu_12_8:47,high_req_min_cons_tue_08_10:47,high_req_min_cons_tue_12_8:47,high_req_min_emer_fri_18_3:47,high_req_min_emer_mon_18_3:47,high_req_min_emer_sat_02_5:47,high_req_min_emer_sat_12_7:47,high_req_min_emer_sat_20_12:47,high_req_min_emer_sun_02_5:47,high_req_min_emer_sun_12_7:47,high_req_min_emer_thu_02_3:47,high_req_min_emer_thu_18_3:47,high_req_min_emer_tue_08_4:47,high_req_min_emer_tue_18_3:47,high_req_min_emer_wed_18_3:47,high_required_:[44,45],high_required_emergency_cardiac:47,higher:[1,8,17,37,46,49,56],highlight:2,hikmah:42,his:5,histogram:46,histori:35,hit:28,hold:[8,10,12,17,22,29],home:[17,29,37,38,39],horizon:43,horizont:24,hospit:[46,47],host:[37,38],hostnam:11,hotdog:[5,51],hour:[44,45,46,47],houston:[52,53],how:[4,5,8,17,29,41,49,51,52,55,57],howev:[5,17,44,45,46],html:[9,28,29,41,48,53],http:[0,2,4,5,9,28,29,40,41,44,45,49,51,52],huge:9,human:[20,38,41,51],hypercub:17,ibm:[0,1,2,4,5,9,28,29,38,39,40,41,43,44,45,46,47,48,49,51,52,53,55,56],ibmdecisionoptim:[0,37,38],ideal:[2,46],ident:[9,17,22,29,48],identifi:[8,17,42,46,47],idna:0,idri:38,idx:[3,17,57],if_ct:[10,17],if_then:17,iff:[44,45],ifthenconstraint:[10,17],ignor:[17,18,29,46],ignore_nam:[17,18],iinf:46,ij_0:17,ij_1:17,ij_2:17,illustr:[17,43,47,51,53],ilocplex_mipstarteffort:9,ilog:[0,1,2,9,28,29,39,42,43,46,47,48,52,56],immedi:1,impact:[43,57],implement:[2,3,22,27,35,43,46,51,53,56],implic:[1,57],import_solut:17,impos:[1,48],imposs:2,impriv:22,improv:[17,22,46],in_division_first_half_:52,in_us:56,in_use_coal1_1:56,in_use_coal1_2:56,in_use_coal1_3:56,in_use_coal1_4:56,in_use_coal1_5:56,incent:[52,53],includ:[1,2,8,10,11,17,20,34,35,37,38,42,43,46,47,48,51,53,56,57],include_infinity_bound:8,include_root:20,incompat:[44,45],incorrect:17,increas:[1,2,14,42,43,46,47,48,52,53,56],incumb:[7,22],incur:[56,57],indct:17,inde:[1,57],indent:[4,41],indent_level:20,independ:35,index:[3,6,7,17,29,39,43,44,45,46,48,50,52,53,56],index_to_var:7,indexableobject:[6,10,12,30],indianapoli:[52,53],indic:[1,2,7,8,10,15,17,19,22,28,29,42,46,52,55],indicator_constraint:17,indicatorconstraint:[10,19],industri:[48,51],inequ:[43,57],inf:9,infeas:[8,10,17,27,44,45,47],infeasibilti:17,infi:8,infin:[12,17],info:[14,35,36],infolevel:14,inform:[3,8,9,10,12,13,17,18,20,22,29,37,38,43,47,53,56,57],inherit:7,init_numpi:17,initi:[0,4,5,17,26,32,37,40,41,43,44,45,46,48,49,52],initial_context:11,initial_multipli:40,inlin:57,inner:56,inplac:7,input:[1,17,35,46,47,48,49,56],ins_var:49,insid:[20,27,35,49,51],inside_:49,inside_var:49,insight:[1,2,42,43,46,47,48,53,56],inspect:37,inspir:40,instal:[0,1,2,13,17,22,35,42,43,46,47,48,51,53,56,57],instanc:[4,7,8,12,15,16,17,18,19,20,22,24,25,27,28,29,31,32,35,37,38,47,51,52,57],instanci:11,instanti:[6,7,10,12,16,20,24,25,28,29,30,31],instead:[9,11,17,35,45,46],institut:[0,37],integ:[1,2,3,6,7,9,10,12,16,17,19,20,22,28,29,30,31,35,42,43,44,45,46,47,48,53,56,57],integer_var:[3,17,41],integer_var_cub:17,integer_var_dict:[3,17,29],integer_var_list:[3,4,17],integer_var_matrix:[3,17],integer_vartyp:[5,17],integervartyp:[17,31],intend:[6,7,16,24,25,32],interact:[3,8,35,38],interb:17,interdivision:[52,53],interest:[13,22,29,57],interfac:[8,27,36],intermedi:[7,22],intern:[22,35,44,45],interpret:[17,29,35,38],intersect:56,interv:[17,24,35,56],intparamet:20,intradivis:[51,52],intradivision:52,introduc:[1,38],introduct:38,invalid:[9,10,29,35,46,55],invalidsettingsfileerror:11,inventori:48,invok:[13,24,43,46],involv:[8,38],ipla:[1,2,42,43,46,47,48,53,56],ipython:38,iron:[5,51],irving_76:2,is_64bit:13,is_ad:10,is_binari:12,is_const:16,is_continu:12,is_debug_mod:35,is_decision_express:15,is_default:20,is_discret:[12,16,31],is_division:52,is_dod:35,is_empti:29,is_feasible_solut:29,is_gener:6,is_integ:12,is_maxim:[9,17],is_minim:[9,17],is_nondefault:20,is_optim:17,is_quad_expr:[6,25],is_relax:27,is_root:20,is_separ:25,is_valid_solut:29,isabel:[44,45,46,47],isact:41,isinst:[44,45],issu:[53,54,55,57],it_row:4,itcnt:[22,46],item1:4,item2:4,item:[4,7,11,17,26,53],item_fill_ct:4,item_id:4,item_s:4,item_t:4,item_usag:4,items_by_id:4,iter:[4,7,8,10,12,16,17,20,22,25,27,28,29,30,40,43,46,52,56],iter_binary_constraint:17,iter_binary_var:17,iter_conflict:8,iter_constraint:[12,17],iter_continuous_var:17,iter_equivalence_constraint:17,iter_indicator_constraint:17,iter_integer_var:17,iter_kpi:17,iter_lazy_constraint:17,iter_linear_constraint:17,iter_mip_start:17,iter_param:20,iter_progress_listen:17,iter_pwl_constraint:17,iter_pwl_funct:17,iter_quad_triplet:25,iter_quadratic_constraint:17,iter_range_constraint:17,iter_record:22,iter_relax:27,iter_semicontinuous_var:17,iter_semiinteger_var:17,iter_so:17,iter_solut:22,iter_sos1:17,iter_sos2:17,iter_term:[16,25],iter_user_cut_constraint:17,iter_var_valu:29,iter_vari:[10,16,17,29,30],itertupl:43,itervalu:57,ither:17,its:[1,2,8,9,10,12,16,17,18,20,22,29,35,39,42,43,44,45,46,47,48,49,51,52,56,57],itself:[3,16,17],ivan:38,ivar:17,jack:5,jacksonvil:[52,53],jaguar:[52,53],jameel:42,jane:[44,45,46,47],janel:[44,45,46,47],janic:[44,45,46,47],januari:[0,4,5,40,41,44,45,49,52],java:36,jemma:[44,45,46,47],jet:[52,53],joan:[44,45,46,47],job:35,jobsolvestatu:17,joel:38,join:[37,43,46,56],jointli:7,jone:38,joyc:[44,45,46,47],json:[2,4,5,11,29,35,40,41,44,45,49,52],json_fil:[4,41],jude:[44,45,46,47],juli:[44,45,46,47],juliet:[44,45,46,47],jupyt:51,just:[2,17,36,46,48],justifi:40,kansa:[52,53],kate:[44,45,46,47],keep:[2,9,17,29,32,38,41,43,44,45,51,57],keep_zero:29,kei:[4,11,12,15,17,20,27,29,35,40,43,44,45,52,56,57],kept:[20,35],key_column_nam:29,key_format:17,keys1:[17,52],keys2:[17,52],keys3:17,keyword:[11,17,18,26],kind:22,kluski:49,knapsack:51,know:[2,56,57],knowledg:[1,2,42,43,46,47,48,53,56],knowledgecent:[9,28,29],known:[5,17,27,51],kpi:[5,11,17,22,29,34,41,45,47,48,50,51,56],kpi_arg:17,kpi_by_nam:[17,29,41],kpi_dict:22,kpi_filt:17,kpi_format:[17,22],kpi_op:15,kpi_report:11,kpi_value_by_nam:[17,29],kpifilterlevel:11,kpilisten:22,kpiprint:22,kpis_as_dict:17,kpis_output:11,kpis_output_field_nam:11,kpis_output_field_valu:11,kwarg:[4,5,8,11,17,18,24,26,27,29,35,40,41,44,45,49,52],label:[53,57],lack:55,laderman:5,lagrangian:[40,51],lagrangian_relax:51,lambda:[4,5,17,46,49,52],languag:48,larg:[3,5,10,12,17,37,46],larger:3,last:[17,24,47,53],lastslop:17,late:[51,52,53],later:[0,37,57],latest:[0,17,22,37],latitud:2,latter:15,lazi:[7,17],lazy_ct:17,lazyconstraintcallback:7,lb_report:41,lb_save_solution_as_json:41,lbm:41,lbs:[17,41],lct:[17,29],lct_stat:17,le_constraint:[3,17],lead:[17,35],leagu:[51,52],learn:[1,2,35,37,38,42,43,46,47,48,53,56],least:[10,17,22,25,26,29,47],lee:42,left:[1,7,10,17,29,43,46],left_expr:10,legaci:7,legend:57,len:[4,5,17,29,40,41,44,45,52,57],length:[17,24,35,43,51,53,57],less:[4,17,43,44,45,57],lesser:47,let:[1,2,17,35,42,43,46,47,48,56,57],letter:[7,31],level:[1,2,9,11,14,17,29,35,36,42,43,47,51,53,56],leverag:[43,57],lex_mipgap:17,lex_timelimit:17,lexicograph:[17,57],lhs:[4,7,10,17],librari:[0,13,28,39,51,55],library_27:2,library_77:2,licens:[0,1,2,4,5,39,40,41,42,43,44,45,46,47,48,49,52,53,56],life:48,lifespan:38,lift:46,lightblu:57,like:[2,8,17,27,29,35,42,57],limit:[0,1,2,3,5,17,28,35,37,42,44,45,46,47,48,52,53,56,57],line:[13,17],linear:[1,2,3,5,7,9,10,12,15,17,19,24,25,26,28,29,42,43,46,47,48,50,51,52,53,56,57],linear_constraint:17,linear_constraint_basis_status:17,linear_ct:[7,10,17],linear_ct_cplex:7,linear_ct_to_cplex:7,linear_expr:[16,17],linear_express:16,linear_part:25,linear_relax:26,linearconstraint:[1,10,17,47],linearexpr:[16,17,25],linearli:24,linearoperand:[10,12,16],linearrelax:26,link:[2,4,10,17],linux:[13,37,38],lion:[52,53],list:[3,4,7,8,9,11,12,17,22,24,29,30,35,37,38,46,47,48,49,52,53,56,57],listen:[17,22],littl:[43,46,57],load:[41,51],load_balanc:51,load_data:[44,45],loan:42,loc:57,local:[1,2,8,11,13,17,18,22,29,35,37,43,46,47,51,53,55,56],localenviron:35,locat:[1,3,13,20,43,46,47,51,53,55,56,57],log:[11,17,22,35,36,43,46],log_output:[5,11,17,41,44,45,46],logger:[11,13,36],loggertodocloud:36,loggertofil:36,logging_util:50,logic:[1,9,10,17,20,26,27,46,53,57],logical_and:17,logical_not:17,logical_or:17,logicalconstraint:10,logxor:17,lombardo:42,london:57,longer:9,longitud:2,longoria:42,look:[2,7,9,17,27,29,37,38,41,43,51,53],loop:[4,27,40],loop_count:[4,40],lopez:42,loui:[52,53],low:21,lower:[8,9,10,12,17,29,31,44,45,56],lower_bound:29,lowercas:[9,20],lowfat:[5,51],loyalti:[1,2,42,43,46,47,48,53,56],lp_line_length:17,lp_string:17,lubanov:38,lutz:38,mac:38,machin:[0,35,37],macro:28,made:[3,22,42,43,45,48,51,57],magin:48,mai:[8,10,12,17,22,24,28,35,38,42,48,56],mailhot:42,main:17,make:[1,2,4,9,17,25,43,46,47,48,53,56,57],make_attachment_nam:35,make_complete_solut:7,make_custstock_master_model:4,make_cutstock_pattern_generation_model:4,make_default_context:[11,17],make_default_load_balancing_model:41,make_solut:7,make_solution_from_var:7,make_solution_from_watch:7,maketran:35,manag:[1,2,4,17,42,43,46,47,48,53,54,56],mandatori:[10,21,27],mani:[2,3,8,24,37,46,56,57],manipul:3,manner:[3,17,22,51,52,57],manual:[3,20,39],manufactur:[48,49],map:[2,4,17,35,36,44,45],mappingpriorit:27,maptplotlib:[1,57],mark:[5,17,38,52],marker:[2,57],market:[42,51],marketing_campaign:[42,51],marrero:42,martelli:38,martin:42,martinez:42,master:4,master_model:4,match:[17,27,29,38,42,46,51,52,56],match_cas:[17,29],matchnamepriorit:27,matcht:38,materi:[1,2,42,43,46,47,48,51,53,56],math:[17,38,51],mathemat:[1,2,11,37,38,42,43,46,47,48,53,56],matheu:42,matplotlib:[13,24,43,46,48,51,56,57],matrix:[3,17,28,48],max:[4,9,17,20,32,40,41,43],max_cut:4,max_extract:43,max_gen:56,max_it:40,max_pattern_id:4,max_process_per_serv:41,max_req:46,max_requir:[44,45],max_sleeping_process:41,max_sleeping_workload:41,max_teams_in_divis:52,max_time_:[44,45],max_valu:20,max_work_tim:[44,45],maxim:[5,9,17,40,42,43,51,52,53,57],maximize_static_lex:17,maximum:[17,20,29,35,42,43,44,45,46,56,57],mckinnei:38,mdl2:32,mdl:[3,5,8,17,26,27,29,32,40,41,44,45,49,52],mdl_:5,mean:[9,17,24,29,43,46,53],mean_objective_valu:29,meant:[10,20,30,31],measur:47,media:39,median:[2,29,51],medium:[21,27],medium_ct_nurse_assoc_:[44,45],medium_ct_nurse_incompat_:[44,45],medium_req_max_:[44,45],medium_vacations_:[44,45],meet:[1,2,38,42,43,46,47,48,49,52,53,56],meister:42,melo:42,member:[0,5,8,11,17,37,46],memori:11,mention:[7,10,16,17,29],merg:[20,46,56],mesg:11,messag:[7,14,17,18,22,26,28,36,55],method:[3,5,7,8,10,12,13,15,16,17,18,19,20,22,23,24,25,26,27,28,29,30,35,41,43,44,45,46,47,48,56,57],metric:[9,17],mfunction:32,miami:[52,53],micha:38,michel:42,michigan:38,microsoft:38,midnight:46,might:[17,22,29,35,38,55,57],migrat:41,milk:[5,51],milp:[1,2,17,42,43,46,47,53,56],min:[9,17,20,32,35,43],min_demand:46,min_downtim:56,min_gen:56,min_req:46,min_requir:[44,45],min_uptim:56,min_valu:20,mine1:43,mine2:43,mine3:43,mine4:43,mine:51,minim:[1,2,4,5,8,9,17,41,44,45,48,49,51,56,57],minimize_static_lex:[17,41,45],minimum:[17,20,22,29,48,57],mininf:9,mining_panda:[43,51],minnesota:[52,53],minquad:9,minsum:9,minu:[16,17,48,53,56],minut:38,minxxx:9,mip:[3,9,11,17,20,22,26,28,29,32,43,45,46,53],mip_cut:20,mip_gap:[3,22],mip_relative_gap:28,mip_start:17,mip_start_sol:17,mip_time_limit_feas:28,mipgap:[17,20,32,45],mipinfocallback:22,miprelgap:28,miqcp:17,miqp:17,miramont:42,miranda:42,miroslav:42,miss:48,mission:54,mitig:[1,2,42,43,46,47,48,53,56],mix:[46,47],mixin:[7,23],mode:[27,35,46],model:[0,4,5,6,7,8,9,10,11,12,15,16,18,19,20,22,24,25,26,27,28,29,30,31,32,35,37,40,41,44,45,49,50,51,52,54],model_class:18,model_nam:18,model_object:32,model_paramet:32,model_read:50,model_solvefix:32,model_stat:[17,50],modelcallbackmixin:7,modelingobjectbas:[6,24],modelobject:6,modelpass:18,modelread:18,modelreadererror:18,modelstatist:[17,19],modern:38,modif:4,modifi:[2,7,16,17,22,24,32,42,43,46],modul:[0,39,43,50,53,57],moment:22,mondai:[44,45,46],monei:5,monitor:22,monomialexpr:16,month:53,more:[2,3,4,5,8,17,20,35,37,39,42,43,44,45,46,51,53,54,57],mortgag:42,most:[2,9,38,42,46,48,56,57],move:[4,24],mps:18,msg:[18,35,36],msol:3,mst:[9,17,29],mst_path:17,much:[3,4,12,35,43,46,48,49,51,52],multi:[17,28,29,45,51],multi_objective_valu:[17,29],multiobject:[41,45,51],multipl:[11,16,17,35,48],multipli:[16,17,24,40,43],muravyov:42,must:[1,2,3,4,7,10,12,15,17,18,22,24,29,30,31,35,37,38,43,46,47,48,52,56,57],mutual:8,my_:29,my_model:17,my_prob_mipstart:29,mybranch:7,mycallback:7,myinput:3,mymodel:[17,18],myprob1:29,n_iter:28,n_nodes_process:28,name:[2,3,4,5,6,8,9,10,11,12,15,17,18,20,22,23,24,27,29,30,31,32,34,35,40,41,42,43,44,45,46,47,48,49,52,53,57],name_kei:29,namedpriorit:27,namedtupl:[4,5,8,23,41,44,45,52,53,57],namespac:0,nan:[17,28],nanci:[44,45,46,47],nathali:[44,45,46,47],nation:5,nativ:11,natur:[17,28,44,45],nb_first_half_gam:52,nb_inter_division:52,nb_intra_division:52,nb_iter:28,nb_linear_nonzero:28,nb_nodes_process:28,nb_plai:52,nb_product:5,nb_re:57,nb_shift:[44,45],nb_store:57,nb_teams_in_divis:52,nb_week:52,nbs:52,ncol:57,ncolumn:28,ndicat:17,necessari:[52,57],necessarili:22,need:[0,1,2,17,22,37,38,39,42,43,46,47,48,51,52,53,54,55,56,57],neg:[8,17,46],negat:[10,16,17],negated_eqct:10,neither:17,neo:5,nest:57,net:[43,48],never:[28,31,44,45],new_dual:4,new_env:35,new_nam:17,new_pattern:4,new_pattern_cut_var:4,new_pattern_id:4,new_solut:17,new_valu:20,newli:[17,18],newslett:42,next:[1,2,4,42,43,46,47,48,53,56,57],nfc:53,nicol:[44,45,46,47],nine:5,no_solut:29,node:[17,20,22,28,42,43,46,47],non:[8,9,10,12,17,20,26,28,29,47,52,56],nonbas:9,none:[3,4,6,7,8,10,11,12,13,15,17,18,20,22,23,24,25,26,27,28,29,30,32,35,40,44,45,57],nonetheless:46,nonzero:[12,16,28,46],nonzerodiscretevar:[17,29],nonzerovar:[9,29],nor:17,normal:[17,28],notabasisstatu:9,notavailableerror:35,note:[2,10,12,16,17,22,24,26,28,29,30,35,41,43,46,48,57],notebook:[1,2,37,38,42,43,46,47,48,51,53,56],notequalconstraint:10,noth:[17,22,23,29,35],notic:48,notifi:4,notify_end:22,notify_progress:22,notify_solut:22,notify_start:22,now:[1,42,43,46,47,48,53,56,57],npr:[44,45],nqueen:3,nstore:57,number:[1,3,5,6,7,8,9,10,12,16,17,19,20,22,24,25,27,28,29,35,41,42,44,45,46,47,48,49,51,52,57],number_of_active_serv:41,number_of_binary_vari:[17,19],number_of_c:40,number_of_conflict:8,number_of_constraint:17,number_of_continuous_vari:[17,19],number_of_eq_constraint:19,number_of_equivalence_constraint:[17,19],number_of_ge_constraint:19,number_of_indicator_constraint:[17,19],number_of_integer_vari:[17,19],number_of_lazy_constraint:17,number_of_le_constraint:19,number_of_linear_constraint:[17,19],number_of_migr:41,number_of_mip_start:17,number_of_overlap:[44,45],number_of_param:20,number_of_progress_listen:17,number_of_pwl_constraint:17,number_of_quadratic_constraint:[17,19],number_of_quadratic_term:25,number_of_range_constraint:[17,19],number_of_relax:27,number_of_semicontinuous_vari:[17,19],number_of_semiinteger_vari:[17,19],number_of_so:17,number_of_solut:22,number_of_sos1:17,number_of_sos2:17,number_of_subgroup:20,number_of_user_constraint:17,number_of_user_cut_constraint:17,number_of_var_valu:29,number_of_vari:[6,16,17,19],numer:[3,7,8,9,15,17,20,22,27,57],numparamet:20,numpi:[0,13,17,38,39,48,53],nurs:[45,51],nurse1:[44,45,46],nurse2:[44,45,46],nurse_assign:[44,45],nurse_assignment_var:[44,45],nurse_associ:[44,45],nurse_associations_:[44,45],nurse_cost:[44,45],nurse_id1:[44,45],nurse_id2:[44,45],nurse_imcompatibilities_:[44,45],nurse_incompat:[44,45],nurse_over_average_time_var:[44,45],nurse_row:[44,45],nurse_skil:[44,45],nurse_under_average_time_var:[44,45],nurse_work_tim:[44,45],nurse_work_time_var:[44,45],nurseassign:[44,45],nurseassoci:[46,47],nurseincompat:[46,47],nurseoveraverageworktim:[44,45],nurses_:[44,45],nurses_by_id:[44,45],nurses_panda:[47,51],nurses_schedul:51,nurseskil:[46,47],nurseunderaverageworktim:[44,45],nursevac:[46,47],nurseworktim:[44,45],nurseworktime_betsi:47,nurseworktime_cathi:47,nurseworktime_cindi:47,nurseworktime_de:47,nurseworktime_debbi:47,nurseworktime_isabel:47,nurseworktime_jan:47,nurseworktime_janel:47,nurseworktime_janic:47,nurseworktime_jemma:47,nurseworktime_joan:47,nurseworktime_jud:47,nurseworktime_juli:47,nurseworktime_k:47,nurseworktime_patrick:47,nurseworktime_suzann:47,nurseworktime_vicki:47,nurseworktime_wendi:47,nutrient:5,nutrit:[5,51],nutshel:38,o_j:1,oaa:35,oakland:[52,53],obj:[4,29,40,49,57],obj_ep:4,obj_fmt:22,object:[3,4,6,7,8,9,10,11,12,13,15,17,18,19,20,22,23,25,26,27,28,29,31,32,35,36,40,41,44,45,49,51,52],objective_coef:17,objective_expr:17,objective_kei:17,objective_sens:17,objective_valu:[4,17,29,40,44,45,49,57],objectivesens:[9,17,32,45],objectivevalu:40,objnam:17,obtain:[11,17,28,48,56,57],occur:[11,17,18,22,28,29,53],odel:18,odm:[9,28,29],off:[1,2,17,35,42,43,46,47,48,53],offer:[51,54],offerig:42,offici:2,offset:48,often:[20,24],ohira:42,oil:51,oil_blend:[48,51],onc:[17,35,52],oncolog:[44,45],one:[1,2,3,4,7,9,10,11,17,20,22,23,25,26,28,29,31,35,38,42,43,44,45,46,47,51,52,53,56,57],ones:17,onli:[1,3,5,11,12,16,17,20,22,29,35,42,44,45,46,52,56,57],onlin:[38,54],opeing:57,open:[35,53,54,57],open_0_0:43,open_0_1:43,open_0_2:43,open_0_3:43,open_0_4:43,open_1_0:43,open_1_1:43,open_1_2:43,open_1_3:43,open_1_4:43,open_var:[43,57],opening1:57,opening2:57,opening_cost_obj:57,opening_first:57,opening_first_then_suppli:57,oper:[1,2,3,8,9,10,11,12,16,17,18,20,25,27,29,35,38,42,46,47,48,51,53,56,57],operand:[6,10,12,16,17],operating_max_gen:56,operator_symbol:9,oppon:[52,53],opportun:[1,2,42,43,46,47,48,53,56],opposit:[10,16,17,56],optim:[0,3,5,8,9,17,28,35,38,40,41,44,45,51,52,54,55,57],optimalitytarget:20,optimum:5,optin:17,optinf:9,option:[3,7,15,17,18,29,32,35,38,48],optquad:9,optsum:9,optxxx:9,ord:17,ord_path:17,order:[7,9,12,13,14,17,22,24,29,30,35,37,43,46,48,51,52,56],ordereddict:1,ordin:35,ore:[17,43],ore_0_0:43,ore_0_1:43,ore_0_2:43,ore_0_3:43,ore_0_4:43,ore_1_0:43,ore_1_1:43,ore_1_2:43,ore_1_3:43,ore_1_4:43,ore_qu:43,org:[0,2,4,5,38,40,41,44,45,49,52],organ:[1,2,37,42,43,46,47,48,53,56],origin:[2,6,8,17,29,35,46,56],orlean:[52,53],osx:37,oth:17,other:[1,8,12,13,16,17,20,27,29,35,43,44,45,46,47,49,51,52,53,57],other_shift:[44,45],otherwis:[7,10,12,13,15,17,18,20,22,23,27],our:[1,2,43,46,53,57],out:[11,20,29],out_var:49,outcom:[1,2,42,43,46,47,48,53,56],output:[3,4,5,10,11,14,17,20,23,29,35,44,45,46,47,49,52,53,57],output_custom:23,output_level:[14,18],output_nam:23,output_process:8,output_propert_nam:23,output_property_nam:23,output_table_custom:23,output_table_property_nam:23,output_table_using_df:23,outsid:[49,51,53],outside_:49,outside_var:49,over:[1,3,5,10,16,17,20,25,29,30,38,43,44,45,46,53,56,57],over_averag:[44,45],over_work:46,overal:[45,46,47,49,56],overlap:[44,45],overload:[3,12,16,20,29,57],overload_param:20,overrid:[11,27,32,35,37],overridden:32,overrideenviron:35,overview:[38,39,51],overwrit:[11,17],own:[2,20,22,35,49],p_austin:2,p_back:2,p_coal1_1:56,p_coal1_2:56,p_coal1_3:56,p_coal1_4:56,p_coal1_5:56,p_manning_13:2,p_sulzer:2,p_var:40,p_woodson:2,packag:[1,2,13,22,24,35,36,42,43,46,47,48,53,54,55,56],packer:[52,53],page:[39,50],pai:[42,43,46,47,48,56],pair:[9,16,17,25,27,29,35,43,44,45,46,47,52,57],palermo:42,panda:[1,2,13,17,23,29,35,38,39,42,43,48,51,53],panther:[52,53],parallel:[43,46,47],param:[6,11,16,17,25,32,44,45,50],param_kei:20,paramet:[1,2,7,8,9,11,12,16,17,18,22,24,25,26,27,29,32,34,35,42,43,44,45,46,47,48,50,53,56,57],parameter_cpx_id:17,parameter_set:[17,45],parametergroup:[17,20],parameterset:17,paramset:45,parent:[6,20,31],parent_group:20,pareto:56,pari:57,park:2,pars:2,part:[1,2,4,7,8,9,10,12,16,25,35,38,42,43,44,45,46,47,48,53,56],parti:0,partial:[4,17,26],particular:[42,47],particularli:[29,46,47],pass:[12,15,16,17,18,20,22,24,25,26,28,29,35,43,57],past:[1,2,42,43,46,47,48,53,56],pat:4,path:[1,2,13,17,18,29,42,43,46,47,48,51,53,56],pathnam:17,patricia:[44,45,46,47],patrick:[44,45,46,47],patriot:[52,53],pattern1:4,pattern2:4,pattern:[4,5,17,51],pattern_detail:4,pattern_item_fil:4,pattern_row:4,pattern_t:4,patterns_by_id:4,pay_rat:[44,45,46,47],pediatr:[44,45],penalized_viol:40,penalti:40,pension:42,per:[4,5,8,35,40,42,44,45,48,53,57],percentag:28,perform:[4,15,17,29,38,43,46,56],performw:4,permit:16,person:[46,47],perspect:46,phase:[9,28,57],philadelphia:[52,53],phone:54,pick:17,pie:[48,57],piecewis:[10,17,24,26,29],piecewise_as_slop:[17,24],pip:[0,55,57],pittsburgh:[52,53],pivot:[43,46,56],place:[17,29,53],plai:[51,52],plain:[17,29],plan:[2,42,43,46,47,51,56],plane:[1,57],planner:51,platform:[13,35,37,38],play_:52,plays_exactly_once_:52,pleas:[1,2,39,42,43,46,47,48,53,54,56],plinux:38,plot:[24,48,56,57],plt:57,plu:[16,46,47,48],point:[1,3,7,8,16,17,20,22,24,28,51,53,56,57],polici:3,pool:[17,29],poolobject:29,pop:[4,5,45],popul:[17,29,51],popular:38,populate_solution_pool:17,portabl:57,pose:[5,51],posit:[1,7,10,12,17,30,35,43,46,53,57],positiveintparamet:20,possibl:[3,7,9,10,12,14,17,20,22,23,26,28,29,31,35,43,46,47,48,51,52,53,56],possible_memb:8,post:[41,43,44,45,56],postopencloseconstraint:43,postpon:52,postslop:[17,24],power:[0,39,51,56],ppc64le:[37,38],practic:[5,38,46],pre:17,precis:[8,12,17,27,29,57],precomput:46,predefin:[17,27],predic:[17,29],predict:[1,2,42,43,46,47,48,53,56],pref:8,prefer:[8,9,18,52,53],preliminari:46,premis:35,prepar:[41,44,45],present:[1,2,3,6,13,16,17,20,22,25,29,35,43,46,47,48,53,55,56,57],preslop:[17,24],presolv:[43,46,47],prevent:[1,2,10,22,42,43,46,47,48,53,56,57],previou:[17,32,42,43,46,56],previous:[17,46],price:[43,48,56],principl:38,print:[3,4,5,7,11,14,17,18,19,20,22,26,29,36,40,41,44,45,46,49,52,57],print_cal:7,print_default:20,print_info_to_str:20,print_info_to_stream:20,print_inform:[3,5,8,13,17,19,40,44,45,49,52,57],print_mst:29,print_production_solut:49,print_solut:[17,44,45],print_sports_solut:52,print_zero:17,priogress:22,priorit:27,prioriti:[10,17,27,29,50],priority_for_non_match:27,priority_for_unnam:27,priority_map:27,prm:[18,20],proactiv:[1,2,42,43,46,47,48,53,56],prob:29,probabl:42,probe:46,problem:[0,3,4,5,9,10,12,17,28,29,32,35,38,39,40,41,44,45,49,51,52],problem_typ:[17,28],process:[1,2,4,17,28,35,41,42,43,46,47,48,53,54,56],prod:49,produc:[8,17,29,43,48,49,51],product1:42,product2:42,product:[17,25,37,42,43,46,51],product_nam:49,profession:[0,37],profit:[17,40,42,43,51],program:[1,2,4,5,8,11,29,35,37,38,42,43,44,45,46,47,48,49,52,53,56],programm:5,progress:[11,17,50],progress_data:22,progressclock:22,progressdata:22,progressdatarecord:22,progresslisten:[17,22],project:[38,46],promot:[28,42],prompt:51,prompt_msg:7,propag:4,proper:[17,20],properti:[4,6,7,8,10,12,16,17,19,20,22,25,27,28,29,30,31,35,44,45,46,56],proport:[48,51],propos:5,protein:[5,51],protocol:22,proven:28,provid:[2,4,5,8,13,17,22,24,35,36,37,38,40,41,43,44,45,46,47,48,49,52,55,56,57],pts:57,publish:[8,11,17,22,27,35,50],publish_nam:[5,17],publish_solve_detail:35,publishresultasdf:[8,23,27],purchas:48,pure:[17,29],purpos:[17,29,46,53],push:56,put:[1,35],pwl:[17,50],pwl_def:24,pwl_expr:10,pwl_func:10,pwlconstraint:10,pwlfunction:24,pycharm:38,pydev:38,pypi:[0,37,38],pyplot:57,python373:[1,2,43,46,47,53,56],python3:57,python:[0,1,2,3,7,12,13,17,35,36,37,43,46,47,48,51,53,54,55,56,57],python_vers:13,pythonpath:[11,17,46,47],q_0:17,q_1:17,q_3:17,qcp:17,qcs:17,qexpr1:10,qexpr2:10,qmax:5,qmin:5,qty:5,qty_var:5,quad:[17,50],quad_expr:[17,25],quadexpr:[17,25],quadrat:[6,10,17,19,25,29],quadratic_dual_slack:17,quadraticconstraint:[10,19],quadray:17,qualif:[44,45,46,47],qualifi:[20,32,37],qualified_nam:[20,32],qualiti:[9,17,48,51],quality_metr:17,qualitymetr:9,quand:9,quantifi:46,quantiti:[9,48],queri:[9,16,17],question:[51,55,56],quickli:[0,39],quit:2,quot:34,quotient:16,qvs:5,raider:[52,53],rais:[7,10,11,12,15,16,17,18,20,24,27,29,32,35,48,55,57],raisin:5,ram:[52,53],ramp_down:56,ramp_up:56,rang:[3,4,5,10,17,19,20,40,44,45,52,57],range_constraint:17,range_min:43,range_year:43,rangeconstraint:[10,17,19],rank:3,rate:[35,42,46,47,48],raven:[52,53],raw:[5,46,48,56],rc_cost:4,rc_ep:4,read:[2,11,17,18,29,35,39,46,47],read_basis_fil:17,read_csv:35,read_df:35,read_mip_start:17,read_model:18,read_msgpack:35,read_priority_order_fil:17,read_prm:18,read_set:[11,17],readabl:[19,20,53],reader:[2,35],readi:46,readm:[37,38],real:[5,22,43,46,47,48,56],realli:22,reason:[17,41,51,57],rebalanc:[41,51],rebuilt:40,receiv:[22,53],recent:5,recogn:11,recommend:[1,2,42,43,46,47,48,53,56],recomput:46,record:[22,35,36,52],record_history_field:35,record_history_s:35,record_interv:35,recurs:20,red:[2,5],redefin:[22,29,44,45,57],redefinit:[12,15,16,22],redefint:22,redskin:[52,53],reduc:[12,17,29,41,46,51],reduced_cost:[12,17],reeiv:22,refcallablelibrari:[9,28],refcppcplex:9,refer:[3,20,29,37,38,39],referenc:8,refin:[8,9,22,44,45],refine_conflict:8,refined_bi:8,reflect:[3,20],reg:[0,37,38,39],regard:13,regardless:[9,17,56],regexpr:17,region:2,regist:7,register_callback:7,register_watched_var:7,regul:46,regular:[17,48],rel:[17,22,28,57],relat:[1,17,19,29,43,44,45,57],relationship:42,relax:[8,9,10,26,40,44,45,46,47,50,51],relax_linear:50,relax_mod:27,relaxationmod:[9,27],relaxed_best:40,relaxed_objective_valu:27,reldiff:22,relev:43,reli:[17,29],reliabl:54,reltol:17,remain:22,remaining_nb_nod:22,rememb:[17,48,57],remov:[8,16,17,35,46],remove_abort_callback:35,remove_constraint:17,remove_kpi:17,remove_object:17,remove_progress_listen:17,remove_term:16,repeat:4,replac:[16,17,35,44,45],replai:53,replic:8,report:[1,2,11,17,22,39,41,42,43,46,47,48,52,53,54,56],report_kpi:[5,17,44,45],repr:17,repres:[2,3,7,8,10,17,35,42,46,47,48,54,56,57],represent:[8,10,11,17,20,30,31,35,53,57],request:[0,35],requir:[0,1,2,3,5,15,17,18,42,44,45,48,51,53,57],requyir:17,res:[49,57],research:[0,37],reserv:56,reset:[12,17,20,46],reset_index:46,resourc:[1,2,17,42,43,46,47,48,49,53,56],resource_nam:49,resp:17,respect:[42,46,48,57],respond:42,respons:[35,42],restor:[17,22,32],restore_numpi:17,restrict:[35,53],result:[1,4,5,8,17,23,29,35,40,41,42,44,45,47,49,51,52,53,56],result_output:11,ret:4,retriev:[17,35],rett:42,return_parti:26,revenu:[42,48],revers:[17,57],revert:10,revis:47,rhs:[7,10,17],rice:38,ridglei:42,right:[1,2,7,10,42,43,46,47,48,51,53,56],right_expr:10,ripe:5,risk:[1,2,41,42,43,46,47,48,51,53,56],rith:7,rmodel:17,rng:57,rng_name:17,roast:5,roberta:[44,45,46,47],robust:57,roland:42,roll:[4,51],roll_width:4,rome:57,root:[17,20,43,46,47],rootparametergroup:[11,17,18,20],round:[17,46],round_solut:17,roush:42,row:[1,5,43,46,53,56],rule:[46,47],run:[4,8,9,13,17,27,35,37,40,41,42,55],run_default_gap_model_with_lagrangian_relax:40,run_gap_model:40,run_gap_model_with_lagrangian_relax:40,runtim:[13,17,18,22,37,42,48,56],s_discount:43,s_mipstart:29,s_to_:41,safe:[16,17],saint:[52,53],salari:[44,45,47],sale:[48,54],same:[9,12,16,17,20,22,24,29,32,35,43,44,45,46,47,51,52,56],sampl:[1,2,38,42,43,45,46,47,48,51,53,56],san:[52,53],sanaa:42,sandra:42,sanger:42,satieti:[5,51],satisfact:[10,17,29,49],satisfactori:4,satisfi:[1,3,5,7,10,17,29,43,47,48,49,51,56,57],saturdai:[44,45,46],sauc:[5,51],sav:[17,18],save:[4,5,11,35,40,41,42,44,45,49,52],scal_prod:[17,40],scal_prod_f:[17,44],scal_prod_vars_all_differ:17,scalar:[16,17],scale:[5,24],scale_factor:40,scatter:[1,57],scatterpoint:57,scenario:[1,2,42,43,46,47,48,53,56],schedul:[47,51,52],scheme:57,scienc:[38,39],scientif:[38,48],scipi:38,scope:[3,11,46,47],scratch:38,script:[35,51],sdetail:[17,29,50],seahawk:[52,53],search:[3,9,11,13,17,22,37,39,46,50,57],season:[51,52,53],seattl:[52,53],sec:[43,46,47],second:[1,2,9,17,25,28,35,53,57],secondnurs:[44,45],section:[3,38,43,54,57],see:[0,7,9,11,12,17,20,22,28,29,35,37,38,43,46,48,57],segment:[1,17],select:[5,37,42],selected_kpi:17,self:[4,7,16,23,24,25,35,41,42,44,45,53,57],sell:48,semi:[9,17,19,26,31],semicontinu:[17,19,31],semicontinuous_var:17,semicontinuous_var_dict:17,semicontinuous_var_list:17,semicontinuous_var_matrix:17,semicontinuous_vartyp:17,semicontinuousvartyp:[17,31],semiinteg:[17,31],semiinteger_var:17,semiinteger_var_dict:17,semiinteger_var_list:17,semiinteger_var_matrix:17,semiinteger_vartyp:17,semiintegertyp:17,semiintegervartyp:[17,31],seminar:42,send:[35,53],senior:[44,45,46,47],sens:[7,10,17,27,32,57],sensit:29,sent:17,sep:20,separ:[17,20,25,43,46,47,56],seper:17,seq_of_kei:17,sequenc:[10,12,17,29,30,43,56],seri:[17,38,43,46,53,56],serial:35,serv:51,server001:41,server002:41,server003:41,server004:41,server005:41,server006:41,server007:41,server:[41,51,57],servic:[35,37,38,41,42,48,51,54],set:[3,5,6,8,10,11,12,14,16,17,18,20,23,24,25,26,29,30,32,35,39,41,44,45,57],set_coeffici:4,set_lex_multi_object:17,set_lp_start_basi:17,set_mandatori:10,set_multi_object:17,set_multi_objective_abstol:17,set_multi_objective_expr:17,set_multi_objective_reltol:17,set_object:17,set_objective_valu:29,set_output_attach:35,set_stop_callback:35,set_time_limit:17,setter:7,setup:[4,41],setup_constraint:[44,45],setup_data:[44,45],setup_object:[44,45],setup_vari:[44,45],sever:[14,22,35,46,47,54],shallow:11,share:[17,22,53],she:47,sheet:[46,47,56],shell:13,shift:[44,45,51],shift_act:[44,45],shift_row:[44,45],shiftact:[44,45],shiftid:46,shifts_:[44,45],shop:51,short_nam:[9,20,31],shortcut:3,shorten:10,shorter:12,should:[2,7,8,11,17,20,22,28,29,35,43,44,45,46,47,48,49,51,52,56],show:[1,5,8,24,42,43,47,51,53,57],shu:42,side:[1,4,7,20,29],sift:46,sign:[1,2,37,42,43,46,47,48,53,56],significantli:[46,56],signup:0,silent:29,similar:[8,17,22,27,53,56],similarli:[46,47],simpl:[2,3,5,22,35,36,38,49,51,53,57],simpler:3,simplest:17,simplex:5,simpli:46,simplifi:[3,43,57],sinc:[11,17,22,35,43,44,45,48,55,56],singl:[3,17,29,56],singleton:[8,29],sis:17,site:[2,38],situat:[1,2,42,43,46,47,48,53,56],six:[0,57],size:[0,4,7,17,28,29,37,42,48,56],skaroupka:42,skill:[44,45,46],skill_requir:[44,45],skilled_nurs:[44,45],skillrequir:[46,47],skin:5,slack:[9,10,17,27,29],slack_valu:[10,17,29],slave:4,sleep:[41,51],sleeping_process:41,slope:[17,24],slopebreaksx:17,slrm:42,small:[4,10,29,57],smaller:[17,43,56],snippet:2,snot:16,social:39,softwar:[37,54],sol:[7,17,22,29,44,45],soluion:9,solut:[3,4,5,7,9,11,12,15,16,17,22,27,28,32,35,40,41,44,45,49,50,52,57],solutiion:17,solution_callbackfn:17,solution_dict:41,solution_fn:22,solution_header_fmt:17,solution_status_cod:28,solution_storage_handl:35,solution_valu:[4,5,12,16,40,41,44,45,46,49,57],solutionlisten:22,solutionpool:[17,29],solutionrecord:22,soluton:7,solv:[0,4,5,8,10,11,12,15,16,17,18,22,23,27,28,29,32,35,37,39,40,41,44,45,49,51,52],solve_detail:[11,17,28,29,45,52],solve_hook:35,solve_lexicograph:57,solve_statu:17,solve_with_go:17,solveattribut:9,solved_bi:29,solvedetail:[17,28,29],solvedetailsfilt:35,solvefix:32,solvefixex:32,solver:[5,11,17,22,23,35,37],solver_ag:18,solvercontext:11,solvesolut:[3,7,15,17,22,27,29],some:[10,12,16,17,20,26,29,34,35,43,46,47,48,49,51,52,55,56,57],somesolv:23,somesolver_output:23,someth:23,sometim:51,soon:17,sophist:5,sort:[1,14,41,42,44,45,46,52],sos1:[9,17,30],sos2:[17,30],sos_arg:17,sos_typ:30,soss:46,sostyp:[9,17,30],sosvariableset:30,sosvarset:50,sourc:[2,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,34,35,36,40,41,44,45,49,52,53,54],source_solut:17,sousa:42,space:[17,35],spaghetti:[5,51],span:46,specfi:34,special:[15,17,22,29,30,31,44,45],specif:[1,2,4,17,20,22,29,35,42,43,46,47,48,51,53,56],specifi:[8,11,13,17,20,23,24,34,35,36,37,38,40,45,47,51,52],specifics:17,spent:48,spin:56,split:[35,52,53],sport:[51,52],sport_schedul:51,sports_schedul:51,sportschedcplex:52,spreadsheet:53,spss:42,squar:[3,9,17,25,52,53],ssl:55,sssa5p_20:[9,28,29],staf:[46,47],stage:[42,48,56],stai:[10,17,42],standalon:56,standard:[3,5,17,29,37,38,46,55],start:[1,2,3,8,9,17,22,29,34,37,38,40,42,44,45,47,48,53,56],start_auto_configur:13,start_cost:56,start_day_index:[44,45],start_tim:[44,45,46],start_time_of_dai:[44,45],startup:56,stat:29,state:[1,10,17,43,44,45,46,55,57],statement:17,staticmethod:[44,45],statist:[8,17,19,29],statu:[3,8,9,10,12,17,22,28,29],status_cod:[17,28],status_str:28,std:[17,43],stdout:[3,11,17,22],steeler:[52,53],steffen:42,step:[3,17,57],sthe:8,stigler:[5,51],still:[10,12,17],stock:[4,51],stop:[4,9,17,22,35,40],stop_callback:35,stor:[44,45],storag:[1,35],store:[1,4,6,11,17,22,29,35,42,43,46,47,48,52,53,56,57],store_:57,store_solut:35,str:[3,4,17,35,40,44,45,57],str_use_spac:17,straightforward:[46,57],strategi:9,stream:[11,17,20,29,35,36],strictli:17,string:[3,7,8,9,10,11,13,17,18,20,23,27,28,29,30,31,32,34,35,44,45,46,57],stringio:17,strip:51,strong:[1,2,42,43,46,47,48,53,56],strong_check:29,strparamet:20,structur:[3,47,48,53],student:0,studio:[0,1,2,35,38,39,42,43,46,47,48,52,53,56],style:[10,29,36],stylesheet:[48,53],sub:[4,17,20,22,31],subclass:[15,17,18,20,22,44,45],subdirectori:51,subgroup:20,subject:[2,49,53],submiss:54,submit:[48,51],subpackag:2,subscript:[42,48,56],subsequ:[17,43,46],subset:[7,46],substitut:[43,46],substr:17,subtract:[16,24,48],succe:[17,18,27,40],succesfulli:12,success:[17,27,29,52,56],successfulli:[10,12,17,29,32,43,46,57],sucess:13,sudoku:3,suffici:46,suffix:57,suggest:[1,2,42,43,46,47,48,53,56],suit:22,sum:[1,3,4,5,9,16,17,35,40,41,43,44,45,46,47,49,52,56,57],sum_squar:17,sum_var:17,sum_vars_all_differ:17,summar:[22,46,57],summat:[17,46],sumsq:17,sundai:[44,45,46],superclass:22,superior:[1,2,42,43,46,47,48,53,56],supersed:57,suppli:[51,57],supply1:57,supply2:57,supply_cost:57,supply_cost_by_warehous:57,supply_cost_obj:57,supply_first:57,supply_first_then_open:57,supply_london_store_1:57,supply_var:57,support:[5,9,11,28,29,35,38],supposedli:43,suppress:14,sure:43,surround:17,suspect:2,suzann:[44,45,46,47],svr:41,sync:[20,43,46,47],synergi:[1,2,42,43,46,47,48,53,56],synonym:[17,18],syntax:[17,20],sys:[3,11,29],system:[1,2,13,17,37,43,46,47,53,56,57],tabl:[1,2,3,11,34,35,42,43,46,47,48,53,56],tabular:53,take:[1,2,5,7,9,11,12,15,16,17,20,27,42,43,44,45,46,47,48,49,53,56,57],taken:[4,7,42,57],tampa:[52,53],target:[17,51],tconflictconstraint:8,team1:[52,53],team2:[52,53],team:[5,46,47,51,52,54],team_div1:52,team_div2:52,team_rang:52,technic:[54,57],technolog:[1,2,5,42,43,46,47,48,53],tell:[22,46],temp:[18,29],temp_obj:32,temp_paramet:32,temp_sens:32,tempfil:[17,29],temporari:[20,29,32],temporarili:[32,35],ten:48,tennesse:[52,53],term:[0,16,17,25,43,44,45,46,47,48,51,57],termin:[4,17],terri:42,test:[3,12,16,17,41,57],texan:[52,53],text:[28,34,46,56],textbook:57,textprogresslisten:22,than:[4,9,17,35,42,43,44,45,46,47,56,57],thei:[2,9,11,16,17,22,24,29,35,37,42,46],them:[5,17,26,29,35,36,46,47,48,49],then_ct:[10,17],theoret:[46,49],therefor:17,thi:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,34,35,36,37,39,40,41,42,43,44,45,46,47,48,49,51,52,53,54,56,57],thing:43,think:53,third:[0,2,35,46,53,57],thoroughli:17,those:[2,17,20,22,32,43,57],though:[3,46,57],thread:[17,35,43,44,45,46,47],threat:[1,2,42,43,46,47,48,53,56],three:[3,9,10,17,48,57],threshold:22,through:[0,1,2,12,16,22,27,42,43,46,47,48,53,54,56],thse:[44,45],thu:[5,22,46,47,48],thursdai:[44,45,46],tick:[28,43,46,47],time:[1,2,4,5,7,9,16,17,20,22,28,35,41,43,44,45,48,51,52,53,56,57],time_limit:17,time_of_dai:[44,45],timeli:2,timelimit:[17,32,44,45],tis:22,titan:[52,53],titem:4,tnurs:[44,45],tnurse1:[44,45],tnursepair:[44,45],to_abstim:[44,45],to_bool:[12,52],to_csv:35,to_msgpack:35,to_str:[10,20,30,31],togeth:[17,47,48,51],toler:[3,7,17,20,29,45],tolernac:32,tomato:5,tool:[37,48,53,54],toolkit:57,top:53,topic:[29,38],total:[1,2,4,5,17,19,20,40,41,42,44,45,46,47,49,51,52,53,56,57],total_assign:[44,45],total_cost:5,total_cutting_cost:4,total_fair:[44,45],total_hour:[44,45],total_inside_cost:49,total_number_of_assign:[44,45],total_number_of_param:20,total_opening_cost:57,total_outside_cost:49,total_over_average_worktim:[44,45],total_penalti:40,total_profit:40,total_salary_cost:[44,45],total_supply_cost:57,total_under_average_worktim:[44,45],totaloff:42,toth:17,toutputt:27,tpattern:4,trace:51,tracker:35,trade:[1,2,42,43,46,47,48,53,56],train:38,transact:[44,45],transform:[17,43,48],translat:[17,24,27,35],travers:[12,20],tree:[3,20,22,46],tri:[43,46],triag:54,trick:[44,45,57],trigger:[17,35],triniti:42,triplet:[17,25],trivial:10,true_valu:17,trust:17,truth:10,truth_valu:10,try_match:17,tshift:[44,45],tskillrequir:[44,45],tsolut:52,tsou:42,tstore1:57,tstore:57,tuesdai:[44,45,46],tupl:[4,5,7,8,17,22,27,28,29,43,44,45,49,52,53,57],turn:[17,38,43],turn_off:56,turn_off_coal1_1:56,turn_off_coal1_2:56,turn_off_coal1_3:56,turn_off_coal1_4:56,turn_off_coal1_5:56,turn_on:56,turn_on_coal1_1:56,turn_on_coal1_2:56,turn_on_coal1_3:56,turn_on_coal1_4:56,turn_on_coal1_5:56,tuser:41,tutori:[1,2,42,43,46,47,48,53,56],tvacat:[44,45],twarehous:57,twarehouse1:57,twice:[43,57],two:[1,2,4,7,9,10,12,16,17,29,35,42,43,44,45,46,47,48,52,53,56,57],tworkrul:[44,45],txt:[0,35],type:[1,2,3,6,7,8,9,10,11,12,14,15,16,17,18,19,20,22,25,26,27,28,29,30,31,32,34,35,42,43,46,47,48,51,53,55,56],typecheck:17,typic:[10,17,20,43,48,51],u_s:41,ubs:17,ucp_panda:51,ultim:37,unambigu:46,unauthor:35,unbounded:17,uncertain:[1,2,42,43,46,47,48,53,56],unchang:[29,32],under:[0,4,5,17,29,35,39,40,41,44,45,46,49,52],under_work:46,underli:[15,35],understood:2,underw_ann:46,underw_bethani:46,underw_betsi:46,underw_cathi:46,underw_cec:46,undo:17,unexpect:17,unfilt:11,unic:57,unicod:35,uniformpriorit:27,uninstal:[37,38],union:17,uniqu:[15,17,20,57],unit:51,unit_cost:5,univers:38,unknown:9,unless:[8,17,23],unlimit:[0,49,52],unnam:27,unnecessari:46,unord:17,unrealist:46,unrelax:26,unsatisfi:7,unsort:46,unstack:[43,46,56],unsupport:55,unsupportedplatformerror:13,until:[4,43],updat:[1,2,4,11,29,35,37,38,40,42,43,46,47,48,53,56],update_solve_detail:35,updated_us:4,updateev:9,upgrad:[37,38],upper:[8,9,10,12,17,31,44,45,56],upper_bound:29,uppercas:[44,45],urllib3:0,urx:0,usabl:35,usag:[4,17,23,44,45],usd:28,use:[1,2,3,4,7,10,11,12,16,17,20,22,23,29,30,32,35,37,38,42,43,45,46,47,48,51,52,53,57],use_dual_expr:4,use_nam:17,use_spac:10,use_valu:4,use_var:4,used:[2,3,4,5,6,7,9,10,11,12,15,16,17,18,20,22,23,24,25,27,29,30,32,34,35,37,38,41,42,43,46,47,48,51,55,56,57],useful:[29,32,35,42],user001:41,user002:41,user003:41,user004:41,user005:41,user006:41,user007:41,user008:41,user009:41,user010:41,user011:41,user012:41,user013:41,user014:41,user015:41,user016:41,user017:41,user018:41,user019:41,user020:41,user021:41,user022:41,user023:41,user024:41,user025:41,user026:41,user027:41,user028:41,user029:41,user030:41,user031:41,user032:41,user033:41,user034:41,user035:41,user036:41,user037:41,user038:41,user039:41,user040:41,user041:41,user042:41,user043:41,user044:41,user045:41,user046:41,user047:41,user048:41,user049:41,user050:41,user051:41,user052:41,user053:41,user054:41,user055:41,user056:41,user057:41,user058:41,user059:41,user060:41,user061:41,user062:41,user063:41,user064:41,user065:41,user066:41,user067:41,user068:41,user069:41,user070:41,user071:41,user072:41,user073:41,user074:41,user075:41,user076:41,user077:41,user078:41,user079:41,user080:41,user081:41,user082:41,user:[6,7,10,17,20,23,27,29,35,41,56],user_assign:41,user_row:41,user_server_pair_nam:41,users_:41,uses:[3,17,22,23,34,41,43,46,53,56],using:[1,2,3,7,10,11,12,13,15,16,17,18,22,23,24,25,27,29,35,37,38,39,41,42,43,45,46,47,48,51,53,54,55,56,57],usr:13,usual:[10,17,56],utf:[4,40,41],util:[4,5,13,17,18,40,41,44,45,49,50,52],uupper:9,v12:[0,37,55],vac_dai:[44,45],vac_n:[44,45],vac_nurse_id:[44,45],vacat:[44,45],vacation_row:[44,45],vacations_:[44,45],valid:[6,7,13,15,17,20,22,29],valu:[1,2,3,4,7,8,9,10,11,12,15,16,17,20,22,24,27,28,29,30,31,32,35,42,43,44,45,46,47,48,53,56,57],value_column_nam:29,value_kei:29,valueerror:35,var1:25,var2:25,var_basis_status:17,var_bound:8,var_cub:17,var_dict:[5,17,29],var_hypercub:17,var_index:7,var_kei:29,var_seq:29,var_value_fmt:17,var_value_map:29,var_values_iter:29,varboundtyp:[8,9],varboundwrapp:8,vari:17,variabel:8,variabl:[4,5,6,7,8,9,10,12,15,16,17,19,22,25,26,29,30,31,35,37,40,44,45,49,52],variable_cost:56,variable_sequ:30,variant:17,variat:[5,51,56],variou:[9,17,19,27,35,46,47,49],varlbconstraintwrapp:8,vartyp:[12,17,50],vartype_spec:17,varubconstraintwrapp:8,vector:1,verb:9,verbos:[4,18,27,44],veri:[5,12,35,43,57],verifi:38,version:[0,1,2,4,5,8,9,10,11,13,17,22,28,29,32,35,37,38,39,40,41,43,44,45,46,47,49,52,53,55,56,57],very_high:21,very_low:21,via:[17,37,38,42],vicki:[44,45,46,47],view:3,vike:[52,53],violat:27,virtual:38,visual:[38,47],vit_a:[5,51],vlad:42,vname:3,vnet:[1,2,39,42,43,46,47,48,53,54,56],wai:[1,2,10,11,17,18,35,37,38,42,43,46,47,48,53,56,57],wait:[43,46,47],walk:2,want:[1,17,23,24,26,38,42,43,46,47,51,56],warehouse_model:57,warm:[17,29],warn:[14,17,35,36,47,51,55,57],washington:[52,53],watch:7,watson:[1,2,35,37,38,39,42,43,46,47,48,53,56],websit:2,wednesdai:[44,45,46],week:[46,47,52],weekdai:[44,45],weight:[17,30,43,46,47,52,56],welcom:[0,39],well:[5,22,46,47,48,51,56,57],wend:46,wendi:[44,45,46,47],were:[8,17,42],wh_label:57,what:[3,9,22,43,48,51,55,56,57],when:[1,2,7,8,11,12,17,18,20,22,24,26,27,28,29,32,35,37,38,42,43,46,47,48,53,56,57],whenev:[43,56,57],where:[2,3,7,9,10,11,13,17,22,25,27,37,51,53,56,57],wherea:42,whether:[6,7,8,10,12,13,16,17,20,22,25,29,42,46,52,53,55,57],which:[1,2,3,6,7,8,10,12,15,16,17,20,22,24,27,29,32,34,35,38,42,43,46,47,48,51,53,56,57],whichi:17,who:[42,46,47,53],whole:4,whose:[8,17,24,35,46,57],why:[17,57],width:[4,56],wil:[44,45],william:[43,51],winclud:17,wind:56,window:[1,2,37,38,43,46,47,53,56,57],wish:[1,43,46,47],witelevel:17,with_func:50,within:[2,8,17,22,27,29,38,42,52,53,56],without:[5,16,17,40,51,56],withth:7,wolfgang:42,wolsei:40,wonder:57,wood:42,word:[1,8,20,29,46,47],work:[5,9,22,35,44,45,51,52,53,56],work_0_0:43,work_0_1:43,work_0_2:43,work_0_3:43,work_0_4:43,work_1_0:43,work_1_1:43,work_1_2:43,work_1_3:43,work_1_4:43,work_2_0:43,work_2_1:43,work_2_2:43,work_2_3:43,work_2_4:43,work_3_0:43,work_3_1:43,work_3_2:43,work_3_3:43,work_3_4:43,work_rul:[44,45],work_time_:[44,45],work_time_max:[44,45],work_time_var:[44,45],workdir:20,worker:[35,41],worker_util:50,workerenviron:35,workflow:51,workload:41,worktim:[44,45,47],worktime_ann:46,worktime_bethani:46,worktime_betsi:46,worktime_cathi:46,worktime_cecilia:46,worktime_chri:46,worktime_cindi:46,worktime_david:46,worktime_de:46,worktime_debbi:46,worktime_gloria:46,worktime_isabel:46,worktime_jan:46,worktime_janel:46,worktime_janic:46,worktime_jemma:46,worktime_joan:46,worktime_joyc:46,worktime_jud:46,worktime_juli:46,worktime_juliet:46,worktime_k:46,worktime_n:46,worktime_nathali:46,worktime_nicol:46,worktime_patricia:46,worktime_patrick:46,worktime_roberta:46,worktime_suzann:46,worktime_vicki:46,worktime_wendi:46,worktime_zo:46,world:5,wors:17,would:[2,48,57],wrangl:38,wrap:8,wrapper:[8,55],write:[2,3,4,17,20,22,29,34,35,40,41],write_df:35,write_level:[17,29],write_output_t:23,write_pass_fil:17,write_table_as_csv:34,writelevel:[9,17,29],writer:35,written:[9,17,20,23,29,35,37,43],wrong:17,wrow:57,wsnotebookenviron:35,wstart:46,wwpdl:[1,2,39,42,43,46,47,48,53,54,56],www:[0,2,4,5,9,28,29,40,41,44,45,49,52],wynkoop:42,x86:[13,37],x_1_2:1,x_i:40,x_k1:17,x_k2:17,x_val:24,x_valu:17,x_var:40,xlabel:57,xml:29,xor:17,xx_var_dict:29,xx_var_matrix:17,xx_var_var_matrix:17,xxx:[9,17,26,37],xxx_var:17,xxx_var_dict:17,xxx_var_matrix:17,yards_20:2,year:[5,51,53],yearli:43,yes:[41,43,57],yet:[5,17,57],yhat:41,yhathq:41,yield:[17,20,46],ylabel:57,york:[52,53],you:[0,1,2,11,12,13,16,17,22,26,28,35,37,38,39,42,43,46,47,48,51,53,54,55,56],your:[0,1,2,11,13,22,35,38,39,42,43,46,47,48,51,53,54,55,56],your_cb:35,yuina:42,z_0:17,z_1:17,z_2:17,zeeb:42,zelaya:42,zero:[8,9,10,12,17,26,28,29,43,44,45,46,48,57],zeroexpr:16,zip:[4,40,51],zoe:[44,45,46,47]},titles:["README.md","Objects in boxes","Finding optimal locations of new stores","Creating a mathematical programming model","cutstock.py","diet.py","docplex.mp.basic module","docplex.mp.callbacks.cb_mixin module","docplex.mp.conflict_refiner module","docplex.mp.constants module","docplex.mp.constr module","docplex.mp.context module","docplex.mp.dvar module","docplex.mp.environment module","docplex.mp.error_handler module","docplex.mp.kpi module","docplex.mp.linear module","docplex.mp.model module","docplex.mp.model_reader module","docplex.mp.model_stats module","docplex.mp.params.parameters module","docplex.mp.priority module","docplex.mp.progress module","docplex.mp.publish module","docplex.mp.pwl module","docplex.mp.quad module","docplex.mp.relax_linear module","docplex.mp.relaxer module","docplex.mp.sdetails module","docplex.mp.solution module","docplex.mp.sosvarset module","docplex.mp.vartype module","docplex.mp.with_funcs module","docplex.mp.worker_utils module","docplex.util.csv_utils module","docplex.util.environment module","docplex.util.logging_utils module","Setting up an optimization engine","Setting up a Python environment","Mathematical Programming Modeling for Python (DOcplex.MP)","lagrangian_relaxation.py","load_balancing.py","How to make targeted offers to customers?","Optimizing mining operations","nurses.py","nurses_multiobj.py","The Nurse Assignment Problem","The Nurses Model","Maximizing the profit of an oil company","production.py","docplex.mp reference manual","Examples of mathematical programming","sport_scheduling.py","Use decision optimization to help a sports league schedule its games","IBM\u00ae Elite Support for DOcplex","Troubleshooting","The Unit Commitment Problem (UCP)","The Warehouse Problem"],titleterms:{"import":[1,2,3,42,43,46,47,48,53,56],"new":[2,46],"try":38,Adding:43,But:46,The:[43,46,47,53,56,57],Use:[1,2,42,43,46,47,48,53,56],Using:[37,43],absolut:46,actual:43,addit:[1,43],after:46,aggreg:3,analysi:[1,2,43,46,47,48,53,56],analyz:[42,46],arbitr:56,arrai:43,assign:[46,47],associ:[46,47],avail:56,averag:[43,47],basic:6,between:[2,53,56],blend:43,box:1,breakdown:56,build:3,busi:[1,2,42,43,46,47,48,53,56,57],callback:7,can:[1,2,42,43,46,47,48,53,56],cannot:[47,53],capac:48,cb_mixin:7,check:[43,46,56],close:43,co2:56,commit:56,compani:48,comprehens:54,comput:[2,37,46,47],conda:[37,38],conflict_refin:8,constant:9,constr:10,constraint:[1,2,3,42,43,46,47,48,53,56,57],contact:[39,54],content:51,context:11,correct:53,cost:[43,46,56],cplex:[37,38,42],creat:[1,2,3,42,43,46,47,48,53,56],csv_util:34,custom:42,cutstock:4,data:[1,2,42,43,46,47,48,53,56,57],datafram:56,decis:[1,2,3,39,42,43,46,47,48,53,56,57],declar:2,defin:[1,2,3,42,43,46,47,48,53,56,57],demand:[46,48,56],describ:[1,2,42,43,46,47,48,56],develop:38,diagram:57,diet:5,discount:43,displai:[2,48,57],distanc:2,distribut:46,docplex:[1,2,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,38,39,43,46,47,48,50,53,54,56,57],down:56,download:51,downtim:56,durat:46,dvar:12,each:[43,46,53],earth:2,eclips:54,econom:56,eighth:47,elit:54,end:46,energi:56,enforc:47,engin:37,environ:[13,35,38],error_handl:14,exactli:53,exampl:[1,2,38,43,46,47,48,51,53,56],excel:[46,47],expect:43,explor:57,express:[1,2,3,42,43,46,47,48,53,56],extract:43,fair:46,fifth:47,file:3,find:2,first:[47,53],follow:39,footbal:53,fourth:47,from:[46,47,56],frontier:57,game:53,gener:3,get:[37,38],global:43,goal:46,half:53,help:[1,2,42,43,46,47,48,53,56],heurist:43,how:[1,2,42,43,46,47,48,53,56],ibm:[37,42,54],ilog:37,incompat:[46,47],indic:[39,50],initi:56,input:57,instal:[37,38],intradivision:53,introduc:46,investig:[1,2,43,46,47,48,53,56],its:53,kpi:[15,43,46],lagrangian_relax:40,lead:48,leagu:53,level:[46,48],librari:[1,2,37,38,42,43,46,47,48,53,56],limit:43,linear:16,link:[46,56],list:2,load:[46,47,56],load_balanc:41,local:38,locat:2,logging_util:36,make:42,manag:38,manual:50,math:57,mathemat:[3,39,51],maxim:48,maximum:[47,48],measur:46,mine:43,minim:[46,47],minimum:[43,46,47,56],model:[1,2,3,17,38,39,42,43,46,47,48,53,56,57],model_read:18,model_stat:19,modul:[3,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36],more:38,must:53,nation:53,necessari:3,number:[2,43,53,56],nurs:[44,46,47],nurses_multiobj:45,object:[1,2,42,43,46,47,48,53,56,57],octan:48,off:56,offer:42,oil:48,onc:[43,53],onli:43,onlin:56,open:[2,43],oper:43,optim:[1,2,37,39,42,43,46,47,48,53,56],out:38,over:47,overlap:[46,47],packag:[37,38],pair:53,panda:[46,47,56],param:20,paramet:[3,20],pareto:57,per:46,period:56,pip:[37,38],plai:53,plant:56,platform:54,point:2,portal:54,post:46,prepar:[1,2,42,43,46,47,48,53,56],prescript:[1,2,42,43,46,47,48,53,56],prioriti:21,problem:[1,2,42,43,46,47,48,53,56,57],product:[48,49,56],profit:48,program:[3,39,51],progress:22,provid:54,publish:23,pwl:24,python:[38,39,42],quad:25,qualiti:43,quantiti:43,ramp:56,rate:43,readm:0,refer:[1,2,42,43,46,47,48,50],relax:27,relax_linear:26,requir:[38,43,46,47,56],resourc:38,result:[3,43,46,57],retriev:3,revenu:43,royalti:43,run:[1,2,43,46,47,48,51,53,56],salari:46,same:53,schedul:53,sdetail:28,second:47,select:47,set:[1,2,37,38,42,43,46,47,48,53,56],setup:37,seventh:47,shift:[46,47],shop:2,should:53,sixth:47,skill:47,solut:[1,2,29,42,43,46,47,48,53,56],solv:[1,2,3,42,43,46,47,48,53,56,57],some:53,sosvarset:30,sourc:51,sport:53,sport_schedul:52,stai:43,start:[43,46],state:56,statu:56,step:[1,2,42,43,46,47,48,53,56],store:2,studio:37,sub:46,success:53,summari:[1,2,42,43,46,47,48,53,56],support:54,system:38,tabl:[39,50],target:42,team:53,technolog:56,them:2,third:47,three:46,time:[46,47],togeth:46,tool:38,total:[43,48],troubleshoot:[38,55],turn:56,ucp:56,under:47,unit:56,uptim:56,use:56,util:[34,35,36],vacat:[46,47],valid:2,variabl:[1,2,3,42,43,46,47,48,53,56,57],vartyp:31,visual:43,warehous:57,week:53,what:[39,46],with_func:32,work:[43,46,47],worker_util:33,worktim:46,would:46,year:43,your:37}}) \ No newline at end of file diff --git a/docs/2.24.232/mp/sport_scheduling.html b/docs/2.24.232/mp/sport_scheduling.html new file mode 100644 index 0000000..512b570 --- /dev/null +++ b/docs/2.24.232/mp/sport_scheduling.html @@ -0,0 +1,399 @@ + + + + + + + + + sport_scheduling.py — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

sport_scheduling.py

+

How can a sports league schedule matches between teams in different divisions such that the teams play each other +the appropriate number of times and maximize the objective of scheduling intradivision matches as late +as possible in the season?

+

A sports league with two divisions needs to schedule games such that each team plays every team within its division a specified +number of times and plays every team in the other division a specified number of times. Each week, a team plays exactly one game. +The preference is for intradivisional matches to be held as late as possible in the season. To model this preference, there is an +incentive for intradivisional matches; this incentive increases in a non-linear manner by week. +The problem consists of assigning an opponent to each team each week in order to maximize the total of the incentives.

+

This example works only with the unlimited version of IBM ILOG CPLEX Optimization Studio as it exceeds the limits of the Community Edition.

+
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
# --------------------------------------------------------------------------
+# Source file provided under Apache License, Version 2.0, January 2004,
+# http://www.apache.org/licenses/
+# (c) Copyright IBM Corp. 2015, 2018
+# --------------------------------------------------------------------------
+
+from collections import namedtuple
+
+from docplex.mp.model import Model
+from docplex.util.environment import get_environment
+
+
+# ----------------------------------------------------------------------------
+# Initialize the problem data
+# ----------------------------------------------------------------------------
+nbs = (8, 3, 2)
+
+team_div1 = {"Baltimore Ravens", "Cincinnati Bengals", "Cleveland Browns",
+             "Pittsburgh Steelers", "Houston Texans", "Indianapolis Colts",
+             "Jacksonville Jaguars", "Tennessee Titans", "Buffalo Bills",
+             "Miami Dolphins", "New England Patriots", "New York Jets",
+             "Denver Broncos", "Kansas City Chiefs", "Oakland Raiders",
+             "San Diego Chargers"}
+
+team_div2 = {"Chicago Bears", "Detroit Lions", "Green Bay Packers",
+             "Minnesota Vikings", "Atlanta Falcons", "Carolina Panthers",
+             "New Orleans Saints", "Tampa Bay Buccaneers", "Dallas Cowboys",
+             "New York Giants", "Philadelphia Eagles", "Washington Redskins",
+             "Arizona Cardinals", "San Francisco 49ers", "Seattle Seahawks",
+             "St. Louis Rams"}
+
+Match = namedtuple("Matches", ["team1", "team2", "is_divisional"])
+
+
+# ----------------------------------------------------------------------------
+# Build the model
+# ----------------------------------------------------------------------------
+def build_sports(**kwargs):
+    print("* building sport scheduling model instance")
+    mdl = Model('sportSchedCPLEX', **kwargs)
+
+    nb_teams_in_division, nb_intra_divisional, nb_inter_divisional = nbs
+    assert len(team_div1) == len(team_div2)
+    mdl.teams = list(team_div1 | team_div2)
+    # team index ranges from 1 to 2N
+    team_range = range(1, 2 * nb_teams_in_division + 1)
+
+    # Calculate the number of weeks necessary.
+    nb_weeks = (nb_teams_in_division - 1) * nb_intra_divisional + nb_teams_in_division * nb_inter_divisional
+    weeks = range(1, nb_weeks + 1)
+    mdl.weeks = weeks
+
+    print("{0} games, {1} intradivisional, {2} interdivisional"
+          .format(nb_weeks, (nb_teams_in_division - 1) * nb_intra_divisional,
+                  nb_teams_in_division * nb_inter_divisional))
+
+    # Season is split into two halves.
+    first_half_weeks = range(1, nb_weeks // 2 + 1)
+    nb_first_half_games = nb_weeks // 3
+
+    # All possible matches (pairings) and whether of not each is intradivisional.
+    matches = [Match(t1, t2, 1 if (t2 <= nb_teams_in_division or t1 > nb_teams_in_division) else 0)
+               for t1 in team_range for t2 in team_range if t1 < t2]
+    mdl.matches = matches
+    # Number of games to play between pairs depends on
+    # whether the pairing is intradivisional or not.
+    nb_play = {m: nb_intra_divisional if m.is_divisional == 1 else nb_inter_divisional for m in matches}
+
+    plays = mdl.binary_var_matrix(keys1=matches, keys2=weeks,
+                                  name=lambda mw: "play_%d_%d_w%d" % (mw[0].team1, mw[0].team2, mw[1]))
+    mdl.plays = plays
+
+    for m in matches:
+        mdl.add_constraint(mdl.sum(plays[m, w] for w in weeks) == nb_play[m],
+                           "correct_nb_games_%d_%d" % (m.team1, m.team2))
+
+    for w in weeks:
+        # Each team must play exactly once in a week.
+        for t in team_range:
+            max_teams_in_division = (plays[m, w] for m in matches if m.team1 == t or m.team2 == t)
+            mdl.add_constraint(mdl.sum(max_teams_in_division) == 1,
+                               "plays_exactly_once_%d_%s" % (w, t))
+
+    # Games between the same teams cannot be on successive weeks.
+    mdl.add_constraints(plays[m, w] + plays[m, w + 1] <= 1
+                        for w in weeks for m in matches if w < nb_weeks)
+
+    # Some intradivisional games should be in the first half.
+    for t in team_range:
+        max_teams_in_division = [plays[m, w] for w in first_half_weeks for m in matches if
+                                 m.is_divisional == 1 and (m.team1 == t or m.team2 == t)]
+
+        mdl.add_constraint(mdl.sum(max_teams_in_division) >= nb_first_half_games,
+                           "in_division_first_half_%s" % t)
+
+    # postpone divisional matches as much as possible
+    # we weight each play variable with the square of w.
+    mdl.maximize(mdl.sum(plays[m, w] * w * w for w in weeks for m in matches if m.is_divisional))
+    return mdl
+
+# a named tuple to store solution
+TSolution = namedtuple("TSolution", ["week", "is_divisional", "team1", "team2"])
+
+
+def print_sports_solution(mdl):
+    # iterate with weeks first
+    solution = [TSolution(w, m.is_divisional, mdl.teams[m.team1], mdl.teams[m.team2])
+                for w in mdl.weeks for m in mdl.matches
+                if mdl.plays[m, w].to_bool()]
+
+    currweek = 0
+    print("Intradivisional games are marked with a *")
+    for s in solution:
+        # assume records are sorted by increasing week indices.
+        if s.week != currweek:
+            currweek = s.week
+            print(" == == == == == == == == == == == == == == == == ")
+            print("On week %d" % currweek)
+
+        print("    {0:s}{1} will meet the {2}".format("*" if s.is_divisional else "", s.team1, s.team2))
+
+
+# ----------------------------------------------------------------------------
+# Solve the model and display the result
+# ----------------------------------------------------------------------------
+if __name__ == '__main__':
+    # Build the model
+    model = build_sports()
+    model.print_information()
+    # Solve the model. If a key has been specified above, the solve
+    # will use IBM Decision Optimization on cloud.
+    if model.solve():
+        model.report()
+        print_sports_solution(model)
+        # Save the CPLEX solution as "solution.json" program output
+        with get_environment().get_output_stream("solution.json") as fp:
+            model.solution.export(fp, "json")
+    else:
+        print("Problem could not be solved: " + model.solve_details.get_status())
+    model.end()
+
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/sports_scheduling.html b/docs/2.24.232/mp/sports_scheduling.html new file mode 100644 index 0000000..630976a --- /dev/null +++ b/docs/2.24.232/mp/sports_scheduling.html @@ -0,0 +1,676 @@ + + + + + + + + + Use decision optimization to help a sports league schedule its games — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

Use decision optimization to help a sports league schedule its games

+

This tutorial includes everything you need to set up decision +optimization engines, build mathematical programming models, and arrive +at a good working schedule for a sports league’s games.

+

When you finish this tutorial, you’ll have a foundational knowledge of +Prescriptive Analytics.

+
+

This notebook is part of Prescriptive Analytics for +Python

+

It requires either an installation of CPLEX +Optimizers +or it can be run on IBM Watson Studio +Cloud (Sign up for a +free IBM Cloud +account +and you can start using Watson Studio Cloud right away).

+
+

Table of contents:

+ +
+

The business problem: Games Scheduling in the National Football League

+
    +
  • A sports league with two divisions must schedule games so that each +team plays every team within its division a given number of times, +and each team plays teams in the other division a given number of +times.
  • +
  • A team plays exactly one game each week.
  • +
  • A pair of teams cannot play each other on consecutive weeks.
  • +
  • While a third of a team’s intradivisional games must be played in the +first half of the season, the preference is for intradivisional games +to be held as late as possible in the season.
      +
    • To model this preference, there is an incentive for +intradivisional games that increases each week as a square of the +week.
    • +
    • An opponent must be assigned to each team each week to maximize +the total of the incentives..
    • +
    +
  • +
+

This is a type of discrete optimization problem that can be solved by +using either Integer Programming (IP) or Constraint Programming +(CP).

+
+
Integer Programming is the class of problems defined as the +optimization of a linear function, subject to linear constraints over +integer variables.
+
+
Constraint Programming problems generally have discrete decision +variables, but the constraints can be logical, and the arithmetic +expressions are not restricted to being linear.
+

For the purposes of this tutorial, we will illustrate a solution with +mathematical programming (MIP).

+
+
+

How decision optimization can help

+
    +
  • Prescriptive analytics (decision optimization) technology recommends +actions that are based on desired outcomes. It takes into account +specific scenarios, resources, and knowledge of past and current +events. With this insight, your organization can make better +decisions and have greater control of business outcomes.

    +
  • +
  • Prescriptive analytics is the next step on the path to insight-based +actions. It creates value through synergy with predictive analytics, +which analyzes data to predict future outcomes.

    +
  • +
  • +
    Prescriptive analytics takes that insight to the next level by +suggesting the optimal way to handle that future situation. +Organizations that can act fast in dynamic conditions and make +superior decisions in uncertain environments gain a strong +competitive advantage.
    +

    +
    +
  • +
+

With prescriptive analytics, you can:

+
    +
  • Automate the complex decisions and trade-offs to better manage your +limited resources.
  • +
  • Take advantage of a future opportunity or mitigate a future risk.
  • +
  • Proactively update recommendations based on changing events.
  • +
  • Meet operational goals, increase customer loyalty, prevent threats +and fraud, and optimize business processes.
  • +
+
+
+

Use decision optimization

+
+

Step 1: Import the library

+

Run the following code to import the Decision Optimization CPLEX +Modeling library. The DOcplex library contains the two modeling +packages, Mathematical Programming (docplex.mp) and Constraint +Programming (docplex.cp).

+

If CPLEX is not installed, install CPLEX Community edition.

+
+
+

Step 2: Model the data

+

In this scenario, the data is simple. There are eight teams in each +division, and the teams must play each team in the division once and +each team outside the division once.

+

Use a Python module, Collections, which implements some data +structures that will help solve some problems. Named tuples helps to +define meaning of each position in a tuple. This helps the code be more +readable and self-documenting. You can use named tuples in any place +where you use tuples.

+

In this example, you create a namedtuple to contain information for +points. You are also defining some of the parameters.

+

Use basic HTML and a stylesheet to format the data.

+

Now you will import the pandas library. Pandas is an open source +Python library for data analysis. It uses two data structures, Series +and DataFrame, which are built on top of NumPy.

+

A Series is a one-dimensional object similar to an array, list, or +column in a table. It will assign a labeled index to each item in the +series. By default, each item receives an index label from 0 to N, where +N is the length of the series minus one.

+

A DataFrame is a tabular data structure comprised of rows and +columns, similar to a spreadsheet, database table, or R’s data.frame +object. Think of a DataFrame as a group of Series objects that share an +index (the column names).

+

In the example, each division (the AFC and the NFC) is part of a +DataFrame.

+

The following display function is a tool to show different +representations of objects. When you issue the display(teams) command, +you are sending the output to the notebook so that the result is stored +in the document.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AFCNFC
0Baltimore RavensChicago Bears
1Cincinnati BengalsDetroit Lions
2Cleveland BrownsGreen Bay Packers
3Pittsburgh SteelersMinnesota Vikings
4Houston TexansAtlanta Falcons
5Indianapolis ColtsCarolina Panthers
6Jacksonville JaguarsNew Orleans Saints
7Tennessee TitansTampa Bay Buccaneers
8Buffalo BillsDallas Cowboys
9Miami DolphinsNew York Giants
10New England PatriotsPhiladelphia Eagles
11New York JetsWashington Redskins
12Denver BroncosArizona Cardinals
13Kansas City ChiefsSan Francisco 49ers
14Oakland RaidersSeattle Seahawks
15San Diego ChargersSt. Louis Rams
+
+
+

Step 3: Prepare the data

+

Given the number of teams in each division and the number of +intradivisional and interdivisional games to be played, you can +calculate the total number of teams and the number of weeks in the +schedule, assuming every team plays exactly one game per week.

+

The season is split into halves, and the number of the intradivisional +games that each team must play in the first half of the season is +calculated.

+

Number of games to play between pairs depends on whether the pairing is +intradivisional or not.

+
+
+

Step 4: Set up the prescriptive model

+
+* system is: Windows 64bit
+* Python version 3.7.3, located at: c:\local\python373\python.exe
+* docplex is present, version is (2, 11, 0)
+* pandas is present, version is 0.25.1
+
+
+

Create the DOcplex model

+

The model contains all the business constraints and defines the +objective.

+
+
+

Define the decision variables

+
+
+

Express the business constraints

+
+
Each pair of teams must play the correct number of games.
+
Model: sports
+ - number of variables: 405
+   - binary=405, integer=0, continuous=0
+ - number of constraints: 45
+   - linear=45
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+
Each team must play exactly once in a week.
+
Model: sports
+ - number of variables: 405
+   - binary=405, integer=0, continuous=0
+ - number of constraints: 135
+   - linear=135
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+
Games between the same teams cannot be on successive weeks.
+
Model: sports
+ - number of variables: 405
+   - binary=405, integer=0, continuous=0
+ - number of constraints: 495
+   - linear=495
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+
Some intradivisional games should be in the first half.
+
Model: sports
+ - number of variables: 405
+   - binary=405, integer=0, continuous=0
+ - number of constraints: 505
+   - linear=505
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+
+

Express the objective

+

The objective function for this example is designed to force +intradivisional games to occur as late in the season as possible. The +incentive for intradivisional games increases by week. There is no +incentive for interdivisional games.

+
+
+
+

Solve with Decision Optimization

+

You will get the best solution found after n seconds, due to a time +limit parameter.

+
Model: sports
+ - number of variables: 405
+   - binary=405, integer=0, continuous=0
+ - number of constraints: 505
+   - linear=505
+ - parameters: defaults
+ - problem type is: MILP
+* model sports solved with objective = 260
+
+
+
+
+

Step 5: Investigate the solution and then run an example analysis

+

Determine which of the scheduled games will be a replay of one of the +last 10 Super Bowls. We start by creating a pandas DataFrame that +contains the year and teams who played the last 10 Super Bowls.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
yearteam1team2
02016Carolina PanthersDenver Broncos
12015New England PatriotsSeattle Seahawks
22014Seattle SeahawksDenver Broncos
32013Baltimore RavensSan Francisco 49ers
42012New York GiantsNew England Patriots
52011Green Bay PackersPittsburgh Steelers
62010New Orleans SaintsIndianapolis Colts
72009Pittsburgh SteelersArizona Cardinals
82008New York GiantsNew England Patriots
92007Indianapolis ColtsChicago Bears
+

We now look for the games in our solution that are replays of one of the +past 10 Super Bowls.

+
[(4, 'February', 'Green Bay Packers', 'Pittsburgh Steelers')]
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
weekMonthTeam1Team2
04FebruaryGreen Bay PackersPittsburgh Steelers
+
+
+
+

Summary

+

You learned how to set up and use IBM Decision Optimization CPLEX +Modeling for Python to formulate a Constraint Programming model and +solve it with CPLEX.

+ +

Copyright � 2017-2019 IBM. IPLA licensed Sample Materials.

+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/support.html b/docs/2.24.232/mp/support.html new file mode 100644 index 0000000..383e6b8 --- /dev/null +++ b/docs/2.24.232/mp/support.html @@ -0,0 +1,124 @@ + + + + + + + + + IBM® Elite Support for DOcplex — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

IBM® Elite Support for DOcplex

+
+

Comprehensive support for the Eclipse platform provided by IBM

+

IBM® Elite Support for Decision Optimization CPLEX Modeling for Python is a technical support package for DOcplex, an open source development environment. +This offering gives you the advantage of using open software with reliable expert support from IBM. +You gain the support you need to build mission-critical applications with this Python modeling package.

+
+
This Elite Support provides:
+
    +
  • Support for the DOcplex packages.
  • +
  • Phone support at no additional cost. IBM support services are available through IBM online issue management tools or by phone, and 24x7 phone support is available for severity 1 issues.
  • +
  • An efficient defect fix process. Defect submissions go through a triage process, and fixes are delivered by the IBM development team.
  • +
+
+
+

Contact your sales representative for more details or contact IBM here in the “Contact IBM” section.

+
+
+

Support portal

+

You can access the Decision Optimization support portal here.

+
+
+

Contacts

+ +
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/troubleshooting.html b/docs/2.24.232/mp/troubleshooting.html new file mode 100644 index 0000000..e5dbf86 --- /dev/null +++ b/docs/2.24.232/mp/troubleshooting.html @@ -0,0 +1,132 @@ + + + + + + + + + Troubleshooting — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

Troubleshooting

+
    +
  • What if I get warnings about SSL while my code is running?
      +
    • These types of warnings indicate that you might be using an unsupported version of Python and +need additional Python libraries. Some details are provided +here.
    • +
    +
  • +
  • How do I check if my local CPLEX® Optimizer is used or not?
      +
    • DOcplex examples issue warnings that state whether a CPLEX Optimizer wrapper is present or not. For example, CPLEX wrapper is present, version is 12.6.3.0, located at: C:\CPLEX_Studio1263.
    • +
    • If an invalid version of CPLEX Optimizer is detected (for example V12.6.1), DOcplex raises an error that indicates the cause.
    • +
    • If no CPLEX Optimizer version is detected, then DOcplex examples raise the message CPLEX wrapper is not available.
    • +
    +
  • +
  • What if I don’t have pip?
      +
    • The lack of pip indicates that you are using an unsupported version of Python since pip is delivered as a standard package +with versions 2.7.9+, 3.4, and 3.5.
    • +
    +
  • +
  • Ask your question in IBM® forums for: +
  • +
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/ucp_pandas.html b/docs/2.24.232/mp/ucp_pandas.html new file mode 100644 index 0000000..801aad9 --- /dev/null +++ b/docs/2.24.232/mp/ucp_pandas.html @@ -0,0 +1,1418 @@ + + + + + + + + + The Unit Commitment Problem (UCP) — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

The Unit Commitment Problem (UCP)

+

This tutorial includes everything you need to set up IBM Decision +Optimization CPLEX Modeling for Python (DOcplex), build a Mathematical +Programming model, and get its solution by solving the model on the +cloud with IBM ILOG CPLEX Optimizer.

+

When you finish this tutorial, you’ll have a foundational knowledge of +Prescriptive Analytics.

+
+

This notebook is part of Prescriptive Analytics for +Python

+

It requires either an installation of CPLEX +Optimizers +or it can be run on IBM Watson Studio +Cloud (Sign up for a +free IBM Cloud +account +and you can start using Watson Studio Cloud right away).

+
+

Table of contents:

+ +
+
+

Describe the business problem

+
    +
  • The Model estimates the lower cost of generating electricity within a +given plan. Depending on the demand for electricity, we turn on or +off units that generate power and which have operational properties +and costs.
  • +
  • The Unit Commitment Problem answers the question “Which power +generators should I run at which times and at what level in order to +satisfy the demand for electricity?”. This model helps users to find +not only a feasible answer to the question, but one that also +optimizes its solution to meet as many of the electricity company’s +overall goals as possible.
  • +
+
+
+

How decision optimization can help

+
    +
  • Prescriptive analytics (decision optimization) technology recommends +actions that are based on desired outcomes. It takes into account +specific scenarios, resources, and knowledge of past and current +events. With this insight, your organization can make better +decisions and have greater control of business outcomes.

    +
  • +
  • Prescriptive analytics is the next step on the path to insight-based +actions. It creates value through synergy with predictive analytics, +which analyzes data to predict future outcomes.

    +
  • +
  • +
    Prescriptive analytics takes that insight to the next level by +suggesting the optimal way to handle that future situation. +Organizations that can act fast in dynamic conditions and make +superior decisions in uncertain environments gain a strong +competitive advantage.
    +

    +
    +
  • +
+

With prescriptive analytics, you can:

+
    +
  • Automate the complex decisions and trade-offs to better manage your +limited resources.
  • +
  • Take advantage of a future opportunity or mitigate a future risk.
  • +
  • Proactively update recommendations based on changing events.
  • +
  • Meet operational goals, increase customer loyalty, prevent threats +and fraud, and optimize business processes.
  • +
+
+
+

Checking minimum requirements

+

This notebook uses some features of pandas that are available in +version 0.17.1 or above.

+
+
+

Use decision optimization

+
+

Step 1: Import the library

+

Run the following code to the import the Decision Optimization CPLEX +Modeling library. The DOcplex library contains the two modeling +packages, Mathematical Programming (docplex.mp) and Constraint +Programming (docplex.cp).

+
+
+

Step 2: Model the data

+
+

Load data from a pandas DataFrame

+

Data for the Unit Commitment Problem is provided as a pandas +DataFrame. For a standalone notebook, we provide the raw data as Python +collections, but real data could be loaded from an Excel sheet, also +using pandas.

+

Update the configuration of notebook so that display matches browser +window width.

+
+
+

Available energy technologies

+

The following df_energy DataFrame stores CO2 cost information, indexed +by energy type.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
co2_cost
coal30
gas5
diesel15
wind0
+

The following df_units DataFrame stores common elements for units of a +given technology.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
energyinitialmin_genmax_genoperating_max_genmin_uptimemin_downtimeramp_upramp_downstart_costfixed_costvariable_cost
coal1coal400100.00425400159212.0183.05000208.61022.536
coal2coal350140.00365350158150.0198.04550117.37031.985
gas1gas20578.0022020567101.295.61320174.12070.500
gas2gas5252.002101975494.8101.71291172.75069.000
gas3gas15554.251651555358.077.5128095.35332.146
gas4gas15039.001581504250.060.01105144.52054.840
diesel1diesel7817.4090783240.024.056054.41740.222
diesel2diesel7615.2087763260.045.055454.55140.522
diesel3diesel04.0020201120.020.030079.638116.330
diesel4diesel02.4012121112.012.025016.25976.642
+
+
+
+

Step 3: Prepare the data

+

The pandas merge operation is used to create a join between the +df_units and df_energy DataFrames. Here, the join is performed based +on the ‘energy’ column of df_units and index column of df_energy.

+

By default, merge performs an inner join. That is, the resulting +DataFrame is based on the intersection of keys from both input +DataFrames.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
energyinitialmin_genmax_genoperating_max_genmin_uptimemin_downtimeramp_upramp_downstart_costfixed_costvariable_costco2_cost
units
coal1coal400100.00425400159212.0183.05000208.61022.53630
coal2coal350140.00365350158150.0198.04550117.37031.98530
gas1gas20578.0022020567101.295.61320174.12070.5005
gas2gas5252.002101975494.8101.71291172.75069.0005
gas3gas15554.251651555358.077.5128095.35332.1465
+

The demand is stored as a pandas Series indexed from 1 to the number +of periods.

+
nb periods = 192
+
+
+
<matplotlib.axes._subplots.AxesSubplot at 0x23fbad3d588>
+
+
+_images/ucp_pandas_20_2.png +
+
+

Step 4: Set up the prescriptive model

+
+* system is: Windows 64bit
+* Python version 3.7.3, located at: c:\local\python373\python.exe
+* docplex is present, version is (2, 11, 0)
+* pandas is present, version is 0.25.1
+
+
+

Create the DOcplex model

+

The model contains all the business constraints and defines the +objective.

+
+
+

Define the decision variables

+

Decision variables are:

+
    +
  • The variable in_use[u,t] is 1 if and only if unit u is working at +period t.
  • +
  • The variable turn_on[u,t] is 1 if and only if unit u is in +production at period t.
  • +
  • The variable turn_off[u,t] is 1 if unit u is switched off at +period t.
  • +
  • The variable production[u,t] is a continuous variables representing +the production of energy for unit u at period t.
  • +
+
Model: ucp
+ - number of variables: 7680
+   - binary=3840, integer=0, continuous=3840
+ - number of constraints: 0
+   - linear=0
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
in_useturn_onturn_offproduction
unitsperiods
coal11in_use_coal1_1turn_on_coal1_1turn_off_coal1_1p_coal1_1
2in_use_coal1_2turn_on_coal1_2turn_off_coal1_2p_coal1_2
3in_use_coal1_3turn_on_coal1_3turn_off_coal1_3p_coal1_3
4in_use_coal1_4turn_on_coal1_4turn_off_coal1_4p_coal1_4
5in_use_coal1_5turn_on_coal1_5turn_off_coal1_5p_coal1_5
+
+
+

Express the business constraints

+
+
Linking in-use status to production
+

Whenever the unit is in use, the production must be within the minimum +and maximum generation.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
in_useturn_onturn_offproductionmin_genmax_gen
unitsperiods
coal11in_use_coal1_1turn_on_coal1_1turn_off_coal1_1p_coal1_1100.0425
2in_use_coal1_2turn_on_coal1_2turn_off_coal1_2p_coal1_2100.0425
3in_use_coal1_3turn_on_coal1_3turn_off_coal1_3p_coal1_3100.0425
4in_use_coal1_4turn_on_coal1_4turn_off_coal1_4p_coal1_4100.0425
5in_use_coal1_5turn_on_coal1_5turn_off_coal1_5p_coal1_5100.0425
+
0.25.1
+
+
+
+
+
Initial state
+

The solution must take into account the initial state. The initial state +of use of the unit is determined by its initial production level.

+
Model: ucp
+ - number of variables: 7680
+   - binary=3840, integer=0, continuous=3840
+ - number of constraints: 3860
+   - linear=3860
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+
Ramp-up / ramp-down constraint
+

Variations of the production level over time in a unit is constrained by +a ramp-up / ramp-down process.

+

We use the pandas groupby operation to collect all decision +variables for each unit in separate series. Then, we iterate over units +to post constraints enforcing the ramp-up / ramp-down process by setting +upper bounds on the variation of the production level for consecutive +periods.

+
Model: ucp
+ - number of variables: 7680
+   - binary=3840, integer=0, continuous=3840
+ - number of constraints: 7700
+   - linear=7700
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+
Turn on / turn off
+

The following constraints determine when a unit is turned on or off.

+

We use the same pandas groupby operation as in the previous +constraint to iterate over the sequence of decision variables for each +unit.

+
Model: ucp
+ - number of variables: 7680
+   - binary=3840, integer=0, continuous=3840
+ - number of constraints: 11520
+   - linear=11520
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
+
+
Minimum uptime and downtime
+

When a unit is turned on, it cannot be turned off before a minimum +uptime. Conversely, when a unit is turned off, it cannot be turned on +again before a minimum downtime.

+

Again, let’s use the same pandas groupby operation to implement this +constraint for each unit.

+
+
+
Demand constraint
+

Total production level must be equal or higher than demand on any +period.

+

This time, the pandas operation groupby is performed on “periods” +since we have to iterate over the list of all units for each period.

+
+
+
+

Express the objective

+

Operating the different units incur different costs: fixed cost, +variable cost, startup cost, co2 cost.

+

In a first step, we define the objective as a non-weighted sum of all +these costs.

+

The following pandas join operation groups all the data to calculate +the objective in a single DataFrame.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
in_useturn_onturn_offproductionfixed_costvariable_coststart_costco2_cost
unitsperiods
coal11in_use_coal1_1turn_on_coal1_1turn_off_coal1_1p_coal1_1208.6122.536500030
2in_use_coal1_2turn_on_coal1_2turn_off_coal1_2p_coal1_2208.6122.536500030
3in_use_coal1_3turn_on_coal1_3turn_off_coal1_3p_coal1_3208.6122.536500030
4in_use_coal1_4turn_on_coal1_4turn_off_coal1_4p_coal1_4208.6122.536500030
5in_use_coal1_5turn_on_coal1_5turn_off_coal1_5p_coal1_5208.6122.536500030
+
+
+

Solve with Decision Optimization

+

If you’re using a Community Edition of CPLEX runtimes, depending on the +size of the problem, the solve stage may fail and will need a paying +subscription or product installation.

+
Model: ucp
+ - number of variables: 7680
+   - binary=3840, integer=0, continuous=3840
+ - number of constraints: 15455
+   - linear=15455
+ - parameters: defaults
+ - problem type is: MILP
+
+
+
* model ucp solved with objective = 14213082.064
+*  KPI: Total Fixed Cost    = 161025.131
+*  KPI: Total Variable Cost = 8865900.433
+*  KPI: Total Startup Cost  = 2832.000
+*  KPI: Total Economic Cost = 9029757.564
+*  KPI: Total CO2 Cost      = 5183324.500
+*  KPI: Total #used         = 1335.000
+*  KPI: Total #starts       = 3.000
+
+
+
+
+
+

Step 5: Investigate the solution and then run an example analysis

+

Now let’s store the results in a new pandas DataFrame.

+

For convenience, the different figures are organized into pivot tables +with periods as row index and units as columns. The pandas +unstack operation does this for us.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
unitscoal1coal2diesel1diesel2diesel3diesel4gas1gas2gas3gas4
periods
1425.0215.090.087.00.00.0109.452.0165.0115.6
2425.0365.090.087.00.00.078.071.0165.0158.0
3425.0312.090.087.00.00.00.052.0165.0158.0
4425.0234.090.087.00.00.00.052.0165.0158.0
5425.0365.090.087.00.00.00.0143.0165.0158.0
+

From these raw DataFrame results, we can compute derived results. For +example, for a given unit and period, the reserve r(u,t) is defined as +the unit’s maximum generation minus the current production.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
coal1coal2diesel1diesel2diesel3diesel4gas1gas2gas3gas4
10.0150.00.00.020.012.0110.6158.00.042.4
20.00.00.00.020.012.0142.0139.00.00.0
30.053.00.00.020.012.0220.0158.00.00.0
40.0131.00.00.020.012.0220.0158.00.00.0
50.00.00.00.020.012.0220.067.00.00.0
+

Let’s plot the evolution of the reserves for the “coal2” unit:

+
<matplotlib.axes._subplots.AxesSubplot at 0x23fd903bf60>
+
+
+_images/ucp_pandas_54_1.png +

Now we want to sum all unit reserves to compute the global spinning +reserve. We need to sum all columns of the DataFrame to get an +aggregated time series. We use the pandas sum method with axis=1 +(for rows).

+
<matplotlib.axes._subplots.AxesSubplot at 0x23fda312710>
+
+
+_images/ucp_pandas_56_1.png +
+

Number of plants online by period

+

The total number of plants online at each period t is the sum of in_use +variables for all units at this period. Again, we use the pandas sum +with axis=1 (for rows) to sum over all units.

+
<matplotlib.axes._subplots.AxesSubplot at 0x23fda37f9b0>
+
+
+_images/ucp_pandas_58_1.png +
+
+

Costs by period

+
<matplotlib.axes._subplots.AxesSubplot at 0x23fda3c7e80>
+
+
+_images/ucp_pandas_60_1.png +
+
+

Cost breakdown by unit and by energy

+
Text(0.5, 1.0, 'total cost by energy type')
+
+
+_images/ucp_pandas_62_1.png +
+
+
+

Arbitration between CO2 cost and economic cost

+

Economic cost and CO2 cost usually push in opposite directions. In the +above discussion, we have minimized the raw sum of economic cost and CO2 +cost, without weights. But how good could we be on CO2, regardless of +economic constraints? To know this, let’s solve again with CO2 cost as +the only objective.

+
* current CO2 cost is: 5183324.5
+* current $$$ cost is: 9029757.56390015
+
+
+
* absolute minimum for CO2 cost is 3399032.0
+* at this point $$$ cost is 12434227.620000225
+
+
+

As expected, we get a significantly lower CO2 cost when minimized alone, +at the price of a higher economic cost.

+

We could do a similar analysis for economic cost to estimate the +absolute minimum of the economic cost, regardless of CO2 cost.

+
* absolute minimum for $$$ cost is 8887433.859000005
+* at this point CO2 cost is 5375417.0
+
+
+

Again, the absolute minimum for economic cost is lower than the figure +we obtained in the original model where we minimized the sum of +economic and CO2 costs, but here we significantly increase the CO2.

+

But what happens in between these two extreme points?

+

To investigate, we will divide the interval of CO2 cost values in +smaller intervals, add an upper limit on CO2, and minimize economic cost +with this constraint. This will give us a Pareto optimal point with at +most this CO2 value.

+

To avoid adding many constraints, we add only one constraint with an +extra variable, and we change only the upper bound of this CO2 limit +variable between successive solves.

+

Then we iterate (with a fixed number of iterations) and collect the cost +values.

+
iteration #0 co2_ub=3399032.0
+iteration #1 co2_ub=3438559.7
+iteration #2 co2_ub=3478087.4
+iteration #3 co2_ub=3517615.1
+iteration #4 co2_ub=3557142.8
+iteration #5 co2_ub=3596670.5
+iteration #6 co2_ub=3636198.2
+iteration #7 co2_ub=3675725.9
+iteration #8 co2_ub=3715253.6
+iteration #9 co2_ub=3754781.3
+iteration #10 co2_ub=3794309.0
+iteration #11 co2_ub=3833836.7
+iteration #12 co2_ub=3873364.4
+iteration #13 co2_ub=3912892.1
+iteration #14 co2_ub=3952419.8
+iteration #15 co2_ub=3991947.5
+iteration #16 co2_ub=4031475.2
+iteration #17 co2_ub=4071002.9
+iteration #18 co2_ub=4110530.6
+iteration #19 co2_ub=4150058.3
+iteration #20 co2_ub=4189586.0
+iteration #21 co2_ub=4229113.7
+iteration #22 co2_ub=4268641.4
+iteration #23 co2_ub=4308169.1
+iteration #24 co2_ub=4347696.8
+iteration #25 co2_ub=4387224.5
+iteration #26 co2_ub=4426752.2
+iteration #27 co2_ub=4466279.9
+iteration #28 co2_ub=4505807.6
+iteration #29 co2_ub=4545335.3
+iteration #30 co2_ub=4584863.0
+iteration #31 co2_ub=4624390.7
+iteration #32 co2_ub=4663918.4
+iteration #33 co2_ub=4703446.1
+iteration #34 co2_ub=4742973.8
+iteration #35 co2_ub=4782501.5
+iteration #36 co2_ub=4822029.2
+iteration #37 co2_ub=4861556.9
+iteration #38 co2_ub=4901084.6
+iteration #39 co2_ub=4940612.3
+iteration #40 co2_ub=4980140.0
+iteration #41 co2_ub=5019667.7
+iteration #42 co2_ub=5059195.4
+iteration #43 co2_ub=5098723.1
+iteration #44 co2_ub=5138250.8
+iteration #45 co2_ub=5177778.5
+iteration #46 co2_ub=5217306.2
+iteration #47 co2_ub=5256833.9
+iteration #48 co2_ub=5296361.6
+iteration #49 co2_ub=5335889.3
+iteration #50 co2_ub=5375417.0
+
+
+_images/ucp_pandas_75_0.png +

This figure demonstrates that the result obtained in the initial model +clearly favored economic cost over CO2 cost: CO2 cost is well above 95% +of its maximum value.

+
+
+
+

Summary

+

You learned how to set up and use IBM Decision Optimization CPLEX +Modeling for Python to formulate a Mathematical Programming model and +solve it with IBM Decision Optimization on Cloud.

+ +

Copyright � 2017-2019 IBM. IPLA licensed Sample Materials.

+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp/warehouse.html b/docs/2.24.232/mp/warehouse.html new file mode 100644 index 0000000..d82afac --- /dev/null +++ b/docs/2.24.232/mp/warehouse.html @@ -0,0 +1,522 @@ + + + + + + + + + The Warehouse Problem — DOcplex.MP: Mathematical Programming Modeling for Python V2.24 documentation + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

The Warehouse Problem

+

The Warehouse Problem is a well-know optimization case found in many +textbooks. The problem consists, given a set of candidate warehouse +locations and a set of stores to decide which warehouse to open and +which warehouse will server which store.

+
+

Input Data

+

Data is provided as follows:

+
    +
  • For each warehouse, we require a tuple (name, capacity, fixed-cost), +where name is the unique name of the warehouse, capacity is the +maximum number of stores it can supply and fixed_cost is the cost +incurred by opening the warehouse.
  • +
  • For each couple (warehouse, store) a supply_cost which estimates +the cost of supplying this store by this warehouse.
  • +
+

A compact way of representing the data is a Python dictionary: for +each warehouse tuple, list all supply costs for all stores.

+
+
+

Business Decisions

+

The problem consists in deciding which warehouse will be open and for +each store, by which warehouse it will be supplied.

+
+
+

Business Constraints

+

Decisions must satisfy the following (simplified) business constraints:

+
    +
  1. Unicity: each store is supplied by one unique warehouse
  2. +
  3. A store can only be supplied by an open warehouse
  4. +
  5. Capacity: the number of stores supplied by a warehouse must be less +than its capacity
  6. +
+
+
+

Business Objective

+

The goal is to minimize the total cost incurred by the decisions, which +is made of two costs:

+
    +
  • The Total Opening Cost is the sum of opening costs ranging over all +open warehouses.
  • +
  • The Total Supply Cost is the sum of supply costs for all the chosen +(warehouse, store) pairs
  • +
+

To begin with, let’s define a small warehouse dataset:

+
WAREHOUSES = {
+    ('Bonn', 1, 20): [20, 28, 74, 2, 46, 42, 1, 10, 93, 47],
+    ('Bordeaux', 4, 35): [24, 27, 97, 55, 96, 22, 5, 73, 35, 65],
+    ('London', 2, 32): [11, 82, 71, 73, 59, 29, 73, 13, 63, 55],
+    ('Paris', 1, 37): [25, 83, 96, 69, 83, 67, 59, 43, 85, 71],
+    ('Rome', 2, 25): [30, 74, 70, 61, 54, 59, 56, 96, 46, 95],
+    ('Brussels', 2, 28): [30, 74, 70, 61, 34, 59, 56, 96, 46, 95]
+}
+
+print(WAREHOUSES)
+print("#candidate warehouses=%d" % len(WAREHOUSES))
+print("#stores to supply=%d" % len(list(WAREHOUSES.values())[0]))
+
+
+
{('London', 2, 32): [11, 82, 71, 73, 59, 29, 73, 13, 63, 55], ('Bordeaux', 4, 35): [24, 27, 97, 55, 96, 22, 5, 73, 35, 65], ('Paris', 1, 37): [25, 83, 96, 69, 83, 67, 59, 43, 85, 71], ('Bonn', 1, 20): [20, 28, 74, 2, 46, 42, 1, 10, 93, 47], ('Brussels', 2, 28): [30, 74, 70, 61, 34, 59, 56, 96, 46, 95], ('Rome', 2, 25): [30, 74, 70, 61, 54, 59, 56, 96, 46, 95]}
+#candidate warehouses=6
+#stores to supply=10
+
+
+

Each key in the dictionary is a tuple with three fields: the name (a +string), the capacity (a positive integer) and the fixed cost (also +positive). Each value is a list of length 10 (the number of stores to +supply). the k^th element of the list being the supply cost from the +warehouse to the k^th store. For example,

+
    +
  • London warehouse can supply at most two stores
  • +
  • London warehouse has an opening cost of 30
  • +
  • The cost incurred by supplying the first store from London is 11
  • +
+
+
+

The math model

+
+
+

Solving with DOcplex

+

Now let’s solve this problem with DOcplex. First, we define named tuples +to store information relative to warehouses and stores in a clear and +convenient manner.

+
from collections import namedtuple
+
+class TWareHouse(namedtuple("TWarehouse1", ["id", "capacity", "fixed_cost"])):
+    def __str__(self):
+        return self.id
+
+class TStore(namedtuple("TStore1", ["id"])):
+    def __str__(self):
+        return 'store_%d' % self.id
+
+
+

Second, we compute the set of warehouse and store tuples. The use of +itervalues from the six module is a technicality to ensure +portability across Python 2 and Python3. We chose to model stores by an +integer range from 1 to nb_stores for convenience.

+
from six import itervalues
+
+nb_stores = 0 if not WAREHOUSES else len(next(itervalues(WAREHOUSES)))
+warehouses = [TWareHouse(*wrow) for wrow in WAREHOUSES.keys()]
+# we'll use the warehouses dictionary as a two-entry dictionary
+supply_costs = WAREHOUSES
+# we count stores from 1 to NSTORES included for convenience
+stores = [TStore(idx) for idx in range(1, nb_stores + 1)]
+
+
+
+
+

The model

+

First we need one instance of model to store our modeling artefacts

+
try:
+    import docplex.mp
+except:
+    !pip install docplex
+
+
+
from docplex.mp.environment import Environment
+env = Environment()
+env.print_information()
+
+
+
* system is: Windows 64bit
+* Python is present, version is 2.7.11
+* docplex is present, version is (2, 7, 113)
+
+
+
from docplex.mp.model import Model
+warehouse_model = Model()
+
+
+
+

Defining decision variables

+

In DOcplex, decision variables are related to objects of the business +model. In our case, we have two decisions to make: which warehouse is +open and, for each store, from which warehouse is it supplied.

+

First, we create one binary (yes/no) decision variable for each +warehouse: the variable will be equal to 1 if and only if the warehouse +is open. We use the binary_var_dict method of the model object to +create a dictionary from warehouses to variables. The first argument +states that keys of the dictionary will be our warehouse objects; the +second argument is a a simple string *’open’, that is used to generate +names for variables, by suffixing ‘open’ with the string representation +of warehouse objects (in other terms the output of the str() Python +function. This is the reason why we redefined the method **str** +method of class TWarehouse in the beginning.

+
open_vars = warehouse_model.binary_var_dict(keys=warehouses, name='open')
+
+
+

Second, we define one binary variable for each pair or (warehouse, +store). This is done using the method binary_var_matrix; in this +case keys to variables are all (warehouse, store) pairs.

+

The naming scheme applies to both components of pairs,for example the +variable deciding whether the London warehouse supplies store 1 will be +named supply_London_store_1.

+
supply_vars = warehouse_model.binary_var_matrix(warehouses, stores, 'supply')
+
+
+

At this step, we can check how many variables we have defined. as we +have 5 warehouses and 10 stores, we expect to have defined 5*10 + 5 = +55 variables.

+
warehouse_model.print_information()
+
+
+
Model: docplex_model1
+ - number of variables: 66
+   - binary=66, integer=0, continuous=0
+ - number of constraints: 0
+ -   LE=0, EQ=0, GE=0, RNG=0
+ - parameters: defaults
+
+
+

As expected, we have not defined any constraints yet.

+
+
+
+

Defining constraints

+

The first constraint states that each store is supplied by exactly one +warehouse. In other terms, the sum of supply variables for a given +store, ranging over all warehouses, must be equal to 1. Printing model +information, we check that this code defines 10 constraints

+
for s in stores:
+    warehouse_model.add_constraint(warehouse_model.sum(supply_vars[w, s] for w in warehouses) == 1)
+warehouse_model.print_information()
+
+
+
Model: docplex_model1
+ - number of variables: 66
+   - binary=66, integer=0, continuous=0
+ - number of constraints: 10
+ -   LE=0, EQ=10, GE=0, RNG=0
+ - parameters: defaults
+
+
+

The second constraints states that a store can be supplied only by an +open warehouse. To model this, we use a little trick of logic, +converting this logical implication (w supplies s) => w is open into an +inequality between binary variables, as in:

+
for s in stores:
+    for w in warehouses:
+        warehouse_model.add_constraint(supply_vars[w, s] <= open_vars[w])
+
+
+

whenever a supply var from warehouse w equals one, its open variable +will also be equal to one. Conversely, when the open variable is zero, +all supply variable for this warehouse will be zero. This constraint +does not prevent having an open warehouse supplying no stores. Though +this could be taken care of by adding an extra constraint, this is not +necessary as such cases will be automatically eliminated by searching +for the minimal cost (see the objective section).

+

The third constraint states the capacity limitation on each warehouse. +Note the overloading of the logical operator <= to express the +constraint.

+
for w in warehouses:
+    warehouse_model.add_constraint(warehouse_model.sum(supply_vars[w, s] for s in stores) <= w.capacity)
+
+
+

At this point we can summarize the variables and constraint we have +defined in the model.

+
warehouse_model.print_information()
+
+
+
Model: docplex_model1
+ - number of variables: 66
+   - binary=66, integer=0, continuous=0
+ - number of constraints: 76
+ -   LE=66, EQ=10, GE=0, RNG=0
+ - parameters: defaults
+
+
+
+
+

Defining the Objective

+

The objective is to minimize the total costs. There are two costs:

+
    +
  • the opening cost which is incurred each time a warehouse is open
  • +
  • the supply cost which is incurred by the assignments of warehouses to +stores
  • +
+

We define two linear expressions to model these two costs and state that +our objective is to minimize the sum.

+
total_opening_cost = warehouse_model.sum(open_vars[w] * w.fixed_cost for w in warehouses)
+total_opening_cost.name = "Total Opening Cost"
+
+def _get_supply_cost(warehouse, store):
+    ''' A nested function to return the supply costs from a warehouse and a store'''
+    return supply_costs[warehouse][store.id - 1]
+
+total_supply_cost = warehouse_model.sum([supply_vars[w,s] * _get_supply_cost(w, s) for w in warehouses for s in stores])
+total_supply_cost.name = "Total Supply Cost"
+
+warehouse_model.minimize(total_opening_cost + total_supply_cost)
+
+
+

The model is now complete and we can solve it and get the final optimal +objective

+
+
+

Solving the model

+
ok = warehouse_model.solve()
+assert ok
+obj = warehouse_model.objective_value
+print("optimal objective is %g" % obj)
+
+
+
optimal objective is 433
+
+
+

But there’s more: we can precise the value of those two costs, by +evaluating the value of the two expressions. Here we see that the +opening cost is 140 and the supply cost is 293.

+
opening_cost_obj = total_opening_cost.solution_value
+supply_cost_obj = total_supply_cost.solution_value
+print("total opening cost=%g" % opening_cost_obj)
+print("total supply cost=%g" % supply_cost_obj)
+
+# store results for later on...
+results=[]
+results.append((opening_cost_obj, supply_cost_obj))
+
+
+
total opening cost=140
+total supply cost=293
+
+
+
+
+

Displaying results

+

We can leverage the graphic toolkit maptplotlib to display the +results. First, let’s draw a pie chart of opening cost vs. supply costs +to clearly see the respective impact of both costs.

+
if env.has_matplotlib:
+    import matplotlib.pyplot as plt
+    %matplotlib inline
+
+def display_costs(costs, labels, colors):
+    if env.has_matplotlib:
+        plt.axis("equal")
+        plt.pie(costs, labels=labels, colors=colors, autopct="%1.1f%%")
+        plt.show()
+    else:
+        print("warning: no display")
+
+display_costs(costs=[opening_cost_obj, supply_cost_obj],
+              labels=["Opening Costs", "Supply Costs"],
+              colors=["gold", "lightBlue"])
+
+
+_images/warehouse_38_0.png +

We can also display the breakdown of supply costs per warehouse. First, +compute the sum of supply cost values for each warehouse: we need to sum +the actual supply cost from w to s for those pairs (w, s) whose variable +is true (here we test the value to be >=0.9, testing equality to 1 would +not be robust because of numerical precision issues)

+
supply_cost_by_warehouse = [ sum(_get_supply_cost(w,s) for s in stores if supply_vars[w,s].solution_value >= 0.9) for w in warehouses]
+wh_labels = [w.id for w in warehouses]
+display_costs(supply_cost_by_warehouse, wh_labels, None)
+
+
+_images/warehouse_40_0.png +
+
+

Exploring the Pareto frontier

+

remember we solved the model by minimizing the sum of the two costs, +getting an optimal solution with total cost of 383, with opening costs = +120 and supply costs = 263.

+

In some cases, it might be interesting to wonder what is the absolute +minimum of any of these two costs: what if we’d like to minimize opening +costs first, and then the supply cost (of course keeping the +best opeing cost we obtained in first phase.

+

This is very easy to achieve with DOcplex, using the +“solve_lexicographic” method on the model. This method takes two +arguments: the first one is a list of expressions, the second one a list +of senses (Minimize or Maximize), the default being to Minimize each +expression.

+

In this section we will explore hat happens when we minimize opening +costs then supply costs and conversely when we minimize supply costs +and then opening costs.

+
opening_first_then_supply = [ total_opening_cost, total_supply_cost]
+warehouse_model.solve_lexicographic(goals=opening_first_then_supply)
+
+opening1 = total_opening_cost.solution_value
+supply1 = total_supply_cost.solution_value
+results.append( (opening1, supply1))
+
+
+
* lexicographic ok, #passes=2, results=[120, 352]
+   - Total Opening Cost =120.000
+   - Total Supply Cost =352.000
+
+
+

From this we can see that the model has successfully solved twice and +that the absolute minimum of opening cost is 120, and for this value, +the minimum supply cost is 263. Remember that in our first attempt we +minimized the combined sum and obtained (120,263, but now we know 120 is +the best we can achieve. The supply cost is now 352 , for a total +combined cost of 472 which is indeed greater than the value of 433 we +found when optimizing the combined sum.

+

Now let’s do the reverse: minimize supply cost and then opening cost. +What if the goal of minimizing total supply cost supersedes the second +goal of optimizing total opening cost? The code is straightforward:

+
supply_first_then_opening = [ total_supply_cost, total_opening_cost]
+warehouse_model.solve_lexicographic(goals=supply_first_then_opening)
+opening2 = total_opening_cost.solution_value
+supply2 = total_supply_cost.solution_value
+
+results.append( (opening2, supply2))
+
+
+
* lexicographic ok, #passes=2, results=[288, 152]
+   - Total Supply Cost =288.000
+   - Total Opening Cost =152.000
+
+
+

Here, we obtain (152, 288), supply costs is down from 352 to 288 but +opening cost raises from 120 to 150. We can check that the combined sum +is 288+152 = 440, which is grater than the optimal of 433 we found at +the beginning.

+
+

Pareto Diagram

+

We can plot these three points on a (opening, supply) plane:

+
def display_pareto(res):
+    if env.has_matplotlib:
+        plt.cla()
+        plt.xlabel('Opening Costs')
+        plt.ylabel('Supply Costs')
+        colors = ['g', 'r', 'b']
+        markers = ['o', '<', '>']
+        nb_res = len(res)
+        pts = []
+        for i, r in enumerate(res):
+            opening, supply = r
+            p = plt.scatter(opening, supply, color=colors[i%3], s=50+10*i, marker=markers[i%3])
+            pts.append(p)
+        plt.legend(pts,
+               ('Sum', 'Opening_first', 'Supply_first'),
+               scatterpoints=1,
+               loc='best',
+               ncol=3,
+               fontsize=8)
+        plt.show()
+    else:
+        print("Warning: no display")
+
+display_pareto(results)
+
+
+_images/warehouse_47_0.png +
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/mp_vs_cp.html b/docs/2.24.232/mp_vs_cp.html new file mode 100644 index 0000000..1d2fd85 --- /dev/null +++ b/docs/2.24.232/mp_vs_cp.html @@ -0,0 +1,170 @@ + + + + + + + + + Mathematical programming versus constraint programming — IBM® Decision Optimization CPLEX® Modeling for Python (DOcplex) V2.24 documentation + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

Mathematical programming versus constraint programming

+

Mathematical programming and constraint programming are two technologies critical to solving complex planning and scheduling problems.

+

At IBM®, we find that knowing both technologies is important in addressing some of the most difficult optimization problems.

+

To make the most of each, it is important to understand their similarities and differences.

+
+

General model structure

+
+
    +
  • A constraint programming optimization model has the same structure as a mathematical programming model: a set of decision variables, an objective function to maximize or minimize, and a set of constraints.
  • +
+
+
+
+

Modeling differences

+
+
    +
  • A constraint programming model natively supports logical constraints as well as a full range of arithmetic expressions, including modulo, integer division, minimum, maximum, and an expression which indexes an array of values by a decision variable.
  • +
  • A constraint programming model can also use specialized constraints, such as the “all-different” constraint, that can accelerate searches for frequently used patterns.
  • +
  • A constraint programming model has no limitation on the arithmetic constraints that can be set on decision variables, while a mathematical programming engine is specific to a class of problems whose formulation satisfies certain mathematical properties (for example: quadratic, MIQCP, and convex vs non-convex).
  • +
  • A constraint programming model supports only discrete decision variables (integer or Boolean) and activity and time-based variables, while a mathematical programming model supports either discrete or continuous decision variables.
  • +
+
+
+
+

Optimization engine differences

+
+
    +
  • A constraint programming engine makes decisions on variables and values and, after each decision, performs a set of logical inferences to reduce the available options for the remaining variables’ domains. In contrast, a mathematical programming engine, in the context of discrete optimization, uses a combination of relaxations (strengthened by cutting-planes) and “branch and bound”.
  • +
  • A constraint programming engine proves optimality by showing that no better solution than the current one can be found, while a mathematical programming engine will use different techniques such as +a lower bound proof provided by cuts and linear relaxation.
  • +
  • A constraint programming engine does not make assumptions on the mathematical properties of the solution space (convexity, linearity etc.), while a mathematical programming engine requires that the model falls in a well-defined mathematical category such as Mixed Integer Quadratic Programming (MIQP).
  • +
+
+
+
+

Mathematical programming / Constraint programming comparison table

+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 Mathematical programmingConstraint programming
Conflict detectionYesYes
RelaxationYes 
Lower bound and optimality gap measureYes 
Optimality proofYesYes
Modeler supportYesYes
Model-and-runYesYes
Modeling limitationsRestricted to linear and quadratic problemsRestricted to discrete problems
Specialized constraints Yes
Logical constraintsYesYes
Theoretical basisAlgebraLogical inferences
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/objects.inv b/docs/2.24.232/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..b894649472f18c2e2dd55df6182ae1c1b839b508 GIT binary patch literal 502 zcmY#Z2rkIT%&Sny%qvUHE6FdaR47X=D$dN$Q!wIERtPA{&q_@$u~P7K@;$Up!6h|0 z6D;RnP?DLOS(R7mrfbZ)DS7_gll9$iS4B+M<_NLBKJD-u&QFYU%$JBh;WcKODtfbeV?q?}W~|3$x$e**u3wVY38ny{RUemL{mr4FA36PF!Z-yO5jKVYltVbAt`v{P){?xnk~gUY&`jFSu$R-}&k8t{)A(YXeKqvA<3%uQa^8ZC|lk`8-~i zO?{Wb66+0YU%K3X>0+E^P^Xffb*ZcDs$s~VV+x6Xz4@02ymgkAyDB@O@}AFjj~fBE pTJIY+MrO?2Jnxuf)r+d*_vJOFsI=7D-=3Qjxs2~0!=lYxs{q!C+KB)F literal 0 HcmV?d00001 diff --git a/docs/2.24.232/search.html b/docs/2.24.232/search.html new file mode 100644 index 0000000..07732eb --- /dev/null +++ b/docs/2.24.232/search.html @@ -0,0 +1,96 @@ + + + + + + + + + Search — IBM® Decision Optimization CPLEX® Modeling for Python (DOcplex) V2.24 documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +

Search

+
+ +

+ Please activate JavaScript to enable the search + functionality. +

+
+

+ From here you can search these documents. Enter your search + words into the box below and click "search". Note that the search + function will automatically search for all of the words. Pages + containing fewer words won't appear in the result list. +

+
+ + + +
+ +
+ +
+ +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/2.24.232/searchindex.js b/docs/2.24.232/searchindex.js new file mode 100644 index 0000000..bb996e9 --- /dev/null +++ b/docs/2.24.232/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({docnames:["CHANGELOG","README.md","cp","getting_started","getting_started_python","index","mp","mp_vs_cp","support"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.todo":1,"sphinx.ext.viewcode":1,sphinx:55},filenames:["CHANGELOG.rst","README.md.rst","cp.rst","getting_started.rst","getting_started_python.rst","index.rst","mp.rst","mp_vs_cp.rst","support.rst"],objects:{},objnames:{},objtypes:{},terms:{"24x7":8,"5th":4,"boolean":[0,2,7],"break":2,"case":0,"class":[0,7],"default":[0,2,3],"export":0,"final":[2,6],"function":[0,6,7],"import":[0,6,7],"long":[0,2],"new":[0,4,5,6],"public":0,"return":0,"true":0,"try":0,"var":0,"while":[2,6,7],Added:0,Adding:0,BAS:0,COS:0,For:[0,2,3,4,5,6],LNS:2,MPS:0,One:2,SOS:0,That:0,The:[0,3,4,6],There:0,These:[1,2,6],Use:[3,4],Using:[0,6],Wes:4,With:[1,5],Yes:7,__bool__:0,__contains__:0,__str__:0,abc:0,abil:2,abl:0,abort:0,abort_search:0,about:[0,3,6],absmodel:0,absolut:0,abstol:0,abstractmodel:0,academ:[1,2,3,6],acceler:[2,7],accept:0,access:[0,1,3,4,8],accident:0,account:[1,3,4,5],accredit:[1,3],accuraci:0,act:5,action:5,activ:[2,7],actual:0,adapt:2,add:[0,1,5],add_constraint:0,add_cpo:0,add_ind:0,add_indicator_constraint:0,add_integer_var_solut:0,add_interval_var_solut:0,add_lazy_constraint:0,add_paramet:0,add_quadratic_constraint:0,add_user_cut_constraint:0,add_var:0,added:0,adding:0,addit:[0,8],additionnali:3,address:7,advanc:6,advantag:[5,8],advmodel:0,affect:6,after:[0,2,4,7],again:0,agent:0,aggreg:0,aggress:2,ago:6,aid:6,aix:[0,4],alex:4,algebra:[2,7],algorithm:[2,5],all:[0,2,7],all_diff:0,alloc:2,allow:[0,2,3,6],allowed_assign:0,alreadi:[3,4],also:[0,2,4,6,7],altern:4,alwai:0,always_const:0,always_equ:0,american:2,ampl:6,anaconda:[0,3,4],anacondacloud:3,analysi:[0,4],analyz:[4,5],angel:0,angelexcept:0,ani:[0,2],annot:0,anoth:0,anyth:4,apach:[1,5],api:[0,5],appli:[2,6],applic:[1,5,6,8],approach:2,apt:2,architectur:2,archiv:1,argument:[0,2],arithmet:[0,2,7],around:2,arrai:[0,2,7],artifici:2,ascend:0,aspect:0,assign:2,associ:[0,2,6],assumpt:7,attach:0,attribut:0,auto:0,auto_publish:0,autom:5,automat:[0,1,2,3,4],automaticreplai:0,autostoplisten:0,avail:[0,2,3,4,5,6,7,8],averag:0,avoid:0,awar:2,backtrack:2,baltimor:6,baptist:2,base:[4,5,6,7],basi:[0,6,7],basic:[2,6],basis_statu:0,batch:[0,2],bdyhhk:0,beazlei:4,becam:6,becaus:0,becom:6,beek:2,been:0,befor:3,beginn:4,behavior:0,behaviour:3,behind:6,being:0,belong:0,bender:0,benders_annot:0,benefit:[0,2],bernd:6,bertsima:6,best:2,better:[0,2,5,6,7],between:[0,2],beyond:5,big:4,biggest:6,bill:4,binari:0,bit:4,biz:[0,1],blackbox:0,blend:6,block:0,book:[2,4,5,6],both:[0,2,4,6,7],bound:[0,6,7],branch:7,breakdown:2,breakpoint:0,bressert:4,brian:4,bucket:2,budget:2,bug:[0,5,8],build:[0,2,6,8],built:[2,6],builtin:0,busi:[2,5,6],calendar:2,call:0,callback:[0,2],can:[0,1,2,3,4,5,6,7,8],cannot:2,capabl:[0,1,3],capac:2,captur:6,categori:[6,7],ceil:0,certain:[2,7],certifi:1,chain:6,challeng:[0,6],chang:[2,3,5,6],change_var_lower_bound:0,change_var_upper_bound:0,channel:[3,4],chardet:1,charg:3,check:[0,3,4],check_list:0,checker:0,children:6,choic:[2,6],choos:4,clarifi:0,clean:4,cleanup:0,cli:0,client:0,clone:0,cloud:[0,3,5],code:0,collect:0,colloquium:2,column:2,com:[0,1,3,4,5,8],combin:[2,6,7],combinatori:2,command:[0,3,4],comment:0,commerci:6,commit:6,common:[0,2],commun:[0,1,3],compat:[0,2],competit:5,compil:0,complet:[0,2],complex:[0,2,5,7],compliant:0,compon:6,compos:[1,5],comprehens:0,comput:[0,2,4,5,6],concept:[2,6],concern:0,conda:[0,5],condit:[0,5],cone:6,confer:4,config:0,configur:[0,3],conflict:[0,2,7],consequ:0,consid:6,consist:[2,6],constant:0,constrain:6,constraint:[0,3,4,6],construct:[0,2],constructor:0,contact:5,contain:[0,2],context:[0,7],contextu:0,continu:[6,7],continuum:[3,4],contrast:[2,7],control:5,conveni:2,convers:0,convert:0,convex:[6,7],coo:0,cookbook:4,coordinate__piecewise_linear:0,copi:0,correct:0,correctli:0,cos12:0,cost:[2,6,8],could:0,count:2,cours:4,cplex:[0,1,2,8],cplex_config:0,cplex_studio_dirxxx:[0,3],cplexcloud:0,cpo:[0,3],cpointvar:0,cpointvarsolut:0,cpomodel:0,cpomodelsolut:0,cpomodelstatist:0,cpoparamet:0,cpoptim:0,cporefineconflictresult:0,cposolv:0,cposolveresult:0,cposolverlisten:0,cpotupleset:0,crash:0,creat:[0,3,5,6],creation:0,credenti:0,criteria:2,critic:[7,8],csr:0,cts_by_nam:0,cumul:[0,2],cumulexpr:0,current:[0,4,5,7],custom:[0,2,5],customiz:0,cut:[0,6,7],dantzig:6,data:[0,4,5,6],datafram:0,date:2,david:4,dead:2,deal:2,debugg:4,dechter:2,decid:2,decis:[1,2,3,4,6,7,8],declar:[0,2],decomposit:0,deduct:2,defect:[0,8],defin:[0,2,7],definit:[2,6],delai:[0,2],delet:0,deliv:[1,6,8],deliveri:2,depend:[0,1,2,3,4,6],deploi:3,deploy:[3,6],deprec:0,dequ:0,describ:[0,2],design:6,desir:5,detail:[0,2,3,4,8],detect:7,determin:[2,6],determinist:0,dettime_limit:0,develop:[2,3,6,8],deviat:2,dictionari:0,did:0,differ:[0,2],difficult:[2,6,7],dimens:2,dimitri:6,direct:0,directli:0,directori:[0,4],disabl:0,discret:[0,2,6,7],disjunct:2,displai:0,distinguish:2,distribut:[3,4,6],divers:6,divis:7,doc:1,docloud:0,docloud_context:0,docplex2:0,docplex:[0,1,3],docplex_cli:0,docplex_cp_context:0,docplex_kei:0,docplex_url:0,docplexcloud:0,docplexrc:0,document:[0,1,3,4],doe:[0,4,7],dofeedback:[5,8],domain:[0,2,5,7],domain_contain:0,domain_iter:0,domain_max:0,domain_min:0,doubl:0,doug:4,down:6,download:[3,4],drive:0,drop:0,dual:0,due:2,durat:2,dure:2,dvar:0,dynam:[2,5],each:[0,2,6,7],earli:2,easi:[3,4],easier:2,easiest:4,easili:[0,1,2,5],econom:6,edit:[0,1,3,4],editor:4,effect:6,effici:[0,2,6,8],effort:0,either:7,element:0,eli:4,elimin:2,embrac:6,empti:0,enabl:[0,2,6],encod:0,end:[0,2],enforc:2,engin:[0,2,4,5],enhanc:0,enough:6,entir:6,enumer:2,environ:[0,5,8],equal:0,equival:0,error:0,essenc:6,essenti:4,etc:7,european:6,event:[0,5],everi:[2,6],everybodi:4,everywher:0,evolv:6,exampl:[0,1,2,3,5,7],exceed:2,except:0,exe:0,execut:0,exist:[0,3,4],experi:[3,4],experiment:0,expert:8,explain_failur:0,explan:[0,6],explicit:0,exploit:2,explor:2,export_as_mps_str:0,export_as_sav_str:0,export_model:0,express:[0,2,7],extend:0,extens:[0,2,6],extern:6,extract:4,facil:6,factor:0,factori:6,faculti:[1,3],fail:0,failur:0,fall:7,familiar:6,farm:6,fashion:2,fast:[0,5],faster:0,fatal:0,feasibl:[0,2],featur:[0,2,4,6],feder:6,field:6,file:0,filter:0,find:[2,3,4,7],finit:2,first:[0,2,3,4,6],fix:[0,6,8],flat:0,flatzinc:0,flexibl:[2,6],floor:0,flow:6,follow:[3,4,6],food:6,forbidden_assign:0,forbiden_assign:0,form:0,format:0,formid:1,formul:[2,6,7],found:[0,2,3,7],foundat:[2,6],four:[0,6],framework:6,francais:6,francesca:2,fraud:5,free:[0,1,2,3,4,5,6],freeli:0,frequent:7,friendli:[1,5],from:[0,2,3,4,5,6,8],full:[0,3,7],fulli:0,fundament:[2,6],further:2,futur:[0,1,5],fzn:0,gain:[0,5,8],gap:[0,7],gartner:6,gather:4,gave:6,gener:[0,2,6],genet:2,georg:6,get:[0,1],get_cpo_str:0,get_express:0,get_fail_statu:0,get_named_expressions_dict:0,get_objective_bound:0,get_objective_gap:0,get_objective_valu:0,get_paramet:0,get_search_statu:0,get_solver_verion:0,get_stop_callback:0,get_stop_caus:0,get_valu:0,get_value_dict:0,get_version_info:0,getter:0,github:[1,4],give:8,given:[0,2],global:[0,3,4],goal:[2,5],goe:6,going:0,good:4,gorelick:4,govern:0,graph:2,greater:[0,5],group:2,gru:4,guid:4,gzip:0,hand:2,handbook:2,handl:[0,5,6],hard:[4,6],harder:6,has:[0,2,6,7],have:[0,3,4,5,6],hellmann:4,help:[0,4,5,6,8],hentenryck:2,here:[3,4,5,8],heurist:2,high:[4,6],higher:3,highest:6,home:[0,3,4],hook:0,host:[3,4],hour:2,how:[0,6],howev:0,http:[0,1,3,4],human:4,ibm:[0,1,2,6,7],ibmdecisionoptim:[1,3,4],ident:0,identifi:[0,2],idna:1,idri:4,if_then:0,ignor:0,ilog:[1,2,5,6],implement:0,import_model:0,import_solut:0,impress:6,improv:[0,6],includ:[0,2,3,4,6,7],incompat:[0,2],incorrectli:0,increas:[5,6],index:[0,7],indic:0,individu:[0,2],industri:6,infeas:0,infer:[0,7],info:0,inform:[2,3,6],initi:[0,1,3],innov:6,insight:5,inspect:3,instal:[0,1,6],instanc:[2,3,4],instead:0,institut:[1,3,6],integ:[0,2,7],integerdomain:0,intellig:2,interact:[0,4,5,6],interest:6,interfac:0,intermedi:0,intern:[2,6],interpret:[0,4],interrupt:2,interv:[0,2],intract:6,introduc:4,introduct:[4,6],invalu:2,invent:6,inventori:2,invert:0,involv:[4,6],ipynb:0,ipython:4,is_valid_solut:0,issu:[0,6,8],item:2,iter:0,its:[2,6],itself:6,ivan:4,januari:1,jiri:6,job:[0,2],jobsolvestatu:0,joel:4,john:6,join:3,jone:4,json:0,just:0,kaufmann:2,keep:[0,4],kei:0,keyword:0,kill:0,know:7,knowledg:5,known:0,kpi:0,kpi_value_by_nam:0,kruk:2,krzysztof:2,languag:[2,6],larg:[0,2,3,6],last:0,later:[1,3],latest:[0,1,3],lazi:0,ldquo:2,lead:6,leader:6,learn:[0,3,4,6],lectur:[2,6],len:0,length:0,length_for_alia:0,length_for_renam:0,level:[0,2,5,6],leverag:6,lexic:2,lexicograph:[0,2],librari:[0,1,2,5,6],licens:[1,5],life:[0,2],lifespan:4,limit:[0,1,2,3,5,6,7],line:0,linear:[0,2,5,7],linearrelax:0,link:0,linux:[3,4],list:[0,3,4,5],listen:0,local:[0,3],localsolverexcept:0,locat:[0,6],log:0,log_output:0,logic:[0,2,7],logical_and:0,logical_or:0,logsearchtag:0,longer:0,look:[3,4],loop:0,lower:7,loyalti:5,lubanov:4,lutz:4,mac:4,machin:[0,1,2,3],made:6,magic:0,magnitud:6,mai:[0,2,4],mainten:2,major:2,make:[0,2,5,6,7],manag:[0,2,5,6,8],mani:[2,3,4,6],manner:6,manpow:6,manual:5,manufactur:6,map_solut:0,mark:4,market:6,martelli:4,match:[0,2,4],materi:6,math:4,mathemat:[2,3,4],mathematician:6,matousek:6,matrix:0,max:0,max_of:0,maxim:[0,2,6,7],maximum:[0,7],mckinnei:4,mean:6,mean_lb:0,mean_ub:0,measur:7,mechan:[0,2],media:5,meet:[2,4,5],member:[0,1,3],messag:0,meta:0,method:[0,6],micha:4,michigan:4,microsoft:4,might:[4,6],million:6,millisecond:2,min:0,min_of:0,minim:[0,2,6,7],minimum:7,minizinc:0,minor:0,minut:2,mip:0,miqcp:7,miqp:7,mis:0,mission:8,mit:6,mitig:5,mix:[2,7],model:[0,1,3,6,8],model_object:0,model_paramet:0,modelanonym:0,modelread:0,modern:4,modifi:0,modul:[0,1,5],modulo:7,monthli:6,more:[0,3,8],morgan:2,most:[2,4,6,7],move:[0,2],mst:0,much:[0,6],multi:[0,2],multipl:[0,6],multipli:0,must:[0,2,3,4],mzn:0,name:[0,6],namespac:1,nan:0,nativ:[0,7],need:[0,1,2,3,4,5,6,8],neighborhood:[0,2],nest:0,network:6,next:[0,5],no_overlap:0,node:0,non:[0,2,7],nonconvex:0,none:0,notebook:[0,3,4],notifi:0,novel:6,now:0,nuijten:2,number:[0,2],numer:[0,2],numpi:[0,1,4,5],nutshel:[4,5],object:[0,2,6,7],occur:0,off:[0,5,6],offer:[0,8],offici:0,one:[0,2,4,7],onli:[0,7],onlin:[2,4,5,6,8],open:[6,8],oper:[0,2,4,5,6],operationel:6,opportun:5,optim:[0,1,4,8],optimis:6,option:[0,2,4,7],ord:0,order:[0,2,3,6],org:[1,4],organ:[3,5],osx:3,other:[0,2,6],otherwis:0,out:2,outcom:5,over:[0,2,4,6],overhaul:0,overlap:2,overli:6,overload:0,overrid:3,overview:[4,5],overwrit:0,pack:2,packag:[0,8],panda:[0,4,5],pape:2,parallel:[2,6],paramet:[0,2],parenthes:0,pars:0,parser:0,part:[0,4],parti:1,particular:[0,6],particularli:6,pascal:2,pass:0,past:5,path:5,pattern:7,peopl:2,perform:[0,2,4,6,7],perhap:2,period:2,personnel:6,peter:2,phase:0,phone:8,pickl:0,piecewis:0,pip:[0,1],plan:[6,7],plane:7,plant:6,platform:[3,4],playlist:6,pleas:[5,8],plinux:[0,4],png:0,pngfile:0,point:0,polici:2,ponder:6,pool:0,popular:4,populate_solution_pool:0,port:0,possibl:[0,2,6],power:[1,5,6],ppc64le:[3,4],practic:[4,6],preced:2,precis:0,predict:5,preemptiv:2,prefer:0,prepar:0,presenc:2,present:[5,6],preserv:0,prevent:[0,5],previou:[0,5],previous:[0,6],primarili:2,princeton:6,principl:[2,4],print:0,prioriti:0,privat:0,proactiv:5,problem:[0,1,2,5,7],proce:2,process:[0,2,5,8],product:[0,2,3,6],profession:[1,2,3,6],profit:6,program:[3,4],progress:0,progressdata:0,project:4,proof:7,propag:[0,2],proper:0,properli:0,properti:[0,2,7],proport:6,prove:7,provid:[0,3,4,6,7],proxi:0,publish:0,pun:6,pure:2,put:0,puzzl:6,puzzlor:6,pwl:0,pycharm:4,pydev:4,pypi:[1,3,4,5],python2:0,python:[0,1,3,8],qiskit:0,quadrat:[0,6,7],quadratic_dual_slack:0,qualifi:3,quantifi:6,quantiti:6,quickli:[1,2,5],rais:0,random:0,rang:[0,2,7],rapidli:2,rate:6,raw:0,rawgit:0,rdquo:2,reach:2,read:0,readm:[3,4],real:[0,2,6],realloc:0,receiv:0,recherch:6,recommend:5,reduc:[0,2,6,7],reduced_cost:0,reduct:2,redund:2,refer:[0,3,4,5,6],refin:0,refine_conflict:0,refineri:6,reformul:2,reg:[1,2,3,4,6,7],regardless:2,rel:0,relat:[0,2],relationship:[2,6],relax:[0,6,7],relax_linear:0,releas:[0,6],relev:2,reliabl:[6,8],reltol:0,remain:7,remov:[0,2],remove_express:0,renam:0,repeat:0,replac:0,report:[0,5,8],report_kpi:0,repres:[0,2,6,8],represent:0,request:1,requir:[0,1,2,6,7],research:[1,3,6],reservoir:2,reset:0,resourc:5,respect:[0,2],restart:0,restrict:[2,6,7],result:[0,2,6],retriev:0,revolution:6,revolv:2,rework:0,rhs:0,rice:4,rina:2,risk:5,robert:6,robust:6,rossi:2,round:0,rout:6,rshift:0,run:[0,3,6,7],run_se:0,runtim:[0,3],sale:8,same:[0,7],same_common_subsequ:0,sampl:[0,4],satisfact:2,satisfi:[2,7],sav:0,save:0,scal_prod:0,scale:[0,2],scenario:5,sched_jobshop_blackbox:0,sched_rcpspmm_json:0,schedul:[0,6,7],scheduling_tuto:0,scienc:[2,4,6],scientif:4,scikit:0,scipi:[0,4],scratch:4,screen:0,script:0,search:[0,2,3,6,7],search_next:0,searchphas:0,searchstatu:0,searchstopcaus:0,searchtyp:0,second:[0,6],section:[0,6,8],see:[0,1,3,4],seed:0,seem:6,select:3,send:0,sensit:0,separ:0,sequenc:[0,2],seri:[2,4],serv:6,servic:[0,2,3,4,5,8],set:[0,2,5,7],set_lex_multiobj:0,set_lp_start_basi:0,set_multi_object:0,set_paramet:0,set_stop_callback:0,setup:2,sever:[0,2,8],sgn:0,shadow:0,share:0,shortcut:0,shot:0,should:0,show:[0,7],side:0,sign:[3,4],signific:0,signup:1,similar:7,similarli:0,simpl:[0,4,6],simplex:6,simpli:0,simplifi:0,sinc:[0,2],singl:0,site:4,situat:[5,6],six:1,size:[0,1,3],skip:0,slack:0,slope:0,slope_piecewise_linear:0,social:5,societ:6,societi:[2,6],softwar:[3,6,8],sol:0,solurt:0,solut:[0,2,6,7],solv:[0,1,2,3,5,7],solve_lexicograph:0,solve_statu:0,solve_with_go:0,solve_with_search_next:0,solve_with_start_next:0,solvelisten:0,solver:[0,3,6],solver_angel:0,solver_listen:0,solver_loc:0,solverangel:0,solveresult:0,solverloc:0,solverprogresspanellisten:0,solvesolut:0,some:[0,2,6,7],sometim:6,sophist:6,sort:0,sort_nam:0,sourc:[0,5,8],space:[0,2,7],special:[0,2,7],specif:[0,5,7],specifi:[0,2,3,4],speed:6,split:0,springer:6,ssl:4,standard:[0,2,3,4],standard_devi:0,start:[0,2,3],start_search:0,startingpoint:0,state:[0,2],state_funct:0,statement:0,statist:0,statu:[0,2],step:[0,5],still:0,stop:0,store:0,strategi:2,strengthen:7,strict_lexicograph:0,string:0,strong:5,structur:2,student:1,studio:[0,1,5],stuff:0,sub:0,sub_circuit:0,submiss:8,submit:0,substitut:0,subtract:0,suggest:5,sum:0,superior:5,suppli:6,support:[0,4,5,6,7],symmetri:2,synergi:5,system:3,systemat:[2,6],tabl:6,tag:0,take:[0,5],tardi:2,task:2,team:8,technic:8,techniqu:[2,7],technolog:[6,7],tempor:2,temporari:0,temporarili:0,term:1,termin:0,test:[2,6],than:[6,7],thar:0,thei:[0,2,3,6],them:[0,5,6],theoret:[2,7],theori:2,therefor:2,thi:[0,1,2,3,5,6,8],third:1,threat:5,through:[1,2,5,8],throughout:2,thrown:0,thu:6,time:[0,2,6,7],time_limit:0,timelimit:0,timeout:0,tobi:2,todai:6,toler:0,too:0,tool:[3,8],topic:5,total:2,track:0,trade:5,train:[2,4,5,6],transform:0,transit:[0,2],transport:6,travers:2,tree:2,triag:8,trunc:0,truth:0,tsitsikli:6,tune:2,tupe:0,tupl:0,tuple_set:0,tupleset:0,turn:[2,4],tutori:[0,5,6],two:[0,7],txt:1,type:[0,2],typeerror:0,typic:2,ultim:3,uncertain:5,under:[1,5],undergon:0,understand:[6,7],unexpect:0,unicod:0,uninstal:[3,4],uniqu:[0,2],univers:[2,4,6],unknown:0,unless:0,unlimit:1,unpreced:6,until:2,unus:0,updat:[0,3,4,5],upgrad:[0,3,4],url:0,urllib3:1,urx:1,usag:0,use:[0,2,3,4,6,7],used:[0,2,3,4,7],user:0,uses:[0,6,7],using:[0,2,3,6,8],util:0,v12:[1,3],valid:0,valu:[0,2,5,6,7],valueerror:0,van:2,vanderbei:6,variabl:[0,2,3,6,7],variant:0,variou:[2,5,6],vehicl:6,veri:0,verifi:[0,4],version:[0,1,3,4,5],versu:5,via:[0,3,4],video:[2,6],violat:6,virtual:4,visu:0,visual:4,vnet:[5,8],wai:[2,3,4,5],walsh:2,want:4,warehous:6,warn:0,watson:[0,3],weight:2,welcom:[1,5],well:[6,7],were:[0,6],what:[0,6],when:[0,2,3,4,6],where:[0,3],whether:[2,6],which:[0,2,4,5,6,7],whole:0,whose:7,wilei:6,william:6,window:[3,4],without:0,work:[0,2],workabl:2,worker:0,workflow:0,world:[2,6],wrangl:4,write:0,written:[3,6],wrong:0,wrongli:0,wrote:0,wwpdl:[5,8],www:[1,3,4],x86:3,xxx:3,year:6,you:[0,1,2,3,4,5,6,8],your:[0,1,4,8],yourself:6,youtub:[2,6],zeppelin:0,zero:0,zip:0},titles:["Changelog","README.md","Overview of constraint programming","Setting up an optimization engine","Getting started with docplex","IBM® Decision Optimization CPLEX® Modeling for Python","Overview of mathematical programming","Mathematical programming versus constraint programming","IBM\u00ae Elite Support for DOcplex"],titleterms:{The:2,Using:3,aka:5,algorithm:6,analyt:5,base:2,chang:0,changelog:[0,5],cloud:4,comparison:7,comprehens:8,comput:3,conda:[3,4],connect:5,constraint:[2,5,7],contact:8,cplex:[3,4,5,6],decis:5,develop:[4,5],differ:7,discov:5,docplex:[4,5,8],eclips:8,elit:8,engin:[3,7],environ:4,essenti:6,exampl:[4,6],feedback:5,gener:7,get:[3,4,5],give:5,ibm:[3,4,5,8],ilog:3,instal:[3,4],integ:6,librari:[3,4],linear:6,manag:4,mathemat:[5,6,7],mix:6,model:[2,4,5,7],more:[2,4,6],optim:[2,3,5,6,7],overview:[2,6],packag:[3,4],pip:[3,4],platform:8,portal:8,power:2,prescript:5,problem:6,program:[2,5,6,7],provid:8,python:[4,5],readm:1,reg:5,requir:4,resourc:[2,4,6],run:2,schedul:2,set:[3,4],setup:3,solv:[4,6],start:[4,5],structur:7,studio:[3,4],support:8,system:4,tabl:7,techniqu:6,technolog:[2,5],tool:4,using:[4,5],versu:7,watson:4,what:[2,5],without:[2,6],your:[3,5]}}) \ No newline at end of file diff --git a/docs/2.24.232/support.html b/docs/2.24.232/support.html new file mode 100644 index 0000000..61574e8 --- /dev/null +++ b/docs/2.24.232/support.html @@ -0,0 +1,105 @@ + + + + + + + + + IBM® Elite Support for DOcplex — IBM® Decision Optimization CPLEX® Modeling for Python (DOcplex) V2.24 documentation + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+

IBM® Elite Support for DOcplex

+
+

Comprehensive support for the Eclipse platform provided by IBM

+

IBM® Elite Support for Decision Optimization CPLEX Modeling for Python is a technical support package for DOcplex, an open source development environment. +This offering gives you the advantage of using open software with reliable expert support from IBM. +You gain the support you need to build mission-critical applications with this Python modeling package.

+
+
This Elite Support provides:
+
    +
  • Support for the DOcplex packages.
  • +
  • Phone support at no additional cost. IBM support services are available through IBM online issue management tools or by phone, and 24x7 phone support is available for severity 1 issues.
  • +
  • An efficient defect fix process. Defect submissions go through a triage process, and fixes are delivered by the IBM development team.
  • +
+
+
+

Contact your sales representative for more details or contact IBM here in the “Contact IBM” section.

+
+
+

Support portal

+

You can access the Decision Optimization support portal here.

+
+
+

Contacts

+ +
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/CHANGELOG.html b/docs/CHANGELOG.html index 303a3bf..a9122a3 100644 --- a/docs/CHANGELOG.html +++ b/docs/CHANGELOG.html @@ -37,7 +37,8 @@

Navigation

Table of Contents