├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE.md ├── README.md ├── asl_io ├── asl_io.ipynb ├── read.py ├── script.py └── write.py ├── diet ├── DietProblem.ipynb ├── diet.dat ├── diet.py └── results.yml ├── maxflow ├── maxflow.dat ├── maxflow.ipynb ├── maxflow.py └── results.yml ├── network_interdiction ├── max_flow │ ├── max_flow_interdict.ipynb │ ├── max_flow_interdict.py │ ├── sample_arcs_data.csv │ └── sample_nodes_data.csv ├── min_cost_flow │ ├── min_cost_flow_interdict.py │ ├── sample_arcs_data.csv │ └── sample_nodes_data.csv ├── multi_commodity_flow │ ├── multi_commodity_flow_interdict.ipynb │ ├── multi_commodity_flow_interdict.py │ ├── sample_arcs_commodity_data.csv │ ├── sample_arcs_data.csv │ ├── sample_nodes_commodity_data.csv │ └── sample_nodes_data.csv └── shortest_path │ ├── sample_arcs_data.csv │ ├── sample_nodes_data.csv │ ├── sp_interdict.ipynb │ └── sp_interdict.py ├── p_median ├── p-median.dat ├── p-median.py ├── p_median.ipynb └── results.yml ├── pandas_min_cost_flow ├── arcs.csv ├── min_cost_flow.ipynb ├── min_cost_flow.py └── nodes.csv ├── row_generation_mst ├── mst.csv ├── mst.ipynb └── mst.py ├── test_notebooks.py └── transport ├── results.yml ├── transport.ipynb └── transport.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug report" 3 | about: Report a bug in Pyomo (command not working as expected, etc.) 4 | labels: "bug" 5 | --- 6 | 7 | 8 | 9 | ## Summary 10 | 11 | 13 | 14 | ### Steps to reproduce the issue 15 | 16 | 17 | 18 | ```console 19 | $ command1 [options] 20 | $ command2 [options] 21 | ... 22 | ``` 23 | 24 | ``` 25 | # example.py 26 | import pyomo.environ 27 | ... 28 | ``` 29 | 30 | ### Error Message 31 | 32 | 33 | 34 | 35 | ```console 36 | $ # Output message here, including entire stack trace, if available 37 | ``` 38 | 39 | ### Information on your system 40 | 41 | Pyomo version: 42 | Python version: 43 | Operating system: 44 | How Pyomo was installed (PyPI, conda, source): 45 | Solver (if applicable): 46 | 47 | 48 | ### Additional information 49 | 50 | 51 | 52 | 53 | 56 | 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Get help/Ask a question 5 | url: https://github.com/Pyomo/pyomo#getting-help 6 | about: Have a question? Need some help from the community? Refer to our online documentation for ways to get help. 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Enhancement/Feature request" 3 | about: Suggest adding an enhancement of a current feature or a new feature in Pyomo 4 | labels: enhancement 5 | 6 | --- 7 | 8 | 9 | 10 | 11 | 12 | ## Summary 13 | 14 | 15 | 16 | ### Rationale 17 | 18 | 19 | 20 | ### Description 21 | 22 | 23 | 24 | 25 | ### Additional information 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ## Fixes # . 13 | 14 | ## Summary/Motivation: 15 | 16 | 17 | ## Changes proposed in this PR: 18 | - 19 | - 20 | 21 | ### Legal Acknowledgement 22 | 23 | By contributing to this software project, I have read the [contribution guide](https://pyomo.readthedocs.io/en/stable/contribution_guide.html) and agree to the following terms and conditions for my contribution: 24 | 25 | 1. I agree my contributions are submitted under the BSD license. 26 | 2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer. 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # temporary editor files 2 | *~ 3 | .#* 4 | \#*# 5 | 6 | # IDE configuration files 7 | .idea 8 | .spyder* 9 | .ropeproject 10 | .vscode 11 | 12 | # Python generates numerous files when byte compiling / installing packages 13 | __pycache__/ 14 | *.pyx 15 | *.py[cod] 16 | *.egg-info/ 17 | 18 | # Documentation builds 19 | doc/OnlineDocs/_build 20 | doc/OnlineDocs/**/*.spy 21 | 22 | # Running pyomo / tests / solvers occasionally leaves extra files 23 | *.out 24 | pyomo/dataportal/parse_table_datacmds.py 25 | gurobi.log 26 | cplex.log 27 | 28 | # Results from pytest --with-coverage 29 | .coverage 30 | *.cover 31 | 32 | # Jupyterhub/Jupyterlab checkpoints 33 | .ipynb_checkpoints 34 | 35 | # Mac tracking files 36 | *.DS_Store* 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | LICENSE 2 | ======= 3 | 4 | Copyright (c) 2015-2025 National Technology and Engineering Solutions of 5 | Sandia, LLC . Under the terms of Contract DE-NA0003525 with National 6 | Technology and Engineering Solutions of Sandia, LLC , the U.S. 7 | Government retains certain rights in this software. 8 | 9 | All rights reserved. 10 | 11 | Redistribution and use in source and binary forms, with or without 12 | modification, are permitted provided that the following conditions 13 | are met: 14 | 15 | * Redistributions of source code must retain the above copyright notice, 16 | this list of conditions and the following disclaimer. 17 | 18 | * Redistributions in binary form must reproduce the above copyright 19 | notice, this list of conditions and the following disclaimer in the 20 | documentation and/or other materials provided with the distribution. 21 | 22 | * Neither the name of the Sandia National Laboratories nor the names of 23 | its contributors may be used to endorse or promote products derived from 24 | this software without specific prior written permission. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 27 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 28 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 29 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 30 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 31 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 32 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 33 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 34 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 35 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 36 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | 38 | 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Project Status: Inactive – The project has reached a stable, usable state but is no longer being actively developed; support/maintenance will be provided as time allows.](https://www.repostatus.org/badges/latest/inactive.svg)](https://www.repostatus.org/#inactive) 2 | 3 | # Pyomo Gallery 4 | A collection of Pyomo examples 5 | 6 | #### For Users 7 | 8 | This project supports a collection of Pyomo models and scripting examples. [See the wiki for the list of examples.](https://github.com/Pyomo/PyomoGallery/wiki) 9 | 10 | The Pyomo Gallery is available under the BSD License. 11 | 12 | #### For Contributors 13 | 14 | We encourage contributions to the Pyomo Gallery from all Pyomo users and developers. Each example in the gallery is stored in a separate subdirectory, and a Jupyter notebook is used to describe the example. Existing examples illustrate the expected level of detail, but feel free to structure your example in a different manner as appropriate. 15 | 16 | By contributing to this software project, you are agreeing to the following terms and conditions for your contributions: 17 | 18 | 1. You agree your contributions are submitted under the BSD license. 19 | 2. You represent you are authorized to make the contributions and grant the license. If your employer has rights to intellectual property that includes your contributions, you represent that you have received permission to make contributions and grant the required license on behalf of that employer. 20 | -------------------------------------------------------------------------------- /asl_io/asl_io.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# ___________________________________________________________________________\n#\n# Pyomo: Python Optimization Modeling Objects\n# Copyright (c) 2015-2025\n# National Technology and Engineering Solutions of Sandia, LLC\n# Under the terms of Contract DE-NA0003525 with National Technology and\n# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain\n# rights in this software.\n# This software is distributed under the 3-clause BSD License.\n# ___________________________________________________________________________" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# Loading ASL Results into a Model\n", 17 | "\n", 18 | "## Summary\n", 19 | "\n", 20 | "In this scripting example we break apart the work flow that occurs when a Pyomo model is solved using the ASL solver plugin. The ASL solver plugin is a generic interface designed for solvers that utilize the AMPL Solver Library. This library takes model input in the form of an NL file and provides a solver solution in the form of an SOL file. As such, it provides a single unifying framework for interacting with a wide array of optimization solvers.\n", 21 | "\n", 22 | "Pyomo includes separate tools for writing NL files and reading SOL files. In this example, we will show how to use these tools directly, as an alternative to calling the ASL solver plugin. In particular, we show how to save information about the symbol map created by the NL writer to a file so that it can be recovered at a later time. The symbol map that is recovered can be used to load a solution from the SOL file reader into any Pyomo model with component names that match those on the model used by the NL writer.\n", 23 | "\n", 24 | "## Solving With ASL\n", 25 | "\n", 26 | "Consider the case below where we solve a simple Pyomo model using Ipopt through the ASL solver plugin and then verify that the solver termination condition is optimal before loading the solution into the model. Note that this example assumes Pyomo version 4.1 or later is installed. Since Pyomo 4.1, the **_load_\\__solutions_** keyword must be assigned a value of _False_ when calling the _solve_ method on a solver plugin in order to prevent the solution from being automatically loaded into the model. This allows us to check the solver termination condition before manually loading the solution via the call to _model.solutions.load_\\__from_." 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 1, 32 | "metadata": {}, 33 | "outputs": [ 34 | { 35 | "name": "stdout", 36 | "output_type": "stream", 37 | "text": [ 38 | "Objective: 0.9999999925059035\n" 39 | ] 40 | } 41 | ], 42 | "source": [ 43 | "# %load script.py\n", 44 | "from pyomo.environ import *\n", 45 | "from pyomo.opt import SolverFactory, TerminationCondition\n", 46 | "\n", 47 | "def create_model():\n", 48 | " model = ConcreteModel()\n", 49 | " model.x = Var()\n", 50 | " model.o = Objective(expr=model.x)\n", 51 | " model.c = Constraint(expr=model.x >= 1)\n", 52 | " model.x.set_value(1.0)\n", 53 | " return model\n", 54 | "\n", 55 | "if __name__ == \"__main__\":\n", 56 | "\n", 57 | " with SolverFactory(\"ipopt\") as opt:\n", 58 | " model = create_model()\n", 59 | " results = opt.solve(model, load_solutions=False)\n", 60 | " if results.solver.termination_condition != TerminationCondition.optimal:\n", 61 | " raise RuntimeError('Solver did not report optimality:\\n%s'\n", 62 | " % (results.solver))\n", 63 | " model.solutions.load_from(results)\n", 64 | " print(\"Objective: %s\" % (model.o()))\n" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "The basic work flow that takes place above can be summarized as:\n", 72 | " 1. Create an ASL solver plugin that uses the _ipopt_ executable appearing in the shell search PATH.\n", 73 | " 2. Construct a Pyomo model.\n", 74 | " 3. Solve the Pyomo model.\n", 75 | " 1. Output the Pyomo model as an NL file.\n", 76 | " 2. Invoke the solver (which produces an SOL file).\n", 77 | " 3. Read the SOL file into a Pyomo results object.\n", 78 | " 4. Check the solver termination condition stored in the results object.\n", 79 | " 5. Load the solution stored in the results object into the Pyomo model.\n", 80 | "\n", 81 | "The remainder of this example shows how to implement step 3 without the use of the ASL solver plugin.\n", 82 | "\n", 83 | "### A note about using the **_with_** statement\n", 84 | "\n", 85 | "In the code provided with this example we make use of Python's **_with_** statement when dealing with objects returned from Pyomo _Factory_ functions such as SolverFactory and ReaderFactory. Pyomo makes use of a Plugin system to instantiate these objects. As a result, they must be deactivated before going out of scope in order to prevent a memory leak. Deactivation of Plugins is managed automatically by the **_with_** statement, but can also be done by calling the _deactivate_ method directly on the Plugin object.\n", 86 | "\n", 87 | "## Writing the NL File\n", 88 | "\n", 89 | "The code block below defines the function **_write_\\__nl_** that outputs a Pyomo model as an NL file and saves the pertinent symbol map data to a file using pickle. This symbol map data will allow a solution stored in an SOL file to be loaded into any Pyomo model with matching component names. The last section of this code block shows how this function can be used with a small example model." 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 2, 95 | "metadata": {}, 96 | "outputs": [ 97 | { 98 | "name": "stdout", 99 | "output_type": "stream", 100 | "text": [ 101 | " NL File: example.nl\n", 102 | "Symbol Map File: example.nl.symbol_map.pickle\n" 103 | ] 104 | } 105 | ], 106 | "source": [ 107 | "# %load write.py\n", 108 | "import pyomo.environ\n", 109 | "from pyomo.core import ComponentUID\n", 110 | "from pyomo.opt import ProblemFormat\n", 111 | "# use fast version of pickle (python 2 or 3)\n", 112 | "from six.moves import cPickle as pickle\n", 113 | "\n", 114 | "def write_nl(model, nl_filename, **kwds):\n", 115 | " \"\"\"\n", 116 | " Writes a Pyomo model in NL file format and stores\n", 117 | " information about the symbol map that allows it to be\n", 118 | " recovered at a later time for a Pyomo model with\n", 119 | " matching component names.\n", 120 | " \"\"\"\n", 121 | " symbol_map_filename = nl_filename+\".symbol_map.pickle\"\n", 122 | "\n", 123 | " # write the model and obtain the symbol_map\n", 124 | " _, smap_id = model.write(nl_filename,\n", 125 | " format=ProblemFormat.nl,\n", 126 | " io_options=kwds)\n", 127 | " symbol_map = model.solutions.symbol_map[smap_id]\n", 128 | "\n", 129 | " # save a persistent form of the symbol_map (using pickle) by\n", 130 | " # storing the NL file label with a ComponentUID, which is\n", 131 | " # an efficient lookup code for model components (created\n", 132 | " # by John Siirola)\n", 133 | " tmp_buffer = {} # this makes the process faster\n", 134 | " symbol_cuid_pairs = tuple(\n", 135 | " (symbol, ComponentUID(var_weakref(), cuid_buffer=tmp_buffer))\n", 136 | " for symbol, var_weakref in symbol_map.bySymbol.items())\n", 137 | " with open(symbol_map_filename, \"wb\") as f:\n", 138 | " pickle.dump(symbol_cuid_pairs, f)\n", 139 | "\n", 140 | " return symbol_map_filename\n", 141 | "\n", 142 | "if __name__ == \"__main__\":\n", 143 | " from script import create_model\n", 144 | "\n", 145 | " model = create_model()\n", 146 | " nl_filename = \"example.nl\"\n", 147 | " symbol_map_filename = write_nl(model, nl_filename)\n", 148 | " print(\" NL File: %s\" % (nl_filename))\n", 149 | " print(\"Symbol Map File: %s\" % (symbol_map_filename))\n" 150 | ] 151 | }, 152 | { 153 | "cell_type": "markdown", 154 | "metadata": {}, 155 | "source": [ 156 | "The first argument to this function is the Pyomo model. The second argument is the name to use for the NL file. Along with the NL file, another file with the suffix \".symbol_map.pickle\" will be created that contains information that can be used to efficiently rebuild the symbol map for any Pyomo model with component names matching those used to build the NL file. Additional options can be passed to the NL writer as keywords to this function. These include:\n", 157 | "* **show_section_timing**: Print timing after writing major sections of the NL file. (default=_False_) \n", 158 | "* **skip_trivial_constraints**: Skip writing constraints whose body section is fixed (i.e., no variables). (default=_False_)\n", 159 | "* **file_determinism**: Sets the level of effort placed on ensuring the NL file is written deterministically. The value of this keyword will affect the row and column ordering assigned to Pyomo constraints and variables in the NLP matrix, respectively.\n", 160 | " * 0: declaration order only \n", 161 | " * 1: sort index sets of indexed components after declaration order (default)\n", 162 | " * 2: sort component names (overriding declaration order) as well as index sets\n", 163 | "* **symbolic_solver_labels**: Generate .row and .col files identifying constraint and variable indices in the NLP matrix. (default=_False_)\n", 164 | "* **include_all_variable_bounds**: Include all variables that are on active blocks of the Pyomo model in the bounds section of the NL file. This includes variables that do not appear in any objective or constraint expressions. (default=_False_)\n", 165 | "* **output_fixed_variable_bounds**: Allow variables that are fixed to appear in the body of preprocessed expressions. Fixing takes place by using a variable's current value as the upper and lower bound in the bounds section of the NL file. This option is experimental. (default=_False_)\n", 166 | "\n", 167 | "The **symbolic_solver_labels** option, when set to _True_, outputs files containing similar information to what is output by this function to recover the symbol map. The difference is that this function outputs component lookup codes (the ComponentUID class) that are meant to allow efficient recovery of components on models that make use of index Sets and/or Blocks. The .row and .col files are meant as debugging tools and use human readable names that are not efficient for recovering model components.\n", 168 | "\n", 169 | "## Invoking the Solver\n", 170 | "\n", 171 | "The solver can be invoked directly from the command shell or by using Python's built-in utilities for executing shell commands. For most ASL-based solvers, we need to use an additional command-line option such as \"-s\" (before the input file) or \"-AMPL\" (after the input file) in order to tell the AMPL Solver Library we want it to store the solution into an SOL file. The code block below issues a bash command that uses the _ipopt_ executable to solve our example model and generate an SOL file. This command requires that the _ipopt_ executable can be found in the shell search PATH and that the code block from the previous section has been executed." 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 3, 177 | "metadata": {}, 178 | "outputs": [ 179 | { 180 | "name": "stdout", 181 | "output_type": "stream", 182 | "text": [ 183 | "\n", 184 | "\n", 185 | "******************************************************************************\n", 186 | "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", 187 | " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", 188 | " For more information visit http://projects.coin-or.org/Ipopt\n", 189 | "******************************************************************************\n", 190 | "\n", 191 | "This is Ipopt version 3.12.3, running with linear solver ma27.\n", 192 | "\n", 193 | "Number of nonzeros in equality constraint Jacobian...: 0\n", 194 | "Number of nonzeros in inequality constraint Jacobian.: 1\n", 195 | "Number of nonzeros in Lagrangian Hessian.............: 0\n", 196 | "\n", 197 | "Total number of variables............................: 1\n", 198 | " variables with only lower bounds: 0\n", 199 | " variables with lower and upper bounds: 0\n", 200 | " variables with only upper bounds: 0\n", 201 | "Total number of equality constraints.................: 0\n", 202 | "Total number of inequality constraints...............: 1\n", 203 | " inequality constraints with only lower bounds: 1\n", 204 | " inequality constraints with lower and upper bounds: 0\n", 205 | " inequality constraints with only upper bounds: 0\n", 206 | "\n", 207 | "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", 208 | " 0 1.0000000e+00 0.00e+00 0.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", 209 | " 1 1.0001504e+00 0.00e+00 1.50e-09 -3.8 9.85e-03 - 1.00e+00 1.00e+00h 1\n", 210 | " 2 1.0000018e+00 0.00e+00 1.84e-11 -5.7 1.49e-04 - 1.00e+00 1.00e+00f 1\n", 211 | " 3 9.9999999e-01 0.00e+00 2.51e-14 -8.6 1.84e-06 - 1.00e+00 1.00e+00f 1\n", 212 | "\n", 213 | "Number of Iterations....: 3\n", 214 | "\n", 215 | " (scaled) (unscaled)\n", 216 | "Objective...............: 9.9999999250590355e-01 9.9999999250590355e-01\n", 217 | "Dual infeasibility......: 2.5091040356528538e-14 2.5091040356528538e-14\n", 218 | "Constraint violation....: 0.0000000000000000e+00 0.0000000000000000e+00\n", 219 | "Complementarity.........: 2.5059035957398297e-09 2.5059035957398297e-09\n", 220 | "Overall NLP error.......: 2.5059035957398297e-09 2.5059035957398297e-09\n", 221 | "\n", 222 | "\n", 223 | "Number of objective function evaluations = 4\n", 224 | "Number of objective gradient evaluations = 4\n", 225 | "Number of equality constraint evaluations = 0\n", 226 | "Number of inequality constraint evaluations = 4\n", 227 | "Number of equality constraint Jacobian evaluations = 0\n", 228 | "Number of inequality constraint Jacobian evaluations = 4\n", 229 | "Number of Lagrangian Hessian evaluations = 3\n", 230 | "Total CPU secs in IPOPT (w/o function evaluations) = 0.001\n", 231 | "Total CPU secs in NLP function evaluations = 0.000\n", 232 | "\n", 233 | "EXIT: Optimal Solution Found.\n", 234 | " \n", 235 | "Ipopt 3.12.3: Optimal Solution Found\n" 236 | ] 237 | } 238 | ], 239 | "source": [ 240 | "%%bash\n", 241 | "ipopt -s example.nl" 242 | ] 243 | }, 244 | { 245 | "cell_type": "markdown", 246 | "metadata": {}, 247 | "source": [ 248 | "## Loading the SOL File\n", 249 | "\n", 250 | "The code block below defines the function **_read_\\__sol_** that produces a results object that can be loaded into a Pyomo model from a given SOL file. The function first calls Pyomo's built-in SOL file reader to produce a bare results object. Symbols used by the SOL file reader take the form < type character >< index >, where < type character > is one of 'o' (objective), 'v' (variable), or 'c' (constraint) and < index > is the row / column index for constraints / variables in the NLP matrix and 0 for the objective (e.g., 'o0', 'v3', 'c1'). These symbols are mapped to component identifiers in the symbol map file created by the **_write_\\__nl_** function from above. The results object returned from the **_read_\\__sol_** function can be loaded into a Pyomo model just like that returned from the _solve_ method on a Pyomo solver plugin when the **_load_\\__solutions_** keyword is set to _False_. The last section of this code block shows how this function can be used to load a solution into a _copy_ of the model used in the section on writing the NL file. It assumes the code blocks in the previous two sections have been executed." 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "execution_count": 4, 256 | "metadata": {}, 257 | "outputs": [ 258 | { 259 | "name": "stdout", 260 | "output_type": "stream", 261 | "text": [ 262 | "Objective: 0.9999999925059035\n" 263 | ] 264 | } 265 | ], 266 | "source": [ 267 | "# %load read.py\n", 268 | "import pyomo.environ\n", 269 | "from pyomo.core import SymbolMap\n", 270 | "from pyomo.opt import (ReaderFactory,\n", 271 | " ResultsFormat)\n", 272 | "# use fast version of pickle (python 2 or 3)\n", 273 | "from six.moves import cPickle as pickle\n", 274 | "\n", 275 | "def read_sol(model, sol_filename, symbol_map_filename, suffixes=[\".*\"]):\n", 276 | " \"\"\"\n", 277 | " Reads the solution from the SOL file and generates a\n", 278 | " results object with an appropriate symbol map for\n", 279 | " loading it into the given Pyomo model. By default all\n", 280 | " suffixes found in the NL file will be extracted. This\n", 281 | " can be overridden using the suffixes keyword, which\n", 282 | " should be a list of suffix names or regular expressions\n", 283 | " (or None).\n", 284 | " \"\"\"\n", 285 | " if suffixes is None:\n", 286 | " suffixes = []\n", 287 | "\n", 288 | " # parse the SOL file\n", 289 | " with ReaderFactory(ResultsFormat.sol) as reader:\n", 290 | " results = reader(sol_filename, suffixes=suffixes)\n", 291 | "\n", 292 | " # regenerate the symbol_map for this model\n", 293 | " with open(symbol_map_filename, \"rb\") as f:\n", 294 | " symbol_cuid_pairs = pickle.load(f)\n", 295 | " symbol_map = SymbolMap()\n", 296 | " symbol_map.addSymbols((cuid.find_component(model), symbol)\n", 297 | " for symbol, cuid in symbol_cuid_pairs)\n", 298 | "\n", 299 | " # tag the results object with the symbol_map\n", 300 | " results._smap = symbol_map\n", 301 | "\n", 302 | " return results\n", 303 | "\n", 304 | "if __name__ == \"__main__\":\n", 305 | " from pyomo.opt import TerminationCondition\n", 306 | " from script import create_model\n", 307 | "\n", 308 | " model = create_model()\n", 309 | " sol_filename = \"example.sol\"\n", 310 | " symbol_map_filename = \"example.nl.symbol_map.pickle\"\n", 311 | " results = read_sol(model, sol_filename, symbol_map_filename)\n", 312 | " if results.solver.termination_condition != \\\n", 313 | " TerminationCondition.optimal:\n", 314 | " raise RuntimeError(\"Solver did not terminate with status = optimal\")\n", 315 | " model.solutions.load_from(results)\n", 316 | " print(\"Objective: %s\" % (model.o()))\n" 317 | ] 318 | } 319 | ], 320 | "metadata": { 321 | "kernelspec": { 322 | "display_name": "Python 3", 323 | "language": "python", 324 | "name": "python3" 325 | }, 326 | "language_info": { 327 | "codemirror_mode": { 328 | "name": "ipython", 329 | "version": 3 330 | }, 331 | "file_extension": ".py", 332 | "mimetype": "text/x-python", 333 | "name": "python", 334 | "nbconvert_exporter": "python", 335 | "pygments_lexer": "ipython3", 336 | "version": "3.6.1" 337 | } 338 | }, 339 | "nbformat": 4, 340 | "nbformat_minor": 1 341 | } -------------------------------------------------------------------------------- /asl_io/read.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | import pyomo.environ 14 | from pyomo.core import SymbolMap 15 | from pyomo.opt import (ReaderFactory, 16 | ResultsFormat) 17 | # use fast version of pickle (python 2 or 3) 18 | from six.moves import cPickle as pickle 19 | 20 | def read_sol(model, sol_filename, symbol_map_filename, suffixes=[".*"]): 21 | """ 22 | Reads the solution from the SOL file and generates a 23 | results object with an appropriate symbol map for 24 | loading it into the given Pyomo model. By default all 25 | suffixes found in the NL file will be extracted. This 26 | can be overridden using the suffixes keyword, which 27 | should be a list of suffix names or regular expressions 28 | (or None). 29 | """ 30 | if suffixes is None: 31 | suffixes = [] 32 | 33 | # parse the SOL file 34 | with ReaderFactory(ResultsFormat.sol) as reader: 35 | results = reader(sol_filename, suffixes=suffixes) 36 | 37 | # regenerate the symbol_map for this model 38 | with open(symbol_map_filename, "rb") as f: 39 | symbol_cuid_pairs = pickle.load(f) 40 | symbol_map = SymbolMap() 41 | symbol_map.addSymbols((cuid.find_component(model), symbol) 42 | for symbol, cuid in symbol_cuid_pairs) 43 | 44 | # tag the results object with the symbol_map 45 | results._smap = symbol_map 46 | 47 | return results 48 | 49 | if __name__ == "__main__": 50 | from pyomo.opt import TerminationCondition 51 | from script import create_model 52 | 53 | model = create_model() 54 | sol_filename = "example.sol" 55 | symbol_map_filename = "example.nl.symbol_map.pickle" 56 | results = read_sol(model, sol_filename, symbol_map_filename) 57 | if results.solver.termination_condition != \ 58 | TerminationCondition.optimal: 59 | raise RuntimeError("Solver did not terminate with status = optimal") 60 | model.solutions.load_from(results) 61 | print("Objective: %s" % (model.o())) 62 | -------------------------------------------------------------------------------- /asl_io/script.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | from pyomo.environ import * 14 | from pyomo.opt import SolverFactory, TerminationCondition 15 | 16 | def create_model(): 17 | model = ConcreteModel() 18 | model.x = Var() 19 | model.o = Objective(expr=model.x) 20 | model.c = Constraint(expr=model.x >= 1) 21 | model.x.set_value(1.0) 22 | return model 23 | 24 | if __name__ == "__main__": 25 | 26 | with SolverFactory("ipopt") as opt: 27 | model = create_model() 28 | results = opt.solve(model, load_solutions=False) 29 | if results.solver.termination_condition != TerminationCondition.optimal: 30 | raise RuntimeError('Solver did not report optimality:\n%s' 31 | % (results.solver)) 32 | model.solutions.load_from(results) 33 | print("Objective: %s" % (model.o())) 34 | -------------------------------------------------------------------------------- /asl_io/write.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | import pyomo.environ 14 | from pyomo.core import ComponentUID 15 | from pyomo.opt import ProblemFormat 16 | # use fast version of pickle (python 2 or 3) 17 | from six.moves import cPickle as pickle 18 | 19 | def write_nl(model, nl_filename, **kwds): 20 | """ 21 | Writes a Pyomo model in NL file format and stores 22 | information about the symbol map that allows it to be 23 | recovered at a later time for a Pyomo model with 24 | matching component names. 25 | """ 26 | symbol_map_filename = nl_filename+".symbol_map.pickle" 27 | 28 | # write the model and obtain the symbol_map 29 | _, smap_id = model.write(nl_filename, 30 | format=ProblemFormat.nl, 31 | io_options=kwds) 32 | symbol_map = model.solutions.symbol_map[smap_id] 33 | 34 | # save a persistent form of the symbol_map (using pickle) by 35 | # storing the NL file label with a ComponentUID, which is 36 | # an efficient lookup code for model components (created 37 | # by John Siirola) 38 | tmp_buffer = {} # this makes the process faster 39 | symbol_cuid_pairs = tuple( 40 | (symbol, ComponentUID(var_weakref(), cuid_buffer=tmp_buffer)) 41 | for symbol, var_weakref in symbol_map.bySymbol.items()) 42 | with open(symbol_map_filename, "wb") as f: 43 | pickle.dump(symbol_cuid_pairs, f) 44 | 45 | return symbol_map_filename 46 | 47 | if __name__ == "__main__": 48 | from script import create_model 49 | 50 | model = create_model() 51 | nl_filename = "example.nl" 52 | symbol_map_filename = write_nl(model, nl_filename) 53 | print(" NL File: %s" % (nl_filename)) 54 | print("Symbol Map File: %s" % (symbol_map_filename)) 55 | -------------------------------------------------------------------------------- /diet/DietProblem.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# ___________________________________________________________________________\n#\n# Pyomo: Python Optimization Modeling Objects\n# Copyright (c) 2015-2025\n# National Technology and Engineering Solutions of Sandia, LLC\n# Under the terms of Contract DE-NA0003525 with National Technology and\n# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain\n# rights in this software.\n# This software is distributed under the 3-clause BSD License.\n# ___________________________________________________________________________" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# The Diet Problem\n", 17 | "\n", 18 | "## Summary\n", 19 | "\n", 20 | "The goal of the Diet Problem is to select foods that satisfy daily nutritional requirements at minimum cost. This problem can be formulated as a linear program, for which constraints limit the number of calories and the amount of vitamins, minerals, fats, sodium, and cholesterol in the diet. Danzig (1990) notes that the diet problem was motivated by the US Army's desire to minimize the cost of feeding GIs in the field while still providing a healthy diet.\n", 21 | "\n", 22 | "## Problem Statement\n", 23 | "\n", 24 | "The Diet Problem can be formulated mathematically as a linear programming problem using the following model. \n", 25 | "\n", 26 | "### Sets\n", 27 | "\n", 28 | " $F$ = set of foods \n", 29 | " $N$ = set of nutrients\n", 30 | "\n", 31 | "### Parameters\n", 32 | "\n", 33 | " $c_i$ = cost per serving of food $i$, $\\forall i \\in F$ \n", 34 | " $a_{ij}$ = amount of nutrient $j$ in food $i$, $\\forall i \\in F, \\forall j \\in N$ \n", 35 | " $Nmin_j$ = minimum level of nutrient $j$, $\\forall j \\in N$ \n", 36 | " $Nmax_j$ = maximum level of nutrient $j$, $\\forall j \\in N$ \n", 37 | " $V_i$ = the volume per serving of food $i$, $\\forall i \\in F$ \n", 38 | " $Vmax$ = maximum volume of food consumed\n", 39 | " \n", 40 | "### Variables\n", 41 | " $x_i$ = number of servings of food $i$ to consume\n", 42 | "\n", 43 | "### Objective\n", 44 | "\n", 45 | "Minimize the total cost of the food \n", 46 | " $\\min \\sum_{i \\in F} c_i x_i$\n", 47 | "\n", 48 | "### Constraints\n", 49 | "\n", 50 | "Limit nutrient consumption for each nutrient $j \\in N$. \n", 51 | " $Nmin_j \\leq \\sum_{i \\in F} a_{ij} x_i \\leq Nmax_j$, $\\forall j \\in N$\n", 52 | "\n", 53 | "Limit the volume of food consumed \n", 54 | " $\\sum_{i \\in F} V_i x_i \\leq Vmax$\n", 55 | " \n", 56 | "Consumption lower bound \n", 57 | " $x_i \\geq 0$, $\\forall i \\in F$\n", 58 | "\n", 59 | "## Pyomo Formulation\n", 60 | "\n", 61 | "We begin by importing the Pyomo package and creating a model object:" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 1, 67 | "metadata": { 68 | "collapsed": true 69 | }, 70 | "outputs": [], 71 | "source": [ 72 | "from pyomo.environ import *\n", 73 | "infinity = float('inf')\n", 74 | "\n", 75 | "model = AbstractModel()" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "The sets $F$ and $N$ are declared abstractly using the `Set` component:" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 2, 88 | "metadata": { 89 | "collapsed": true 90 | }, 91 | "outputs": [], 92 | "source": [ 93 | "# Foods\n", 94 | "model.F = Set()\n", 95 | "# Nutrients\n", 96 | "model.N = Set()" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "Similarly, the model parameters are defined abstractly using the `Param` component:" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 3, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "# Cost of each food\n", 113 | "model.c = Param(model.F, within=PositiveReals)\n", 114 | "# Amount of nutrient in each food\n", 115 | "model.a = Param(model.F, model.N, within=NonNegativeReals)\n", 116 | "# Lower and upper bound on each nutrient\n", 117 | "model.Nmin = Param(model.N, within=NonNegativeReals, default=0.0)\n", 118 | "model.Nmax = Param(model.N, within=NonNegativeReals, default=infinity)\n", 119 | "# Volume per serving of food\n", 120 | "model.V = Param(model.F, within=PositiveReals)\n", 121 | "# Maximum volume of food consumed\n", 122 | "model.Vmax = Param(within=PositiveReals)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "metadata": {}, 128 | "source": [ 129 | "The `within` option is used in these parameter declarations to define expected properties of the parameters. This information is used to perform error checks on the data that is used to initialize the parameter components.\n", 130 | "\n", 131 | "The `Var` component is used to define the decision variables:" 132 | ] 133 | }, 134 | { 135 | "cell_type": "code", 136 | "execution_count": 4, 137 | "metadata": { 138 | "collapsed": true 139 | }, 140 | "outputs": [], 141 | "source": [ 142 | "# Number of servings consumed of each food\n", 143 | "model.x = Var(model.F, within=NonNegativeIntegers)" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "metadata": {}, 149 | "source": [ 150 | "The `within` option is used to restrict the domain of the decision variables to the non-negative reals. This eliminates the need for explicit bound constraints for variables.\n", 151 | "\n", 152 | "The `Objective` component is used to define the cost objective. This component uses a rule function to construct the objective expression:" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 5, 158 | "metadata": { 159 | "collapsed": true 160 | }, 161 | "outputs": [], 162 | "source": [ 163 | "# Minimize the cost of food that is consumed\n", 164 | "def cost_rule(model):\n", 165 | " return sum(model.c[i]*model.x[i] for i in model.F)\n", 166 | "model.cost = Objective(rule=cost_rule)" 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "metadata": {}, 172 | "source": [ 173 | "Similarly, rule functions are used to define constraint expressions in the `Constraint` component:" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": 6, 179 | "metadata": { 180 | "collapsed": true 181 | }, 182 | "outputs": [], 183 | "source": [ 184 | "# Limit nutrient consumption for each nutrient\n", 185 | "def nutrient_rule(model, j):\n", 186 | " value = sum(model.a[i,j]*model.x[i] for i in model.F)\n", 187 | " return inequality(model.Nmin[j], value, model.Nmax[j])\n", 188 | "model.nutrient_limit = Constraint(model.N, rule=nutrient_rule)\n", 189 | "\n", 190 | "# Limit the volume of food consumed\n", 191 | "def volume_rule(model):\n", 192 | " return sum(model.V[i]*model.x[i] for i in model.F) <= model.Vmax\n", 193 | "model.volume = Constraint(rule=volume_rule)" 194 | ] 195 | }, 196 | { 197 | "cell_type": "markdown", 198 | "metadata": {}, 199 | "source": [ 200 | "Putting these declarations all together gives the following model:" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": 7, 206 | "metadata": {}, 207 | "outputs": [ 208 | { 209 | "name": "stdout", 210 | "output_type": "stream", 211 | "text": [ 212 | "from pyomo.environ import *\r\n", 213 | "infinity = float('inf')\r\n", 214 | "\r\n", 215 | "model = AbstractModel()\r\n", 216 | "\r\n", 217 | "# Foods\r\n", 218 | "model.F = Set()\r\n", 219 | "# Nutrients\r\n", 220 | "model.N = Set()\r\n", 221 | "\r\n", 222 | "# Cost of each food\r\n", 223 | "model.c = Param(model.F, within=PositiveReals)\r\n", 224 | "# Amount of nutrient in each food\r\n", 225 | "model.a = Param(model.F, model.N, within=NonNegativeReals)\r\n", 226 | "# Lower and upper bound on each nutrient\r\n", 227 | "model.Nmin = Param(model.N, within=NonNegativeReals, default=0.0)\r\n", 228 | "model.Nmax = Param(model.N, within=NonNegativeReals, default=infinity)\r\n", 229 | "# Volume per serving of food\r\n", 230 | "model.V = Param(model.F, within=PositiveReals)\r\n", 231 | "# Maximum volume of food consumed\r\n", 232 | "model.Vmax = Param(within=PositiveReals)\r\n", 233 | "\r\n", 234 | "# Number of servings consumed of each food\r\n", 235 | "model.x = Var(model.F, within=NonNegativeIntegers)\r\n", 236 | "\r\n", 237 | "# Minimize the cost of food that is consumed\r\n", 238 | "def cost_rule(model):\r\n", 239 | " return sum(model.c[i]*model.x[i] for i in model.F)\r\n", 240 | "model.cost = Objective(rule=cost_rule)\r\n", 241 | "\r\n", 242 | "# Limit nutrient consumption for each nutrient\r\n", 243 | "def nutrient_rule(model, j):\r\n", 244 | " value = sum(model.a[i,j]*model.x[i] for i in model.F)\r\n", 245 | " return inequality(model.Nmin[j], value, model.Nmax[j])\r\n", 246 | "model.nutrient_limit = Constraint(model.N, rule=nutrient_rule)\r\n", 247 | "\r\n", 248 | "# Limit the volume of food consumed\r\n", 249 | "def volume_rule(model):\r\n", 250 | " return sum(model.V[i]*model.x[i] for i in model.F) <= model.Vmax\r\n", 251 | "model.volume = Constraint(rule=volume_rule)\r\n" 252 | ] 253 | } 254 | ], 255 | "source": [ 256 | "!cat diet.py" 257 | ] 258 | }, 259 | { 260 | "cell_type": "markdown", 261 | "metadata": {}, 262 | "source": [ 263 | "## Model Data\n", 264 | "\n", 265 | "Since this is an abstract Pyomo model, the set and parameter values need to be provided to initialize the model. The following data command file provides a synthetic data set:" 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": 8, 271 | "metadata": {}, 272 | "outputs": [ 273 | { 274 | "name": "stdout", 275 | "output_type": "stream", 276 | "text": [ 277 | "param: F: c V :=\r\n", 278 | " \"Cheeseburger\" 1.84 4.0 \r\n", 279 | " \"Ham Sandwich\" 2.19 7.5 \r\n", 280 | " \"Hamburger\" 1.84 3.5 \r\n", 281 | " \"Fish Sandwich\" 1.44 5.0 \r\n", 282 | " \"Chicken Sandwich\" 2.29 7.3 \r\n", 283 | " \"Fries\" .77 2.6 \r\n", 284 | " \"Sausage Biscuit\" 1.29 4.1 \r\n", 285 | " \"Lowfat Milk\" .60 8.0 \r\n", 286 | " \"Orange Juice\" .72 12.0 ;\r\n", 287 | "\r\n", 288 | "param Vmax := 75.0;\r\n", 289 | "\r\n", 290 | "param: N: Nmin Nmax :=\r\n", 291 | " Cal 2000 .\r\n", 292 | " Carbo 350 375\r\n", 293 | " Protein 55 .\r\n", 294 | " VitA 100 .\r\n", 295 | " VitC 100 .\r\n", 296 | " Calc 100 .\r\n", 297 | " Iron 100 . ;\r\n", 298 | "\r\n", 299 | "param a:\r\n", 300 | " Cal Carbo Protein VitA VitC Calc Iron :=\r\n", 301 | " \"Cheeseburger\" 510 34 28 15 6 30 20\r\n", 302 | " \"Ham Sandwich\" 370 35 24 15 10 20 20\r\n", 303 | " \"Hamburger\" 500 42 25 6 2 25 20\r\n", 304 | " \"Fish Sandwich\" 370 38 14 2 0 15 10\r\n", 305 | " \"Chicken Sandwich\" 400 42 31 8 15 15 8\r\n", 306 | " \"Fries\" 220 26 3 0 15 0 2\r\n", 307 | " \"Sausage Biscuit\" 345 27 15 4 0 20 15\r\n", 308 | " \"Lowfat Milk\" 110 12 9 10 4 30 0\r\n", 309 | " \"Orange Juice\" 80 20 1 2 120 2 2 ;\r\n" 310 | ] 311 | } 312 | ], 313 | "source": [ 314 | "!cat diet.dat" 315 | ] 316 | }, 317 | { 318 | "cell_type": "markdown", 319 | "metadata": {}, 320 | "source": [ 321 | "Set data is defined with the `set` command, and parameter data is defined with the `param` command.\n", 322 | "\n", 323 | "This data set considers the problem of designing a daily diet with only food from a fast food chain.\n", 324 | "\n", 325 | "## Solution\n", 326 | "\n", 327 | "Pyomo includes a `pyomo` command that automates the construction and optimization of models. The GLPK solver can be used in this simple example:" 328 | ] 329 | }, 330 | { 331 | "cell_type": "code", 332 | "execution_count": 9, 333 | "metadata": {}, 334 | "outputs": [ 335 | { 336 | "name": "stdout", 337 | "output_type": "stream", 338 | "text": [ 339 | "[ 0.00] Setting up Pyomo environment\r\n", 340 | "[ 0.00] Applying Pyomo preprocessing actions\r\n", 341 | "[ 0.00] Creating model\r\n", 342 | "[ 0.02] Applying solver\r\n", 343 | "[ 0.06] Processing results\r\n", 344 | " Number of solutions: 1\r\n", 345 | " Solution Information\r\n", 346 | " Gap: 0.0\r\n", 347 | " Status: optimal\r\n", 348 | " Function Value: 15.05\r\n", 349 | " Solver results file: results.json\r\n", 350 | "[ 0.06] Applying Pyomo postprocessing actions\r\n", 351 | "[ 0.06] Pyomo Finished\r\n" 352 | ] 353 | } 354 | ], 355 | "source": [ 356 | "!pyomo solve --solver=glpk diet.py diet.dat" 357 | ] 358 | }, 359 | { 360 | "cell_type": "markdown", 361 | "metadata": {}, 362 | "source": [ 363 | "By default, the optimization results are stored in the file `results.yml`:" 364 | ] 365 | }, 366 | { 367 | "cell_type": "code", 368 | "execution_count": 10, 369 | "metadata": {}, 370 | "outputs": [ 371 | { 372 | "name": "stdout", 373 | "output_type": "stream", 374 | "text": [ 375 | "# ==========================================================\r\n", 376 | "# = Solver Results =\r\n", 377 | "# ==========================================================\r\n", 378 | "# ----------------------------------------------------------\r\n", 379 | "# Problem Information\r\n", 380 | "# ----------------------------------------------------------\r\n", 381 | "Problem: \r\n", 382 | "- Name: unknown\r\n", 383 | " Lower bound: 15.05\r\n", 384 | " Upper bound: 15.05\r\n", 385 | " Number of objectives: 1\r\n", 386 | " Number of constraints: 10\r\n", 387 | " Number of variables: 10\r\n", 388 | " Number of nonzeros: 77\r\n", 389 | " Sense: minimize\r\n", 390 | "# ----------------------------------------------------------\r\n", 391 | "# Solver Information\r\n", 392 | "# ----------------------------------------------------------\r\n", 393 | "Solver: \r\n", 394 | "- Status: ok\r\n", 395 | " Termination condition: optimal\r\n", 396 | " Statistics: \r\n", 397 | " Branch and bound: \r\n", 398 | " Number of bounded subproblems: 89\r\n", 399 | " Number of created subproblems: 89\r\n", 400 | " Error rc: 0\r\n", 401 | " Time: 0.00977396965027\r\n", 402 | "# ----------------------------------------------------------\r\n", 403 | "# Solution Information\r\n", 404 | "# ----------------------------------------------------------\r\n", 405 | "Solution: \r\n", 406 | "- number of solutions: 1\r\n", 407 | " number of solutions displayed: 1\r\n", 408 | "- Gap: 0.0\r\n", 409 | " Status: optimal\r\n", 410 | " Message: None\r\n", 411 | " Objective:\r\n", 412 | " cost:\r\n", 413 | " Value: 15.05\r\n", 414 | " Variable:\r\n", 415 | " x[Cheeseburger]:\r\n", 416 | " Value: 4\r\n", 417 | " x[Fries]:\r\n", 418 | " Value: 5\r\n", 419 | " x[Fish Sandwich]:\r\n", 420 | " Value: 1\r\n", 421 | " x[Lowfat Milk]:\r\n", 422 | " Value: 4\r\n", 423 | " Constraint: No values\r\n" 424 | ] 425 | } 426 | ], 427 | "source": [ 428 | "!cat results.yml" 429 | ] 430 | }, 431 | { 432 | "cell_type": "markdown", 433 | "metadata": {}, 434 | "source": [ 435 | "This solution shows that for about $15 per day, a person can get by with 4 \n", 436 | "cheeseburgers, 5 fries, 1 fish sandwich and 4 milks." 437 | ] 438 | }, 439 | { 440 | "cell_type": "markdown", 441 | "metadata": {}, 442 | "source": [ 443 | "## References\n", 444 | "\n", 445 | "* G.B. Dantzig. The Diet Problem, Interfaces 20(4), 1990, 43-47" 446 | ] 447 | } 448 | ], 449 | "metadata": { 450 | "kernelspec": { 451 | "display_name": "Python 3", 452 | "language": "python", 453 | "name": "python3" 454 | }, 455 | "language_info": { 456 | "codemirror_mode": { 457 | "name": "ipython", 458 | "version": 3 459 | }, 460 | "file_extension": ".py", 461 | "mimetype": "text/x-python", 462 | "name": "python", 463 | "nbconvert_exporter": "python", 464 | "pygments_lexer": "ipython3", 465 | "version": "3.6.1" 466 | } 467 | }, 468 | "nbformat": 4, 469 | "nbformat_minor": 1 470 | } -------------------------------------------------------------------------------- /diet/diet.dat: -------------------------------------------------------------------------------- 1 | param: F: c V := 2 | "Cheeseburger" 1.84 4.0 3 | "Ham Sandwich" 2.19 7.5 4 | "Hamburger" 1.84 3.5 5 | "Fish Sandwich" 1.44 5.0 6 | "Chicken Sandwich" 2.29 7.3 7 | "Fries" .77 2.6 8 | "Sausage Biscuit" 1.29 4.1 9 | "Lowfat Milk" .60 8.0 10 | "Orange Juice" .72 12.0 ; 11 | 12 | param Vmax := 75.0; 13 | 14 | param: N: Nmin Nmax := 15 | Cal 2000 . 16 | Carbo 350 375 17 | Protein 55 . 18 | VitA 100 . 19 | VitC 100 . 20 | Calc 100 . 21 | Iron 100 . ; 22 | 23 | param a: 24 | Cal Carbo Protein VitA VitC Calc Iron := 25 | "Cheeseburger" 510 34 28 15 6 30 20 26 | "Ham Sandwich" 370 35 24 15 10 20 20 27 | "Hamburger" 500 42 25 6 2 25 20 28 | "Fish Sandwich" 370 38 14 2 0 15 10 29 | "Chicken Sandwich" 400 42 31 8 15 15 8 30 | "Fries" 220 26 3 0 15 0 2 31 | "Sausage Biscuit" 345 27 15 4 0 20 15 32 | "Lowfat Milk" 110 12 9 10 4 30 0 33 | "Orange Juice" 80 20 1 2 120 2 2 ; 34 | -------------------------------------------------------------------------------- /diet/diet.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | from pyomo.environ import * 14 | infinity = float('inf') 15 | 16 | model = AbstractModel() 17 | 18 | # Foods 19 | model.F = Set() 20 | # Nutrients 21 | model.N = Set() 22 | 23 | # Cost of each food 24 | model.c = Param(model.F, within=PositiveReals) 25 | # Amount of nutrient in each food 26 | model.a = Param(model.F, model.N, within=NonNegativeReals) 27 | # Lower and upper bound on each nutrient 28 | model.Nmin = Param(model.N, within=NonNegativeReals, default=0.0) 29 | model.Nmax = Param(model.N, within=NonNegativeReals, default=infinity) 30 | # Volume per serving of food 31 | model.V = Param(model.F, within=PositiveReals) 32 | # Maximum volume of food consumed 33 | model.Vmax = Param(within=PositiveReals) 34 | 35 | # Number of servings consumed of each food 36 | model.x = Var(model.F, within=NonNegativeIntegers) 37 | 38 | # Minimize the cost of food that is consumed 39 | def cost_rule(model): 40 | return sum(model.c[i]*model.x[i] for i in model.F) 41 | model.cost = Objective(rule=cost_rule) 42 | 43 | # Limit nutrient consumption for each nutrient 44 | def nutrient_rule(model, j): 45 | value = sum(model.a[i,j]*model.x[i] for i in model.F) 46 | return inequality(model.Nmin[j], value, model.Nmax[j]) 47 | model.nutrient_limit = Constraint(model.N, rule=nutrient_rule) 48 | 49 | # Limit the volume of food consumed 50 | def volume_rule(model): 51 | return sum(model.V[i]*model.x[i] for i in model.F) <= model.Vmax 52 | model.volume = Constraint(rule=volume_rule) 53 | -------------------------------------------------------------------------------- /diet/results.yml: -------------------------------------------------------------------------------- 1 | # ========================================================== 2 | # = Solver Results = 3 | # ========================================================== 4 | # ---------------------------------------------------------- 5 | # Problem Information 6 | # ---------------------------------------------------------- 7 | Problem: 8 | - Name: unknown 9 | Lower bound: 15.05 10 | Upper bound: 15.05 11 | Number of objectives: 1 12 | Number of constraints: 10 13 | Number of variables: 10 14 | Number of nonzeros: 77 15 | Sense: minimize 16 | # ---------------------------------------------------------- 17 | # Solver Information 18 | # ---------------------------------------------------------- 19 | Solver: 20 | - Status: ok 21 | Termination condition: optimal 22 | Statistics: 23 | Branch and bound: 24 | Number of bounded subproblems: 89 25 | Number of created subproblems: 89 26 | Error rc: 0 27 | Time: 0.00977396965027 28 | # ---------------------------------------------------------- 29 | # Solution Information 30 | # ---------------------------------------------------------- 31 | Solution: 32 | - number of solutions: 1 33 | number of solutions displayed: 1 34 | - Gap: 0.0 35 | Status: optimal 36 | Message: None 37 | Objective: 38 | cost: 39 | Value: 15.05 40 | Variable: 41 | x[Cheeseburger]: 42 | Value: 4 43 | x[Fries]: 44 | Value: 5 45 | x[Fish Sandwich]: 46 | Value: 1 47 | x[Lowfat Milk]: 48 | Value: 4 49 | Constraint: No values 50 | -------------------------------------------------------------------------------- /maxflow/maxflow.dat: -------------------------------------------------------------------------------- 1 | set N := Zoo A B C D E Home; 2 | set A := (Zoo,A) (Zoo,B) (A,C) (A,D) (B,A) (B,C) (C,D) (C,E) (D,E) (D,Home) (E,Home); 3 | 4 | param s := Zoo; 5 | param t := Home; 6 | param: c := 7 | Zoo A 11 8 | Zoo B 8 9 | A C 5 10 | A D 8 11 | B A 4 12 | B C 3 13 | C D 2 14 | C E 4 15 | D E 5 16 | D Home 8 17 | E Home 6; 18 | -------------------------------------------------------------------------------- /maxflow/maxflow.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# ___________________________________________________________________________\n#\n# Pyomo: Python Optimization Modeling Objects\n# Copyright (c) 2015-2025\n# National Technology and Engineering Solutions of Sandia, LLC\n# Under the terms of Contract DE-NA0003525 with National Technology and\n# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain\n# rights in this software.\n# This software is distributed under the 3-clause BSD License.\n# ___________________________________________________________________________" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# Max Flow\n", 17 | "\n", 18 | "## Summary\n", 19 | "\n", 20 | "The goal of the maximum flow problem is to find the maximum flow possible in a network from some given source node to a given sink node. Applications of the max flow problem include finding the maximum flow of orders through a job shop, the maximum flow of water through a storm sewer system, and the maximum flow of product through a product distribution system, among others. Schrijver (2002) note that the maximum flow problem was first formulated in 1954 by T. E. Harris and F. S. Ross as a simplified model of Soviet railway traffic flow.\n", 21 | "\n", 22 | "A network is a directed graph, and the arc capacities, or upper bounds, are the only relevant parameters. A network graph does not have to be symmetric: if an arc (v,w) is in the graph, the reverse arc (w,v) does not have to be in the graph. Further, parallel arcs are not allowed, and self-loops are not allowed. The source and the sink are distinct nodes in the network, but the sink may be unreachable from the source.\n", 23 | " \n", 24 | "\n", 25 | "## Problem Statement\n", 26 | "\n", 27 | "The max flow problem can be formulated mathematically as a linear programming problem using the following model. \n", 28 | "\n", 29 | "### Sets\n", 30 | "\n", 31 | " $N$ = nodes in the network \n", 32 | " $A$ = network arcs\n", 33 | "\n", 34 | "### Parameters\n", 35 | "\n", 36 | " $s$ = source node \n", 37 | " $t$ = sink node \n", 38 | " $c_{ij}$ = arc flow capacity, $\\forall (i,j) \\in A$\n", 39 | " \n", 40 | "### Variables\n", 41 | " $f_{i,j}$ = arc flow, $\\forall (i,j) \\in A$\n", 42 | "\n", 43 | "### Objective\n", 44 | "\n", 45 | "Maximize the flow into the sink nodes \n", 46 | " $\\max \\sum_{\\{i \\mid (i,t) \\in A\\}} c_{i,t} f_{i,t}$\n", 47 | "\n", 48 | "### Constraints\n", 49 | "\n", 50 | "Enforce an upper limit on the flow across each arc \n", 51 | " $f_{i,j} \\leq c_{i,j}$, $\\forall (i,j) \\in A$\n", 52 | "\n", 53 | "Enforce flow through each node \n", 54 | " $\\sum_{\\{i \\mid (i,j) \\in A\\}} f_{i,j} = \\sum_{\\{i \\mid (j,i) \\in A\\}} f_{j,i}$, $\\forall j \\in N - \\{s,t\\}$\n", 55 | " \n", 56 | "Flow lower bound \n", 57 | " $f_{i,j} \\geq 0$, $\\forall (i,j) \\in A$\n", 58 | "\n", 59 | "## Pyomo Formulation\n", 60 | "\n", 61 | "We begin by importing the Pyomo package and creating a model object:" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 1, 67 | "metadata": { 68 | "collapsed": true 69 | }, 70 | "outputs": [], 71 | "source": [ 72 | "from pyomo.environ import *\n", 73 | "\n", 74 | "model = AbstractModel()" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "The sets $N$ and $A$ are declared abstractly using the `Set` component:" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 2, 87 | "metadata": { 88 | "collapsed": true 89 | }, 90 | "outputs": [], 91 | "source": [ 92 | "# Nodes in the network\n", 93 | "model.N = Set()\n", 94 | "# Network arcs\n", 95 | "model.A = Set(within=model.N*model.N)" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "Similarly, the model parameters are defined abstractly using the `Param` component:" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": 3, 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "# Source node\n", 112 | "model.s = Param(within=model.N)\n", 113 | "# Sink node\n", 114 | "model.t = Param(within=model.N)\n", 115 | "# Flow capacity limits\n", 116 | "model.c = Param(model.A)" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "The `within` option is used in these parameter declarations to define expected properties of the parameters. This information is used to perform error checks on the data that is used to initialize the parameter components.\n", 124 | "\n", 125 | "The `Var` component is used to define the decision variables:" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": 4, 131 | "metadata": { 132 | "collapsed": true 133 | }, 134 | "outputs": [], 135 | "source": [ 136 | "# The flow over each arc\n", 137 | "model.f = Var(model.A, within=NonNegativeReals)" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "The `within` option is used to restrict the domain of the decision variables to the non-negative reals. This eliminates the need for explicit bound constraints for variables.\n", 145 | "\n", 146 | "The `Objective` component is used to define the cost objective. This component uses a rule function to construct the objective expression:" 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": 5, 152 | "metadata": { 153 | "collapsed": true 154 | }, 155 | "outputs": [], 156 | "source": [ 157 | "# Maximize the flow into the sink nodes\n", 158 | "def total_rule(model):\n", 159 | " return sum(model.f[i,j] for (i, j) in model.A if j == value(model.t))\n", 160 | "model.total = Objective(rule=total_rule, sense=maximize)" 161 | ] 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "metadata": {}, 166 | "source": [ 167 | "Similarly, rule functions are used to define constraint expressions in the `Constraint` component:" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": 6, 173 | "metadata": { 174 | "collapsed": true 175 | }, 176 | "outputs": [], 177 | "source": [ 178 | "# Enforce an upper limit on the flow across each arc\n", 179 | "def limit_rule(model, i, j):\n", 180 | " return model.f[i,j] <= model.c[i, j]\n", 181 | "model.limit = Constraint(model.A, rule=limit_rule)\n", 182 | "\n", 183 | "# Enforce flow through each node\n", 184 | "def flow_rule(model, k):\n", 185 | " if k == value(model.s) or k == value(model.t):\n", 186 | " return Constraint.Skip\n", 187 | " inFlow = sum(model.f[i,j] for (i,j) in model.A if j == k)\n", 188 | " outFlow = sum(model.f[i,j] for (i,j) in model.A if i == k)\n", 189 | " return inFlow == outFlow\n", 190 | "model.flow = Constraint(model.N, rule=flow_rule)" 191 | ] 192 | }, 193 | { 194 | "cell_type": "markdown", 195 | "metadata": {}, 196 | "source": [ 197 | "Putting these declarations all together gives the following model:" 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": 7, 203 | "metadata": {}, 204 | "outputs": [ 205 | { 206 | "name": "stdout", 207 | "output_type": "stream", 208 | "text": [ 209 | "from pyomo.environ import *\r\n", 210 | "\r\n", 211 | "model = AbstractModel()\r\n", 212 | "\r\n", 213 | "# Nodes in the network\r\n", 214 | "model.N = Set()\r\n", 215 | "# Network arcs\r\n", 216 | "model.A = Set(within=model.N*model.N)\r\n", 217 | "\r\n", 218 | "# Source node\r\n", 219 | "model.s = Param(within=model.N)\r\n", 220 | "# Sink node\r\n", 221 | "model.t = Param(within=model.N)\r\n", 222 | "# Flow capacity limits\r\n", 223 | "model.c = Param(model.A)\r\n", 224 | "\r\n", 225 | "# The flow over each arc\r\n", 226 | "model.f = Var(model.A, within=NonNegativeReals)\r\n", 227 | "\r\n", 228 | "# Maximize the flow into the sink nodes\r\n", 229 | "def total_rule(model):\r\n", 230 | " return sum(model.f[i,j] for (i, j) in model.A if j == value(model.t))\r\n", 231 | "model.total = Objective(rule=total_rule, sense=maximize)\r\n", 232 | "\r\n", 233 | "# Enforce an upper limit on the flow across each arc\r\n", 234 | "def limit_rule(model, i, j):\r\n", 235 | " return model.f[i,j] <= model.c[i, j]\r\n", 236 | "model.limit = Constraint(model.A, rule=limit_rule)\r\n", 237 | "\r\n", 238 | "# Enforce flow through each node\r\n", 239 | "def flow_rule(model, k):\r\n", 240 | " if k == value(model.s) or k == value(model.t):\r\n", 241 | " return Constraint.Skip\r\n", 242 | " inFlow = sum(model.f[i,j] for (i,j) in model.A if j == k)\r\n", 243 | " outFlow = sum(model.f[i,j] for (i,j) in model.A if i == k)\r\n", 244 | " return inFlow == outFlow\r\n", 245 | "model.flow = Constraint(model.N, rule=flow_rule)\r\n", 246 | "\r\n" 247 | ] 248 | } 249 | ], 250 | "source": [ 251 | "!cat maxflow.py" 252 | ] 253 | }, 254 | { 255 | "cell_type": "markdown", 256 | "metadata": {}, 257 | "source": [ 258 | "## Model Data\n", 259 | "\n", 260 | "Since this is an abstract Pyomo model, the set and parameter values need to be provided to initialize the model. The following data command file provides a synthetic data set:" 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": 8, 266 | "metadata": {}, 267 | "outputs": [ 268 | { 269 | "name": "stdout", 270 | "output_type": "stream", 271 | "text": [ 272 | "set N := Zoo A B C D E Home;\r\n", 273 | "set A := (Zoo,A) (Zoo,B) (A,C) (A,D) (B,A) (B,C) (C,D) (C,E) (D,E) (D,Home) (E,Home);\r\n", 274 | "\r\n", 275 | "param s := Zoo;\r\n", 276 | "param t := Home;\r\n", 277 | "param: c :=\r\n", 278 | "Zoo A 11\r\n", 279 | "Zoo B 8\r\n", 280 | "A C 5\r\n", 281 | "A D 8\r\n", 282 | "B A 4\r\n", 283 | "B C 3\r\n", 284 | "C D 2\r\n", 285 | "C E 4\r\n", 286 | "D E 5\r\n", 287 | "D Home 8\r\n", 288 | "E Home 6;\r\n" 289 | ] 290 | } 291 | ], 292 | "source": [ 293 | "!cat maxflow.dat" 294 | ] 295 | }, 296 | { 297 | "cell_type": "markdown", 298 | "metadata": {}, 299 | "source": [ 300 | "Set data is defined with the `set` command, and parameter data is defined with the `param` command.\n", 301 | "\n", 302 | "This data set considers the problem of maximizing flow in a zoo. A panda is about to give birth at the zoo! Officials anticipate that attendance will skyrocket to see the new, adorable baby panda. There's one particular residential area called \"Home\" that is full of panda loving families and there's a fear that the increased number of people visiting the zoo will overload the public transportation system. It will be especially bad in the evening since the zoo closes about the same time as rush hour, so everyone will be trying to find space on the already crowded buses and subways. As a city planner, you were given a map of routes from the zoo to Home, along with the estimated number of families that could go on each route. Additionally, it was estimated that 16 families from Home will visit each day, and it's your task to figure out if this will overload the public transportation system, and, if it does, how could the system be improved?\n", 303 | "\n", 304 | "## Solution\n", 305 | "\n", 306 | "Pyomo includes a `pyomo` command that automates the construction and optimization of models. The GLPK solver can be used in this simple example:" 307 | ] 308 | }, 309 | { 310 | "cell_type": "code", 311 | "execution_count": 9, 312 | "metadata": {}, 313 | "outputs": [ 314 | { 315 | "name": "stdout", 316 | "output_type": "stream", 317 | "text": [ 318 | "[ 0.00] Setting up Pyomo environment\r\n", 319 | "[ 0.00] Applying Pyomo preprocessing actions\r\n", 320 | "[ 0.00] Creating model\r\n", 321 | "[ 0.02] Applying solver\r\n", 322 | "[ 0.06] Processing results\r\n", 323 | " Number of solutions: 1\r\n", 324 | " Solution Information\r\n", 325 | " Gap: 0.0\r\n", 326 | " Status: feasible\r\n", 327 | " Function Value: 14.0\r\n", 328 | " Solver results file: results.json\r\n", 329 | "[ 0.06] Applying Pyomo postprocessing actions\r\n", 330 | "[ 0.06] Pyomo Finished\r\n" 331 | ] 332 | } 333 | ], 334 | "source": [ 335 | "!pyomo solve --solver=glpk maxflow.py maxflow.dat" 336 | ] 337 | }, 338 | { 339 | "cell_type": "markdown", 340 | "metadata": {}, 341 | "source": [ 342 | "By default, the optimization results are stored in the file `results.yml`:" 343 | ] 344 | }, 345 | { 346 | "cell_type": "code", 347 | "execution_count": 10, 348 | "metadata": {}, 349 | "outputs": [ 350 | { 351 | "name": "stdout", 352 | "output_type": "stream", 353 | "text": [ 354 | "# ==========================================================\r\n", 355 | "# = Solver Results =\r\n", 356 | "# ==========================================================\r\n", 357 | "# ----------------------------------------------------------\r\n", 358 | "# Problem Information\r\n", 359 | "# ----------------------------------------------------------\r\n", 360 | "Problem: \r\n", 361 | "- Name: unknown\r\n", 362 | " Lower bound: 14.0\r\n", 363 | " Upper bound: 14.0\r\n", 364 | " Number of objectives: 1\r\n", 365 | " Number of constraints: 17\r\n", 366 | " Number of variables: 12\r\n", 367 | " Number of nonzeros: 30\r\n", 368 | " Sense: maximize\r\n", 369 | "# ----------------------------------------------------------\r\n", 370 | "# Solver Information\r\n", 371 | "# ----------------------------------------------------------\r\n", 372 | "Solver: \r\n", 373 | "- Status: ok\r\n", 374 | " Termination condition: optimal\r\n", 375 | " Statistics: \r\n", 376 | " Branch and bound: \r\n", 377 | " Number of bounded subproblems: 0\r\n", 378 | " Number of created subproblems: 0\r\n", 379 | " Error rc: 0\r\n", 380 | " Time: 0.00943398475647\r\n", 381 | "# ----------------------------------------------------------\r\n", 382 | "# Solution Information\r\n", 383 | "# ----------------------------------------------------------\r\n", 384 | "Solution: \r\n", 385 | "- number of solutions: 1\r\n", 386 | " number of solutions displayed: 1\r\n", 387 | "- Gap: 0.0\r\n", 388 | " Status: feasible\r\n", 389 | " Message: None\r\n", 390 | " Objective:\r\n", 391 | " total:\r\n", 392 | " Value: 14\r\n", 393 | " Variable:\r\n", 394 | " f[C,E]:\r\n", 395 | " Value: 4\r\n", 396 | " f[C,D]:\r\n", 397 | " Value: 2\r\n", 398 | " f[Zoo,A]:\r\n", 399 | " Value: 7\r\n", 400 | " f[A,C]:\r\n", 401 | " Value: 3\r\n", 402 | " f[E,Home]:\r\n", 403 | " Value: 6\r\n", 404 | " f[B,A]:\r\n", 405 | " Value: 4\r\n", 406 | " f[D,Home]:\r\n", 407 | " Value: 8\r\n", 408 | " f[D,E]:\r\n", 409 | " Value: 2\r\n", 410 | " f[B,C]:\r\n", 411 | " Value: 3\r\n", 412 | " f[Zoo,B]:\r\n", 413 | " Value: 7\r\n", 414 | " f[A,D]:\r\n", 415 | " Value: 8\r\n", 416 | " Constraint: No values\r\n" 417 | ] 418 | } 419 | ], 420 | "source": [ 421 | "!cat results.yml" 422 | ] 423 | }, 424 | { 425 | "cell_type": "markdown", 426 | "metadata": {}, 427 | "source": [ 428 | "This output tells us how many people should travel along each route for the optimal solution. More importantly, though, is the line which says our objective value is 14. This means that at most 14 families can arrive at Home. However, we were told 16 families from Home were expected to visit the zoo each day. Therefore, unless something is done, the public transportation network in place will be overloaded." 429 | ] 430 | }, 431 | { 432 | "cell_type": "markdown", 433 | "metadata": {}, 434 | "source": [ 435 | "## References\n", 436 | "\n", 437 | "* A. Schrijver, (2002). \"On the history of the transportation and maximum flow problems\". Mathematical Programming 91 (3): 437\u2013445. " 438 | ] 439 | } 440 | ], 441 | "metadata": { 442 | "kernelspec": { 443 | "display_name": "Python 3", 444 | "language": "python", 445 | "name": "python3" 446 | }, 447 | "language_info": { 448 | "codemirror_mode": { 449 | "name": "ipython", 450 | "version": 3 451 | }, 452 | "file_extension": ".py", 453 | "mimetype": "text/x-python", 454 | "name": "python", 455 | "nbconvert_exporter": "python", 456 | "pygments_lexer": "ipython3", 457 | "version": "3.6.1" 458 | } 459 | }, 460 | "nbformat": 4, 461 | "nbformat_minor": 1 462 | } -------------------------------------------------------------------------------- /maxflow/maxflow.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | from pyomo.environ import * 14 | 15 | model = AbstractModel() 16 | 17 | # Nodes in the network 18 | model.N = Set() 19 | # Network arcs 20 | model.A = Set(within=model.N*model.N) 21 | 22 | # Source node 23 | model.s = Param(within=model.N) 24 | # Sink node 25 | model.t = Param(within=model.N) 26 | # Flow capacity limits 27 | model.c = Param(model.A) 28 | 29 | # The flow over each arc 30 | model.f = Var(model.A, within=NonNegativeReals) 31 | 32 | # Maximize the flow into the sink nodes 33 | def total_rule(model): 34 | return sum(model.f[i,j] for (i, j) in model.A if j == value(model.t)) 35 | model.total = Objective(rule=total_rule, sense=maximize) 36 | 37 | # Enforce an upper limit on the flow across each arc 38 | def limit_rule(model, i, j): 39 | return model.f[i,j] <= model.c[i, j] 40 | model.limit = Constraint(model.A, rule=limit_rule) 41 | 42 | # Enforce flow through each node 43 | def flow_rule(model, k): 44 | if k == value(model.s) or k == value(model.t): 45 | return Constraint.Skip 46 | inFlow = sum(model.f[i,j] for (i,j) in model.A if j == k) 47 | outFlow = sum(model.f[i,j] for (i,j) in model.A if i == k) 48 | return inFlow == outFlow 49 | model.flow = Constraint(model.N, rule=flow_rule) 50 | 51 | -------------------------------------------------------------------------------- /maxflow/results.yml: -------------------------------------------------------------------------------- 1 | # ========================================================== 2 | # = Solver Results = 3 | # ========================================================== 4 | # ---------------------------------------------------------- 5 | # Problem Information 6 | # ---------------------------------------------------------- 7 | Problem: 8 | - Name: unknown 9 | Lower bound: 14.0 10 | Upper bound: 14.0 11 | Number of objectives: 1 12 | Number of constraints: 17 13 | Number of variables: 12 14 | Number of nonzeros: 30 15 | Sense: maximize 16 | # ---------------------------------------------------------- 17 | # Solver Information 18 | # ---------------------------------------------------------- 19 | Solver: 20 | - Status: ok 21 | Termination condition: optimal 22 | Statistics: 23 | Branch and bound: 24 | Number of bounded subproblems: 0 25 | Number of created subproblems: 0 26 | Error rc: 0 27 | Time: 0.00943398475647 28 | # ---------------------------------------------------------- 29 | # Solution Information 30 | # ---------------------------------------------------------- 31 | Solution: 32 | - number of solutions: 1 33 | number of solutions displayed: 1 34 | - Gap: 0.0 35 | Status: feasible 36 | Message: None 37 | Objective: 38 | total: 39 | Value: 14 40 | Variable: 41 | f[C,E]: 42 | Value: 4 43 | f[C,D]: 44 | Value: 2 45 | f[Zoo,A]: 46 | Value: 7 47 | f[A,C]: 48 | Value: 3 49 | f[E,Home]: 50 | Value: 6 51 | f[B,A]: 52 | Value: 4 53 | f[D,Home]: 54 | Value: 8 55 | f[D,E]: 56 | Value: 2 57 | f[B,C]: 58 | Value: 3 59 | f[Zoo,B]: 60 | Value: 7 61 | f[A,D]: 62 | Value: 8 63 | Constraint: No values 64 | -------------------------------------------------------------------------------- /network_interdiction/max_flow/max_flow_interdict.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# ___________________________________________________________________________\n#\n# Pyomo: Python Optimization Modeling Objects\n# Copyright (c) 2015-2025\n# National Technology and Engineering Solutions of Sandia, LLC\n# Under the terms of Contract DE-NA0003525 with National Technology and\n# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain\n# rights in this software.\n# This software is distributed under the 3-clause BSD License.\n# ___________________________________________________________________________" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "metadata": {}, 16 | "outputs": [ 17 | { 18 | "name": "stdout", 19 | "output_type": "stream", 20 | "text": [ 21 | "import pandas\r\n", 22 | "import pyomo\r\n", 23 | "import pyomo.opt\r\n", 24 | "import pyomo.environ as pe\r\n", 25 | "import logging\r\n", 26 | "\r\n", 27 | "class MaxFlowInterdiction:\r\n", 28 | " \"\"\"A class to compute max-flow interdictions.\"\"\"\r\n", 29 | "\r\n", 30 | " def __init__(self, nodefile, arcfile, attacks=0):\r\n", 31 | " \"\"\"\r\n", 32 | " All the files are CSVs with columns described below. Attacks is the number of attacks.\r\n", 33 | "\r\n", 34 | " - nodefile:\r\n", 35 | " Node\r\n", 36 | "\r\n", 37 | " Every node must appear as a line in the nodefile. There are two special nodes called 'Start' and 'End' that define the source and sink of the max-flow problem. \r\n", 38 | "\r\n", 39 | " - arcfile:\r\n", 40 | " StartNode,EndNode,Capacity,Attackable\r\n", 41 | "\r\n", 42 | " Every arc must appear in the arcfile. The data also describes the arc's capacity and whether we can attack this arc.\r\n", 43 | " \"\"\"\r\n", 44 | " # Read in the node_data\r\n", 45 | " self.node_data = pandas.read_csv(nodefile)\r\n", 46 | " self.node_data.set_index(['Node'], inplace=True)\r\n", 47 | " self.node_data.sort_index(inplace=True)\r\n", 48 | " # Read in the arc_data\r\n", 49 | " self.arc_data = pandas.read_csv(arcfile)\r\n", 50 | " self.arc_data['xbar'] = 0\r\n", 51 | " self.arc_data.set_index(['StartNode','EndNode'], inplace=True)\r\n", 52 | " self.arc_data.sort_index(inplace=True)\r\n", 53 | "\r\n", 54 | " self.attacks = attacks\r\n", 55 | " \r\n", 56 | " self.node_set = self.node_data.index.unique()\r\n", 57 | " self.arc_set = self.arc_data.index.unique()\r\n", 58 | " \r\n", 59 | " self.createPrimal()\r\n", 60 | " self.createInterdictionDual()\r\n", 61 | "\r\n", 62 | "\r\n", 63 | " def createPrimal(self): \r\n", 64 | " \"\"\"Create the primal pyomo model. \r\n", 65 | " \r\n", 66 | " This is used to compute flows after interdiction. The interdiction is stored in arc_data.xbar.\"\"\"\r\n", 67 | "\r\n", 68 | " model = pe.ConcreteModel()\r\n", 69 | " # Tell pyomo to read in dual-variable information from the solver\r\n", 70 | " model.dual = pe.Suffix(direction=pe.Suffix.IMPORT) \r\n", 71 | "\r\n", 72 | " # Add the sets\r\n", 73 | " model.node_set = pe.Set( initialize=self.node_set )\r\n", 74 | " model.edge_set = pe.Set( initialize=self.arc_set, dimen=2)\r\n", 75 | "\r\n", 76 | " # Create the variables\r\n", 77 | " model.y = pe.Var(model.edge_set, domain=pe.NonNegativeReals) \r\n", 78 | " model.v = pe.Var(domain=pe.NonNegativeReals)\r\n", 79 | "\r\n", 80 | " \r\n", 81 | " # Create the objective\r\n", 82 | " def obj_rule(model):\r\n", 83 | " return model.v - 1.1*sum( data['xbar']*model.y[e] for e,data in self.arc_data.iterrows())\r\n", 84 | " model.OBJ = pe.Objective(rule=obj_rule, sense=pe.maximize)\r\n", 85 | "\r\n", 86 | " # Create the constraints, one for each node\r\n", 87 | " def flow_bal_rule(model, n):\r\n", 88 | " tmp = self.arc_data.reset_index()\r\n", 89 | " successors = tmp.ix[ tmp.StartNode == n, 'EndNode'].values\r\n", 90 | " predecessors = tmp.ix[ tmp.EndNode == n, 'StartNode'].values \r\n", 91 | " lhs = sum(model.y[(i,n)] for i in predecessors) - sum(model.y[(n,i)] for i in successors) \r\n", 92 | " start_node = int(n == 'Start')\r\n", 93 | " end_node = int(n == 'End')\r\n", 94 | " rhs = 0 - model.v*(start_node) + model.v*(end_node)\r\n", 95 | " constr = (lhs == rhs)\r\n", 96 | " if isinstance(constr, bool):\r\n", 97 | " return pe.Constraint.Skip\r\n", 98 | " return constr\r\n", 99 | "\r\n", 100 | " model.FlowBalance = pe.Constraint(model.node_set, rule=flow_bal_rule)\r\n", 101 | " \r\n", 102 | " # Capacity constraints, one for each edge\r\n", 103 | " def capacity_rule(model, i, j):\r\n", 104 | " capacity = self.arc_data['Capacity'].get((i,j),-1)\r\n", 105 | " if capacity < 0:\r\n", 106 | " return pe.Constraint.Skip\r\n", 107 | " return model.y[(i,j)] <= capacity \r\n", 108 | "\r\n", 109 | " model.Capacity = pe.Constraint(model.edge_set, rule=capacity_rule)\r\n", 110 | " \r\n", 111 | " # Store the model\r\n", 112 | " self.primal = model\r\n", 113 | "\r\n", 114 | " def createInterdictionDual(self):\r\n", 115 | " # Create the model\r\n", 116 | " model = pe.ConcreteModel()\r\n", 117 | " \r\n", 118 | " # Add the sets\r\n", 119 | " model.node_set = pe.Set( initialize=self.node_set )\r\n", 120 | " model.edge_set = pe.Set( initialize=self.arc_set, dimen=2)\r\n", 121 | "\r\n", 122 | " # Create the variables\r\n", 123 | " model.rho = pe.Var(model.node_set, domain=pe.Reals)\r\n", 124 | " model.pi = pe.Var(model.edge_set, domain=pe.NonNegativeReals)\r\n", 125 | " \r\n", 126 | " model.x = pe.Var(model.edge_set, domain=pe.Binary)\r\n", 127 | "\r\n", 128 | " # Create the objective\r\n", 129 | " def obj_rule(model):\r\n", 130 | " return sum(data['Capacity']*model.pi[e] for e,data in self.arc_data.iterrows() if data['Capacity']>=0)\r\n", 131 | "\r\n", 132 | " model.OBJ = pe.Objective(rule=obj_rule, sense=pe.minimize)\r\n", 133 | "\r\n", 134 | " # Create the constraints for y_ij\r\n", 135 | " def edge_constraint_rule(model, i, j):\r\n", 136 | " attackable = int(self.arc_data['Attackable'].get((i,j),0))\r\n", 137 | " hasCap = int(self.arc_data['Capacity'].get((i,j),-1)>=0)\r\n", 138 | " return model.rho[j] - model.rho[i] + model.pi[(i,j)]*hasCap >= 0 - 1.1*model.x[(i,j)]*attackable\r\n", 139 | "\r\n", 140 | " model.DualEdgeConstraint = pe.Constraint(model.edge_set, rule=edge_constraint_rule)\r\n", 141 | "\r\n", 142 | " # Set the x's for non-blockable arcs\r\n", 143 | " def v_constraint_rule(model):\r\n", 144 | " return model.rho['Start'] - model.rho['End'] == 1\r\n", 145 | "\r\n", 146 | " model.VConstraint = pe.Constraint(rule=v_constraint_rule)\r\n", 147 | " \r\n", 148 | " # Create the interdiction budget constraint \r\n", 149 | " def block_limit_rule(model):\r\n", 150 | " model.attacks = self.attacks\r\n", 151 | " return pe.summation(model.x) <= model.attacks\r\n", 152 | "\r\n", 153 | " model.BlockLimit = pe.Constraint(rule=block_limit_rule)\r\n", 154 | "\r\n", 155 | " # Create, save the model\r\n", 156 | " self.Idual = model\r\n", 157 | "\r\n", 158 | " def solve(self, tee=False):\r\n", 159 | " solver = pyomo.opt.SolverFactory('gurobi')\r\n", 160 | "\r\n", 161 | " # Solve the dual first\r\n", 162 | " self.Idual.BlockLimit.construct()\r\n", 163 | " self.Idual.BlockLimit._constructed = False\r\n", 164 | " del self.Idual.BlockLimit._data[None] \r\n", 165 | " self.Idual.BlockLimit.reconstruct()\r\n", 166 | " self.Idual.preprocess()\r\n", 167 | " results = solver.solve(self.Idual, tee=tee, keepfiles=False, options_string=\"mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0\")\r\n", 168 | "\r\n", 169 | " # Check that we actually computed an optimal solution, load results\r\n", 170 | " if (results.solver.status != pyomo.opt.SolverStatus.ok):\r\n", 171 | " logging.warning('Check solver not ok?')\r\n", 172 | " if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): \r\n", 173 | " logging.warning('Check solver optimality?')\r\n", 174 | "\r\n", 175 | " self.Idual.solutions.load_from(results)\r\n", 176 | " # Now put interdictions into xbar and solve primal\r\n", 177 | " \r\n", 178 | " for e in self.arc_data.index:\r\n", 179 | " self.arc_data.ix[e,'xbar'] = self.Idual.x[e].value\r\n", 180 | "\r\n", 181 | " self.primal.OBJ.construct()\r\n", 182 | " self.primal.OBJ._constructed = False\r\n", 183 | " self.primal.OBJ._init_sense = pe.maximize\r\n", 184 | " del self.primal.OBJ._data[None] \r\n", 185 | " self.primal.OBJ.reconstruct()\r\n", 186 | " self.primal.preprocess()\r\n", 187 | " results = solver.solve(self.primal, tee=tee, keepfiles=False, options_string=\"mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0\")\r\n", 188 | "\r\n", 189 | " # Check that we actually computed an optimal solution, load results\r\n", 190 | " if (results.solver.status != pyomo.opt.SolverStatus.ok):\r\n", 191 | " logging.warning('Check solver not ok?')\r\n", 192 | " if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): \r\n", 193 | " logging.warning('Check solver optimality?')\r\n", 194 | "\r\n", 195 | " self.primal.solutions.load_from(results)\r\n", 196 | "\r\n", 197 | " def printSolution(self):\r\n", 198 | " print()\r\n", 199 | " print('Using %d attacks:'%self.attacks)\r\n", 200 | " print()\r\n", 201 | " edges = sorted(self.arc_set)\r\n", 202 | " for e in edges:\r\n", 203 | " if self.Idual.x[e].value > 0:\r\n", 204 | " print('Interdict arc %s -> %s'%(str(e[0]), str(e[1])))\r\n", 205 | " print()\r\n", 206 | " \r\n", 207 | " for e0,e1 in self.arc_set:\r\n", 208 | " flow = self.primal.y[(e0,e1)].value\r\n", 209 | " if flow > 0:\r\n", 210 | " print('Flow on arc %s -> %s: %.2f'%(str(e0), str(e1), flow))\r\n", 211 | " print()\r\n", 212 | "\r\n", 213 | " print('----------')\r\n", 214 | " print('Total flow = %.2f (primal) %.2f (dual)'%(self.primal.OBJ(), self.Idual.OBJ()))\r\n", 215 | "\r\n", 216 | "\r\n", 217 | "########################\r\n", 218 | "# Now lets do something\r\n", 219 | "########################\r\n", 220 | "\r\n", 221 | "if __name__ == '__main__':\r\n", 222 | " m = MaxFlowInterdiction('sample_nodes_data.csv', 'sample_arcs_data.csv')\r\n", 223 | " m.solve()\r\n", 224 | " m.printSolution()\r\n", 225 | " m.attacks = 1\r\n", 226 | " m.solve()\r\n", 227 | " m.printSolution()\r\n", 228 | " m.attacks = 2\r\n", 229 | " m.solve()\r\n", 230 | " m.printSolution()\r\n" 231 | ] 232 | } 233 | ], 234 | "source": [ 235 | "!cat max_flow_interdict.py" 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": 2, 241 | "metadata": {}, 242 | "outputs": [ 243 | { 244 | "name": "stdout", 245 | "output_type": "stream", 246 | "text": [ 247 | "\r\n", 248 | "Using 0 attacks:\r\n", 249 | "\r\n", 250 | "\r\n", 251 | "Flow on arc B -> End: 40.00\r\n", 252 | "Flow on arc C -> B: 30.00\r\n", 253 | "Flow on arc C -> D: 40.00\r\n", 254 | "Flow on arc D -> End: 40.00\r\n", 255 | "Flow on arc Start -> B: 10.00\r\n", 256 | "Flow on arc Start -> C: 70.00\r\n", 257 | "\r\n", 258 | "----------\r\n", 259 | "Total flow = 80.00 (primal) 80.00 (dual)\r\n", 260 | "\r\n", 261 | "Using 1 attacks:\r\n", 262 | "\r\n", 263 | "Interdict arc Start -> C\r\n", 264 | "\r\n", 265 | "Flow on arc B -> End: 10.00\r\n", 266 | "Flow on arc Start -> B: 10.00\r\n", 267 | "\r\n", 268 | "----------\r\n", 269 | "Total flow = 10.00 (primal) 10.00 (dual)\r\n", 270 | "\r\n", 271 | "Using 2 attacks:\r\n", 272 | "\r\n", 273 | "Interdict arc B -> End\r\n", 274 | "Interdict arc D -> End\r\n", 275 | "\r\n", 276 | "\r\n", 277 | "----------\r\n", 278 | "Total flow = 0.00 (primal) 0.00 (dual)\r\n" 279 | ] 280 | } 281 | ], 282 | "source": [ 283 | "!python max_flow_interdict.py" 284 | ] 285 | } 286 | ], 287 | "metadata": { 288 | "kernelspec": { 289 | "display_name": "Python 3", 290 | "language": "python", 291 | "name": "python3" 292 | }, 293 | "language_info": { 294 | "codemirror_mode": { 295 | "name": "ipython", 296 | "version": 3 297 | }, 298 | "file_extension": ".py", 299 | "mimetype": "text/x-python", 300 | "name": "python", 301 | "nbconvert_exporter": "python", 302 | "pygments_lexer": "ipython3", 303 | "version": "3.6.1" 304 | } 305 | }, 306 | "nbformat": 4, 307 | "nbformat_minor": 1 308 | } -------------------------------------------------------------------------------- /network_interdiction/max_flow/max_flow_interdict.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | import pandas 14 | import pyomo 15 | import pyomo.opt 16 | import pyomo.environ as pe 17 | import logging 18 | 19 | class MaxFlowInterdiction: 20 | """A class to compute max-flow interdictions.""" 21 | 22 | def __init__(self, nodefile, arcfile, attacks=0): 23 | """ 24 | All the files are CSVs with columns described below. Attacks is the number of attacks. 25 | 26 | - nodefile: 27 | Node 28 | 29 | Every node must appear as a line in the nodefile. There are two special nodes called 'Start' and 'End' that define the source and sink of the max-flow problem. 30 | 31 | - arcfile: 32 | StartNode,EndNode,Capacity,Attackable 33 | 34 | Every arc must appear in the arcfile. The data also describes the arc's capacity and whether we can attack this arc. 35 | """ 36 | # Read in the node_data 37 | self.node_data = pandas.read_csv(nodefile) 38 | self.node_data.set_index(['Node'], inplace=True) 39 | self.node_data.sort_index(inplace=True) 40 | # Read in the arc_data 41 | self.arc_data = pandas.read_csv(arcfile) 42 | self.arc_data['xbar'] = 0 43 | self.arc_data.set_index(['StartNode','EndNode'], inplace=True) 44 | self.arc_data.sort_index(inplace=True) 45 | 46 | self.attacks = attacks 47 | 48 | self.node_set = self.node_data.index.unique() 49 | self.arc_set = self.arc_data.index.unique() 50 | 51 | self.createPrimal() 52 | self.createInterdictionDual() 53 | 54 | 55 | def createPrimal(self): 56 | """Create the primal pyomo model. 57 | 58 | This is used to compute flows after interdiction. The interdiction is stored in arc_data.xbar.""" 59 | 60 | model = pe.ConcreteModel() 61 | # Tell pyomo to read in dual-variable information from the solver 62 | model.dual = pe.Suffix(direction=pe.Suffix.IMPORT) 63 | 64 | # Add the sets 65 | model.node_set = pe.Set( initialize=self.node_set ) 66 | model.edge_set = pe.Set( initialize=self.arc_set, dimen=2) 67 | 68 | # Create the variables 69 | model.y = pe.Var(model.edge_set, domain=pe.NonNegativeReals) 70 | model.v = pe.Var(domain=pe.NonNegativeReals) 71 | 72 | 73 | # Create the objective 74 | def obj_rule(model): 75 | return model.v - 1.1*sum( data['xbar']*model.y[e] for e,data in self.arc_data.iterrows()) 76 | model.OBJ = pe.Objective(rule=obj_rule, sense=pe.maximize) 77 | 78 | # Create the constraints, one for each node 79 | def flow_bal_rule(model, n): 80 | tmp = self.arc_data.reset_index() 81 | successors = tmp.ix[ tmp.StartNode == n, 'EndNode'].values 82 | predecessors = tmp.ix[ tmp.EndNode == n, 'StartNode'].values 83 | lhs = sum(model.y[(i,n)] for i in predecessors) - sum(model.y[(n,i)] for i in successors) 84 | start_node = int(n == 'Start') 85 | end_node = int(n == 'End') 86 | rhs = 0 - model.v*(start_node) + model.v*(end_node) 87 | constr = (lhs == rhs) 88 | if isinstance(constr, bool): 89 | return pe.Constraint.Skip 90 | return constr 91 | 92 | model.FlowBalance = pe.Constraint(model.node_set, rule=flow_bal_rule) 93 | 94 | # Capacity constraints, one for each edge 95 | def capacity_rule(model, i, j): 96 | capacity = self.arc_data['Capacity'].get((i,j),-1) 97 | if capacity < 0: 98 | return pe.Constraint.Skip 99 | return model.y[(i,j)] <= capacity 100 | 101 | model.Capacity = pe.Constraint(model.edge_set, rule=capacity_rule) 102 | 103 | # Store the model 104 | self.primal = model 105 | 106 | def createInterdictionDual(self): 107 | # Create the model 108 | model = pe.ConcreteModel() 109 | 110 | # Add the sets 111 | model.node_set = pe.Set( initialize=self.node_set ) 112 | model.edge_set = pe.Set( initialize=self.arc_set, dimen=2) 113 | 114 | # Create the variables 115 | model.rho = pe.Var(model.node_set, domain=pe.Reals) 116 | model.pi = pe.Var(model.edge_set, domain=pe.NonNegativeReals) 117 | 118 | model.x = pe.Var(model.edge_set, domain=pe.Binary) 119 | 120 | # Create the objective 121 | def obj_rule(model): 122 | return sum(data['Capacity']*model.pi[e] for e,data in self.arc_data.iterrows() if data['Capacity']>=0) 123 | 124 | model.OBJ = pe.Objective(rule=obj_rule, sense=pe.minimize) 125 | 126 | # Create the constraints for y_ij 127 | def edge_constraint_rule(model, i, j): 128 | attackable = int(self.arc_data['Attackable'].get((i,j),0)) 129 | hasCap = int(self.arc_data['Capacity'].get((i,j),-1)>=0) 130 | return model.rho[j] - model.rho[i] + model.pi[(i,j)]*hasCap >= 0 - 1.1*model.x[(i,j)]*attackable 131 | 132 | model.DualEdgeConstraint = pe.Constraint(model.edge_set, rule=edge_constraint_rule) 133 | 134 | # Set the x's for non-blockable arcs 135 | def v_constraint_rule(model): 136 | return model.rho['Start'] - model.rho['End'] == 1 137 | 138 | model.VConstraint = pe.Constraint(rule=v_constraint_rule) 139 | 140 | # Create the interdiction budget constraint 141 | def block_limit_rule(model): 142 | model.attacks = self.attacks 143 | return pe.summation(model.x) <= model.attacks 144 | 145 | model.BlockLimit = pe.Constraint(rule=block_limit_rule) 146 | 147 | # Create, save the model 148 | self.Idual = model 149 | 150 | def solve(self, tee=False): 151 | solver = pyomo.opt.SolverFactory('gurobi') 152 | 153 | # Solve the dual first 154 | self.Idual.BlockLimit.construct() 155 | self.Idual.BlockLimit._constructed = False 156 | del self.Idual.BlockLimit._data[None] 157 | self.Idual.BlockLimit.reconstruct() 158 | self.Idual.preprocess() 159 | results = solver.solve(self.Idual, tee=tee, keepfiles=False, options_string="mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0") 160 | 161 | # Check that we actually computed an optimal solution, load results 162 | if (results.solver.status != pyomo.opt.SolverStatus.ok): 163 | logging.warning('Check solver not ok?') 164 | if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): 165 | logging.warning('Check solver optimality?') 166 | 167 | self.Idual.solutions.load_from(results) 168 | # Now put interdictions into xbar and solve primal 169 | 170 | for e in self.arc_data.index: 171 | self.arc_data.ix[e,'xbar'] = self.Idual.x[e].value 172 | 173 | self.primal.OBJ.construct() 174 | self.primal.OBJ._constructed = False 175 | self.primal.OBJ._init_sense = pe.maximize 176 | del self.primal.OBJ._data[None] 177 | self.primal.OBJ.reconstruct() 178 | self.primal.preprocess() 179 | results = solver.solve(self.primal, tee=tee, keepfiles=False, options_string="mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0") 180 | 181 | # Check that we actually computed an optimal solution, load results 182 | if (results.solver.status != pyomo.opt.SolverStatus.ok): 183 | logging.warning('Check solver not ok?') 184 | if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): 185 | logging.warning('Check solver optimality?') 186 | 187 | self.primal.solutions.load_from(results) 188 | 189 | def printSolution(self): 190 | print() 191 | print('Using %d attacks:'%self.attacks) 192 | print() 193 | edges = sorted(self.arc_set) 194 | for e in edges: 195 | if self.Idual.x[e].value > 0: 196 | print('Interdict arc %s -> %s'%(str(e[0]), str(e[1]))) 197 | print() 198 | 199 | for e0,e1 in self.arc_set: 200 | flow = self.primal.y[(e0,e1)].value 201 | if flow > 0: 202 | print('Flow on arc %s -> %s: %.2f'%(str(e0), str(e1), flow)) 203 | print() 204 | 205 | print('----------') 206 | print('Total flow = %.2f (primal) %.2f (dual)'%(self.primal.OBJ(), self.Idual.OBJ())) 207 | 208 | 209 | ######################## 210 | # Now lets do something 211 | ######################## 212 | 213 | if __name__ == '__main__': 214 | m = MaxFlowInterdiction('sample_nodes_data.csv', 'sample_arcs_data.csv') 215 | m.solve() 216 | m.printSolution() 217 | m.attacks = 1 218 | m.solve() 219 | m.printSolution() 220 | m.attacks = 2 221 | m.solve() 222 | m.printSolution() 223 | -------------------------------------------------------------------------------- /network_interdiction/max_flow/sample_arcs_data.csv: -------------------------------------------------------------------------------- 1 | StartNode,EndNode,Capacity,Attackable 2 | Start,B,10,1 3 | Start,C,70,1 4 | C,B,30,1 5 | B,End,100,1 6 | C,D,40,1 7 | D,End,50,1 8 | -------------------------------------------------------------------------------- /network_interdiction/max_flow/sample_nodes_data.csv: -------------------------------------------------------------------------------- 1 | Node 2 | Start 3 | B 4 | C 5 | D 6 | End 7 | -------------------------------------------------------------------------------- /network_interdiction/min_cost_flow/min_cost_flow_interdict.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | { 14 | "metadata": { 15 | "name": "", 16 | "signature": "sha256:3203a62c794b056e9202075c3d2f3cde57af44eeb452a1c3f9b1d52b52e69f8a" 17 | }, 18 | "nbformat": 3, 19 | "nbformat_minor": 0, 20 | "worksheets": [ 21 | { 22 | "cells": [ 23 | { 24 | "cell_type": "code", 25 | "collapsed": false, 26 | "input": [ 27 | "%load min_cost_flow_interdict.py" 28 | ], 29 | "language": "python", 30 | "metadata": {}, 31 | "outputs": [], 32 | "prompt_number": 1 33 | }, 34 | { 35 | "cell_type": "code", 36 | "collapsed": false, 37 | "input": [ 38 | "import networkx\n", 39 | "import pandas\n", 40 | "import pyomo\n", 41 | "import pyomo.opt\n", 42 | "import pyomo.environ as pe\n", 43 | "import scipy\n", 44 | "import itertools\n", 45 | "import logging\n", 46 | "\n", 47 | "class MinCostFlowInterdiction:\n", 48 | " \"\"\"A class to compute min-cost-flow interdictions.\"\"\"\n", 49 | "\n", 50 | " def __init__(self, nodefile, arcfile, attacks=0):\n", 51 | " \"\"\"\n", 52 | " All the files are CSVs with columns described below. Attacks is the number of attacks.\n", 53 | "\n", 54 | " - nodefile:\n", 55 | " Node, SupplyDemand\n", 56 | "\n", 57 | " Every node must appear as a line in the nodefile. SupplyDemand describes the flow imbalance at the node.\n", 58 | "\n", 59 | " - arcfile:\n", 60 | " StartNode,EndNode,Capacity,Cost,Attackable\n", 61 | "\n", 62 | " Every arc must appear in the arcfile. The data also describes the arc's capacity, cost, and whether we can attack this arc.\n", 63 | " \"\"\"\n", 64 | " # Read in the node_data\n", 65 | " self.node_data = pandas.read_csv(nodefile)\n", 66 | " self.node_data.set_index(['Node'], inplace=True)\n", 67 | " self.node_data.sort_index(inplace=True)\n", 68 | " # Read in the arc_data\n", 69 | " self.arc_data = pandas.read_csv(arcfile)\n", 70 | " self.arc_data['xbar'] = 0\n", 71 | " self.arc_data.set_index(['StartNode','EndNode'], inplace=True)\n", 72 | " self.arc_data.sort_index(inplace=True)\n", 73 | "\n", 74 | " self.attacks = attacks\n", 75 | " \n", 76 | " self.node_set = self.node_data.index.unique()\n", 77 | " self.arc_set = self.arc_data.index.unique()\n", 78 | " \n", 79 | " # Compute nCmax\n", 80 | " self.nCmax = len(self.node_set) * self.arc_data['Cost'].max()\n", 81 | "\n", 82 | " self.createPrimal()\n", 83 | " self.createInterdictionDual()\n", 84 | "\n", 85 | "\n", 86 | " def createPrimal(self): \n", 87 | " \"\"\"Create the primal pyomo model. \n", 88 | " \n", 89 | " This is used to compute flows after interdiction. The interdiction is stored in arc_data.xbar.\"\"\"\n", 90 | "\n", 91 | " model = pe.ConcreteModel()\n", 92 | " # Tell pyomo to read in dual-variable information from the solver\n", 93 | " model.dual = pe.Suffix(direction=pe.Suffix.IMPORT) \n", 94 | "\n", 95 | " # Add the sets\n", 96 | " model.node_set = pe.Set( initialize=self.node_set )\n", 97 | " model.edge_set = pe.Set( initialize=self.arc_set, dimen=2)\n", 98 | "\n", 99 | " # Create the variables\n", 100 | " model.y = pe.Var(model.edge_set, domain=pe.NonNegativeReals) \n", 101 | " model.UnsatSupply = pe.Var(model.node_set, domain=pe.NonNegativeReals)\n", 102 | " model.UnsatDemand = pe.Var(model.node_set, domain=pe.NonNegativeReals)\n", 103 | " \n", 104 | " # Create the objective\n", 105 | " def obj_rule(model):\n", 106 | " return sum( (data['Cost']+data['xbar']*(2*self.nCmax+1))*model.y[e] for e,data in self.arc_data.iterrows()) + sum(self.nCmax*(model.UnsatSupply[n] + model.UnsatDemand[n]) for n,data in self.node_data.iterrows())\n", 107 | " model.OBJ = pe.Objective(rule=obj_rule, sense=pe.minimize)\n", 108 | "\n", 109 | " # Create the constraints, one for each node\n", 110 | " def flow_bal_rule(model, n):\n", 111 | " tmp = self.arc_data.reset_index()\n", 112 | " successors = tmp.ix[ tmp.StartNode == n, 'EndNode'].values\n", 113 | " predecessors = tmp.ix[ tmp.EndNode == n, 'StartNode'].values \n", 114 | " lhs = sum(model.y[(i,n)] for i in predecessors) - sum(model.y[(n,i)] for i in successors) \n", 115 | " imbalance = self.node_data['SupplyDemand'].get(n,0)\n", 116 | " supply_node = int(imbalance < 0)\n", 117 | " demand_node = int(imbalance > 0)\n", 118 | " rhs = (imbalance + model.UnsatSupply[n]*(supply_node) - model.UnsatDemand[n]*(demand_node))\n", 119 | " constr = (lhs == rhs)\n", 120 | " if isinstance(constr, bool):\n", 121 | " return pe.Constraint.Skip\n", 122 | " return constr\n", 123 | "\n", 124 | " model.FlowBalance = pe.Constraint(model.node_set, rule=flow_bal_rule)\n", 125 | " \n", 126 | " # Capacity constraints, one for each edge\n", 127 | " def capacity_rule(model, i, j):\n", 128 | " capacity = self.arc_data['Capacity'].get((i,j),-1)\n", 129 | " if capacity < 0:\n", 130 | " return pe.Constraint.Skip\n", 131 | " return model.y[(i,j)] <= capacity \n", 132 | "\n", 133 | " model.Capacity = pe.Constraint(model.edge_set, rule=capacity_rule)\n", 134 | " \n", 135 | " # Store the model\n", 136 | " self.primal = model\n", 137 | "\n", 138 | " def createInterdictionDual(self):\n", 139 | " # Create the model\n", 140 | " model = pe.ConcreteModel()\n", 141 | " \n", 142 | " # Add the sets\n", 143 | " model.node_set = pe.Set( initialize=self.node_set )\n", 144 | " model.edge_set = pe.Set( initialize=self.arc_set, dimen=2)\n", 145 | "\n", 146 | " # Create the variables\n", 147 | " model.rho = pe.Var(model.node_set, domain=pe.Reals)\n", 148 | " model.pi = pe.Var(model.edge_set, domain=pe.NonPositiveReals)\n", 149 | " \n", 150 | " model.x = pe.Var(model.edge_set, domain=pe.Binary)\n", 151 | "\n", 152 | " # Create the objective\n", 153 | " def obj_rule(model):\n", 154 | " return sum(data['Capacity']*model.pi[e] for e,data in self.arc_data.iterrows() if data['Capacity']>=0) +\\\n", 155 | " sum(data['SupplyDemand']*model.rho[n] for n,data in self.node_data.iterrows())\n", 156 | "\n", 157 | " model.OBJ = pe.Objective(rule=obj_rule, sense=pe.maximize)\n", 158 | "\n", 159 | " # Create the constraints for y_ij\n", 160 | " def edge_constraint_rule(model, i, j):\n", 161 | " attackable = int(self.arc_data['Attackable'].get((i,j),0))\n", 162 | " hasCap = int(self.arc_data['Capacity'].get((i,j),-1)>=0)\n", 163 | " return model.rho[j] - model.rho[i] + model.pi[(i,j)]*hasCap <= self.arc_data['Cost'].get((i,j),0) + (2*self.nCmax+1)*model.x[(i,j)]*attackable\n", 164 | "\n", 165 | " model.DualEdgeConstraint = pe.Constraint(model.edge_set, rule=edge_constraint_rule)\n", 166 | " \n", 167 | " # Create constraints for the UnsatDemand variables \n", 168 | " def unsat_constraint_rule(model, n):\n", 169 | " imbalance = self.node_data['SupplyDemand'].get(n,0)\n", 170 | " supply_node = int(imbalance < 0)\n", 171 | " demand_node = int(imbalance > 0)\n", 172 | " if (supply_node):\n", 173 | " return -model.rho[n] <= self.nCmax\n", 174 | " if (demand_node):\n", 175 | " return model.rho[n] <= self.nCmax\n", 176 | " return pe.Constraint.Skip\n", 177 | "\n", 178 | " model.UnsatConstraint = pe.Constraint(model.node_set, rule=unsat_constraint_rule)\n", 179 | " \n", 180 | " # Create the interdiction budget constraint \n", 181 | " def block_limit_rule(model):\n", 182 | " model.attacks = self.attacks\n", 183 | " return pe.summation(model.x) <= model.attacks\n", 184 | "\n", 185 | " model.BlockLimit = pe.Constraint(rule=block_limit_rule)\n", 186 | "\n", 187 | " # Create, save the model\n", 188 | " self.Idual = model\n", 189 | "\n", 190 | " def solve(self, tee=False):\n", 191 | " solver = pyomo.opt.SolverFactory('cplex')\n", 192 | "\n", 193 | " # Solve the dual first\n", 194 | " self.Idual.BlockLimit.construct()\n", 195 | " self.Idual.BlockLimit._constructed = False\n", 196 | " del self.Idual.BlockLimit._data[None] \n", 197 | " self.Idual.BlockLimit.reconstruct()\n", 198 | " self.Idual.preprocess()\n", 199 | " results = solver.solve(self.Idual, tee=tee, keepfiles=False, options_string=\"mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0\")\n", 200 | "\n", 201 | " # Check that we actually computed an optimal solution, load results\n", 202 | " if (results.solver.status != pyomo.opt.SolverStatus.ok):\n", 203 | " logging.warning('Check solver not ok?')\n", 204 | " if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): \n", 205 | " logging.warning('Check solver optimality?')\n", 206 | "\n", 207 | " self.Idual.solutions.load_from(results)\n", 208 | " # Now put interdictions into xbar and solve primal\n", 209 | " \n", 210 | " for e in self.arc_data.index:\n", 211 | " self.arc_data.ix[e,'xbar'] = self.Idual.x[e].value\n", 212 | "\n", 213 | " self.primal.OBJ.construct()\n", 214 | " self.primal.OBJ._constructed = False\n", 215 | " self.primal.OBJ._init_sense = pe.minimize\n", 216 | " del self.primal.OBJ._data[None] \n", 217 | " self.primal.OBJ.reconstruct()\n", 218 | " self.primal.preprocess()\n", 219 | " results = solver.solve(self.primal, tee=tee, keepfiles=False, options_string=\"mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0\")\n", 220 | "\n", 221 | " # Check that we actually computed an optimal solution, load results\n", 222 | " if (results.solver.status != pyomo.opt.SolverStatus.ok):\n", 223 | " logging.warning('Check solver not ok?')\n", 224 | " if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): \n", 225 | " logging.warning('Check solver optimality?')\n", 226 | "\n", 227 | " self.primal.solutions.load_from(results)\n", 228 | "\n", 229 | " def printSolution(self):\n", 230 | " print\n", 231 | " print 'Using %d attacks:'%self.attacks\n", 232 | " print\n", 233 | " edges = sorted(self.arc_set)\n", 234 | " for e in edges:\n", 235 | " if self.Idual.x[e].value > 0:\n", 236 | " print 'Interdict arc %s -> %s'%(str(e[0]), str(e[1]))\n", 237 | " print\n", 238 | " \n", 239 | " nodes = sorted(self.node_data.index)\n", 240 | " for n in nodes:\n", 241 | " remaining_supply = self.primal.UnsatSupply[n].value\n", 242 | " if remaining_supply > 0:\n", 243 | " print 'Remaining supply on node %s: %.2f'%(str(n), remaining_supply)\n", 244 | " for n in nodes:\n", 245 | " remaining_demand = self.primal.UnsatDemand[n].value\n", 246 | " if remaining_demand > 0:\n", 247 | " print 'Remaining demand on node %s: %.2f'%(str(n), remaining_demand)\n", 248 | " print\n", 249 | " \n", 250 | " for e0,e1 in self.arc_set:\n", 251 | " flow = self.primal.y[(e0,e1)].value\n", 252 | " if flow > 0:\n", 253 | " print 'Flow on arc %s -> %s: %.2f'%(str(e0), str(e1), flow)\n", 254 | " print\n", 255 | "\n", 256 | " print '----------'\n", 257 | " print 'Total cost = %.2f (primal) %.2f (dual)'%(self.primal.OBJ(), self.Idual.OBJ())\n", 258 | "\n", 259 | "\n", 260 | "########################\n", 261 | "# Now lets do something\n", 262 | "########################\n", 263 | "\n", 264 | "if __name__ == '__main__':\n", 265 | " m = MinCostFlowInterdiction('sample_nodes_data.csv', 'sample_arcs_data.csv')\n", 266 | " m.solve()\n", 267 | " m.printSolution()\n", 268 | " m.attacks = 1\n", 269 | " m.solve()\n", 270 | " m.printSolution()\n", 271 | " m.attacks = 2\n", 272 | " m.solve()\n", 273 | " m.printSolution()\n" 274 | ], 275 | "language": "python", 276 | "metadata": {}, 277 | "outputs": [ 278 | { 279 | "output_type": "stream", 280 | "stream": "stdout", 281 | "text": [ 282 | "\n", 283 | "Using 0 attacks:\n", 284 | "\n", 285 | "\n", 286 | "\n", 287 | "Flow on arc B -> End: 30.00\n", 288 | "Flow on arc C -> B: 20.00\n", 289 | "Flow on arc Start -> B: 10.00\n", 290 | "Flow on arc Start -> C: 10.00\n", 291 | "\n", 292 | "----------\n", 293 | "Total cost = 700.00 (primal) 700.00 (dual)\n" 294 | ] 295 | }, 296 | { 297 | "output_type": "stream", 298 | "stream": "stdout", 299 | "text": [ 300 | "\n", 301 | "Using 1 attacks:\n", 302 | "\n", 303 | "Interdict arc Start -> C\n", 304 | "\n", 305 | "Remaining supply on node Start: 10.00\n", 306 | "Remaining demand on node End: 10.00\n", 307 | "\n", 308 | "Flow on arc B -> End: 20.00\n", 309 | "Flow on arc C -> B: 10.00\n", 310 | "Flow on arc Start -> B: 10.00\n", 311 | "\n", 312 | "----------\n", 313 | "Total cost = 7300.00 (primal) 7300.00 (dual)\n" 314 | ] 315 | }, 316 | { 317 | "output_type": "stream", 318 | "stream": "stdout", 319 | "text": [ 320 | "\n", 321 | "Using 2 attacks:\n", 322 | "\n", 323 | "Interdict arc B -> End\n", 324 | "Interdict arc C -> D\n", 325 | "\n", 326 | "Remaining supply on node C: 10.00\n", 327 | "Remaining supply on node Start: 20.00\n", 328 | "Remaining demand on node End: 30.00\n", 329 | "\n", 330 | "\n", 331 | "----------\n", 332 | "Total cost = 21000.00 (primal) 21000.00 (dual)\n" 333 | ] 334 | } 335 | ], 336 | "prompt_number": 2 337 | }, 338 | { 339 | "cell_type": "code", 340 | "collapsed": false, 341 | "input": [], 342 | "language": "python", 343 | "metadata": {}, 344 | "outputs": [] 345 | } 346 | ], 347 | "metadata": {} 348 | } 349 | ] 350 | } -------------------------------------------------------------------------------- /network_interdiction/min_cost_flow/sample_arcs_data.csv: -------------------------------------------------------------------------------- 1 | StartNode,EndNode,Capacity,Cost,Attackable 2 | Start,B,10,0,1 3 | Start,C,70,10,1 4 | C,B,30,30,1 5 | B,End,100,0,1 6 | C,D,40,20,1 7 | D,End,50,70,1 8 | -------------------------------------------------------------------------------- /network_interdiction/min_cost_flow/sample_nodes_data.csv: -------------------------------------------------------------------------------- 1 | Node,SupplyDemand 2 | Start,-20 3 | B,0 4 | C,-10 5 | D,0 6 | End,30 7 | -------------------------------------------------------------------------------- /network_interdiction/multi_commodity_flow/multi_commodity_flow_interdict.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | import pandas 14 | import pyomo 15 | import pyomo.opt 16 | import pyomo.environ as pe 17 | import logging 18 | 19 | class MultiCommodityInterdiction: 20 | """A class to compute multicommodity flow interdictions.""" 21 | 22 | def __init__(self, nodefile, node_commodity_file, arcfile, arc_commodity_file, attacks=0): 23 | """ 24 | All the files are CSVs with columns described below. Attacks is the number of attacks. 25 | 26 | - nodefile: 27 | Node 28 | 29 | Every node must appear as a line in the nodefile. You can have additional columns as well. 30 | 31 | - node_commodity_file: 32 | Node,Commodity,SupplyDemand 33 | 34 | Every commodity node imbalance that is not zero must appear in the node_commodity_file 35 | 36 | - arcfile: 37 | StartNode,EndNode,Capacity,Attackable 38 | 39 | Every arc must appear in the arcfile. Also the arcs total capacity and whether we can attack this arc. 40 | 41 | - arc_commodity_file: 42 | StartNode,EndNode,Commodity,Cost,Capacity 43 | 44 | This file specifies the costs and capacities of moving each commodity across each arc. If an (node, node, commodity) tuple does not appear in this file, then it means the commodity cannot flow across that edge. 45 | """ 46 | # Read in the node_data 47 | self.node_data = pandas.read_csv(nodefile) 48 | self.node_data.set_index(['Node'], inplace=True) 49 | self.node_data.sort_index(inplace=True) 50 | # Read in the node_commodity_data 51 | self.node_commodity_data = pandas.read_csv(node_commodity_file) 52 | self.node_commodity_data.set_index(['Node','Commodity'], inplace=True) 53 | self.node_commodity_data.sort_index(inplace=True) 54 | # Read in the arc_data 55 | self.arc_data = pandas.read_csv(arcfile) 56 | self.arc_data.set_index(['StartNode','EndNode'], inplace=True) 57 | self.arc_data.sort_index(inplace=True) 58 | # Read in the arc_commodity_data 59 | self.arc_commodity_data = pandas.read_csv(arc_commodity_file) 60 | self.arc_commodity_data['xbar'] = 0 61 | self.arc_commodity_data.set_index(['StartNode','EndNode','Commodity'], inplace=True) 62 | self.arc_commodity_data.sort_index(inplace=True) 63 | # Can df.reset_index() to go back 64 | 65 | self.attacks = attacks 66 | 67 | self.node_set = self.node_data.index.unique() 68 | self.commodity_set = self.node_commodity_data.index.levels[1].unique() 69 | self.arc_set = self.arc_data.index.unique() 70 | 71 | # Compute nCmax 72 | self.nCmax = len(self.node_set) * self.arc_commodity_data['Cost'].max() 73 | 74 | self.createPrimal() 75 | self.createInterdictionDual() 76 | 77 | 78 | def createPrimal(self): 79 | """Create the primal pyomo model. 80 | 81 | This is used to compute flows after interdiction. The interdiction is stored in arc_commodity_data.xbar.""" 82 | 83 | model = pe.ConcreteModel() 84 | # Tell pyomo to read in dual-variable information from the solver 85 | model.dual = pe.Suffix(direction=pe.Suffix.IMPORT) 86 | 87 | # Add the sets 88 | model.node_set = pe.Set( initialize=self.node_set ) 89 | model.edge_set = pe.Set( initialize=self.arc_set, dimen=2) 90 | model.commodity_set = pe.Set( initialize=self.commodity_set ) 91 | 92 | # Create the variables 93 | model.y = pe.Var(model.edge_set*model.commodity_set, domain=pe.NonNegativeReals) 94 | model.UnsatSupply = pe.Var(model.node_set*model.commodity_set, domain=pe.NonNegativeReals) 95 | model.UnsatDemand = pe.Var(model.node_set*model.commodity_set, domain=pe.NonNegativeReals) 96 | 97 | # Create the objective 98 | def obj_rule(model): 99 | return sum( (data['Cost']+data['xbar']*(2*self.nCmax+1))*model.y[e] for e,data in self.arc_commodity_data.iterrows()) + sum(self.nCmax*(model.UnsatSupply[n] + model.UnsatDemand[n]) for n,data in self.node_commodity_data.iterrows()) 100 | model.OBJ = pe.Objective(rule=obj_rule, sense=pe.minimize) 101 | 102 | # Create the constraints, one for each node 103 | def flow_bal_rule(model, n,k): 104 | tmp = self.arc_data.reset_index() 105 | successors = tmp.ix[ tmp.StartNode == n, 'EndNode'].values 106 | predecessors = tmp.ix[ tmp.EndNode == n, 'StartNode'].values 107 | lhs = sum(model.y[(i,n,k)] for i in predecessors) - sum(model.y[(n,i,k)] for i in successors) 108 | imbalance = self.node_commodity_data['SupplyDemand'].get((n,k),0) 109 | supply_node = int(imbalance < 0) 110 | demand_node = int(imbalance > 0) 111 | rhs = (imbalance + model.UnsatSupply[n,k]*(supply_node) - model.UnsatDemand[n,k]*(demand_node)) 112 | constr = (lhs == rhs) 113 | if isinstance(constr, bool): 114 | return pe.Constraint.Skip 115 | return constr 116 | 117 | model.FlowBalance = pe.Constraint(model.node_set*model.commodity_set, rule=flow_bal_rule) 118 | 119 | # Capacity constraints, one for each edge and commodity 120 | def capacity_rule(model, i, j, k): 121 | capacity = self.arc_commodity_data['Capacity'].get((i,j,k),-1) 122 | if capacity < 0: 123 | return pe.Constraint.Skip 124 | return model.y[(i,j,k)] <= capacity 125 | 126 | model.Capacity = pe.Constraint(model.edge_set*model.commodity_set, rule=capacity_rule) 127 | 128 | # Joint capacity constraints, one for each edge 129 | def joint_capacity_rule(model, i, j): 130 | capacity = self.arc_data['Capacity'].get((i,j), -1) 131 | if capacity < 0: 132 | return pe.Constraint.Skip 133 | return sum(model.y[(i,j,k)] for k in self.commodity_set) <= capacity 134 | 135 | model.JointCapacity = pe.Constraint(model.edge_set, rule=joint_capacity_rule) 136 | 137 | # Store the model 138 | self.primal = model 139 | 140 | def createInterdictionDual(self): 141 | # Create the model 142 | model = pe.ConcreteModel() 143 | 144 | # Add the sets 145 | model.node_set = pe.Set( initialize=self.node_set ) 146 | model.edge_set = pe.Set( initialize=self.arc_set, dimen=2) 147 | model.commodity_set = pe.Set( initialize=self.commodity_set ) 148 | 149 | # Create the variables 150 | model.rho = pe.Var(model.node_set*model.commodity_set, domain=pe.Reals) 151 | model.piSingle = pe.Var(model.edge_set*model.commodity_set, domain=pe.NonPositiveReals) 152 | model.piJoint = pe.Var(model.edge_set, domain=pe.NonPositiveReals) 153 | 154 | model.x = pe.Var(model.edge_set, domain=pe.Binary) 155 | 156 | # Create the objective 157 | def obj_rule(model): 158 | return sum(data['Capacity']*model.piJoint[e] for e,data in self.arc_data.iterrows() if data['Capacity']>=0) +\ 159 | sum(data['Capacity']*model.piSingle[e] for e,data in self.arc_commodity_data.iterrows() if data['Capacity']>=0)+\ 160 | sum(data['SupplyDemand']*model.rho[n] for n,data in self.node_commodity_data.iterrows()) 161 | 162 | model.OBJ = pe.Objective(rule=obj_rule, sense=pe.maximize) 163 | 164 | # Create the constraints for y_ijk 165 | def edge_constraint_rule(model, i, j, k): 166 | if (i,j,k) not in self.arc_commodity_data.index: 167 | return pe.Constraint.Skip 168 | attackable = int(self.arc_data['Attackable'].get((i,j),0)) 169 | hasSingleCap = int(self.arc_commodity_data['Capacity'].get((i,j,k),-1)>=0) 170 | hasJointCap = int(self.arc_data['Capacity'].get((i,j),-1)>=0) 171 | return model.rho[(j,k)] - model.rho[(i,k)] + model.piSingle[(i,j,k)]*hasSingleCap + model.piJoint[(i,j)]*hasJointCap <= self.arc_commodity_data['Cost'].get((i,j,k)) + (2*self.nCmax+1)*model.x[(i,j)]*attackable 172 | 173 | model.DualEdgeConstraint = pe.Constraint(model.edge_set*model.commodity_set, rule=edge_constraint_rule) 174 | 175 | # Create constraints for the UnsatDemand variables 176 | def unsat_constraint_rule(model, n, k): 177 | if (n,k) not in self.node_commodity_data.index: 178 | return pe.Constraint.Skip 179 | imbalance = self.node_commodity_data['SupplyDemand'].get((n,k),0) 180 | supply_node = int(imbalance < 0) 181 | demand_node = int(imbalance > 0) 182 | if (supply_node): 183 | return -model.rho[(n,k)] <= self.nCmax 184 | if (demand_node): 185 | return model.rho[(n,k)] <= self.nCmax 186 | return pe.Constraint.Skip 187 | 188 | model.UnsatConstraint = pe.Constraint(model.node_set*model.commodity_set, rule=unsat_constraint_rule) 189 | 190 | # Create the interdiction budget constraint 191 | def block_limit_rule(model): 192 | model.attacks = self.attacks 193 | return pe.summation(model.x) <= model.attacks 194 | 195 | model.BlockLimit = pe.Constraint(rule=block_limit_rule) 196 | 197 | # Create, save the model 198 | self.Idual = model 199 | 200 | def solve(self, tee=False): 201 | solver = pyomo.opt.SolverFactory('cplex') 202 | 203 | # Solve the dual first 204 | self.Idual.BlockLimit.construct() 205 | self.Idual.BlockLimit._constructed = False 206 | del self.Idual.BlockLimit._data[None] 207 | self.Idual.BlockLimit.reconstruct() 208 | self.Idual.preprocess() 209 | results = solver.solve(self.Idual, tee=tee, keepfiles=False, options_string="mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0") 210 | 211 | # Check that we actually computed an optimal solution, load results 212 | if (results.solver.status != pyomo.opt.SolverStatus.ok): 213 | logging.warning('Check solver not ok?') 214 | if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): 215 | logging.warning('Check solver optimality?') 216 | 217 | self.Idual.solutions.load_from(results) 218 | # Now put interdictions into xbar and solve primal 219 | 220 | for e in self.arc_data.index: 221 | self.arc_commodity_data.ix[e,'xbar'] = self.Idual.x[e].value 222 | 223 | self.primal.OBJ.construct() 224 | self.primal.OBJ._constructed = False 225 | self.primal.OBJ._init_sense = pe.minimize 226 | del self.primal.OBJ._data[None] 227 | self.primal.OBJ.reconstruct() 228 | self.primal.preprocess() 229 | results = solver.solve(self.primal, tee=tee, keepfiles=False, options_string="mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0") 230 | 231 | # Check that we actually computed an optimal solution, load results 232 | if (results.solver.status != pyomo.opt.SolverStatus.ok): 233 | logging.warning('Check solver not ok?') 234 | if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): 235 | logging.warning('Check solver optimality?') 236 | 237 | self.primal.solutions.load_from(results) 238 | 239 | def printSolution(self): 240 | print() 241 | print('Using %d attacks:'%self.attacks) 242 | print() 243 | edges = sorted(self.arc_set) 244 | for e in edges: 245 | if self.Idual.x[e].value > 0: 246 | print('Interdict arc %s -> %s'%(str(e[0]), str(e[1]))) 247 | print() 248 | 249 | nodes = sorted(self.node_commodity_data.index) 250 | for n in nodes: 251 | remaining_supply = self.primal.UnsatSupply[n].value 252 | if remaining_supply > 0: 253 | print('Remaining supply of %s on node %s: %.2f'%(str(n[1]), str(n[0]), remaining_supply)) 254 | for n in nodes: 255 | remaining_demand = self.primal.UnsatDemand[n].value 256 | if remaining_demand > 0: 257 | print('Remaining demand of %s on node %s: %.2f'%(str(n[1]), str(n[0]), remaining_demand)) 258 | print() 259 | 260 | for e0,e1 in self.arc_set: 261 | for k in self.commodity_set: 262 | flow = self.primal.y[(e0,e1,k)].value 263 | if flow > 0: 264 | print('Flow on arc %s -> %s: %.2f %s'%(str(e0), str(e1), flow, str(k))) 265 | print() 266 | 267 | print('----------') 268 | print('Total cost = %.2f (primal) %.2f (dual)'%(self.primal.OBJ(), self.Idual.OBJ())) 269 | 270 | 271 | ######################## 272 | # Now lets do something 273 | ######################## 274 | 275 | if __name__ == '__main__': 276 | m = MultiCommodityInterdiction('sample_nodes_data.csv', 'sample_nodes_commodity_data.csv', 'sample_arcs_data.csv', 'sample_arcs_commodity_data.csv') 277 | m.solve() 278 | m.printSolution() 279 | m.attacks = 1 280 | m.solve() 281 | m.printSolution() 282 | m.attacks = 2 283 | m.solve() 284 | m.printSolution() 285 | -------------------------------------------------------------------------------- /network_interdiction/multi_commodity_flow/sample_arcs_commodity_data.csv: -------------------------------------------------------------------------------- 1 | StartNode,EndNode,Commodity,Cost,Capacity 2 | Start,B,Rice,0,-1 3 | Start,C,Rice,10,-1 4 | C,B,Rice,30,-1 5 | B,End,Rice,0,-1 6 | B,D,Rice,40,-1 7 | C,D,Rice,20,-1 8 | D,End,Rice,70,-1 9 | Start,B,Corn,0,-1 10 | Start,C,Corn,100,-1 11 | C,B,Corn,300,-1 12 | B,End,Corn,0,-1 13 | B,D,Corn,400,-1 14 | C,D,Corn,200,-1 15 | D,End,Corn,700,-1 16 | -------------------------------------------------------------------------------- /network_interdiction/multi_commodity_flow/sample_arcs_data.csv: -------------------------------------------------------------------------------- 1 | StartNode,EndNode,Capacity,Attackable 2 | Start,B,20,1 3 | Start,C,70,1 4 | C,B,30,1 5 | B,End,100,1 6 | B,D,20,1 7 | C,D,40,1 8 | D,End,50,1 9 | -------------------------------------------------------------------------------- /network_interdiction/multi_commodity_flow/sample_nodes_commodity_data.csv: -------------------------------------------------------------------------------- 1 | Node,Commodity,SupplyDemand 2 | Start,Rice,-20 3 | C,Corn,-10 4 | D,Rice,20 5 | End,Corn,10 6 | -------------------------------------------------------------------------------- /network_interdiction/multi_commodity_flow/sample_nodes_data.csv: -------------------------------------------------------------------------------- 1 | Node,UnusedExampleColumn 2 | Start,0 3 | B,0 4 | C,0 5 | D,0 6 | End,0 7 | -------------------------------------------------------------------------------- /network_interdiction/shortest_path/sample_arcs_data.csv: -------------------------------------------------------------------------------- 1 | StartNode,EndNode,Cost,Attackable 2 | Start,B,10,1 3 | Start,C,2,1 4 | C,B,2,1 5 | B,End,1,1 6 | C,D,7,1 7 | D,End,8,1 8 | -------------------------------------------------------------------------------- /network_interdiction/shortest_path/sample_nodes_data.csv: -------------------------------------------------------------------------------- 1 | Node,SupplyDemand 2 | Start,-1 3 | B,0 4 | C,0 5 | D,0 6 | End,1 7 | -------------------------------------------------------------------------------- /network_interdiction/shortest_path/sp_interdict.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# ___________________________________________________________________________\n#\n# Pyomo: Python Optimization Modeling Objects\n# Copyright (c) 2015-2025\n# National Technology and Engineering Solutions of Sandia, LLC\n# Under the terms of Contract DE-NA0003525 with National Technology and\n# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain\n# rights in this software.\n# This software is distributed under the 3-clause BSD License.\n# ___________________________________________________________________________" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "metadata": {}, 16 | "outputs": [ 17 | { 18 | "name": "stdout", 19 | "output_type": "stream", 20 | "text": [ 21 | "import pandas\r\n", 22 | "import pyomo\r\n", 23 | "import pyomo.opt\r\n", 24 | "import pyomo.environ as pe\r\n", 25 | "import logging\r\n", 26 | "\r\n", 27 | "class SPInterdiction:\r\n", 28 | " \"\"\"A class to compute shortest path interdictions.\"\"\"\r\n", 29 | "\r\n", 30 | " def __init__(self, nodefile, arcfile, attacks=0):\r\n", 31 | " \"\"\"\r\n", 32 | " All the files are CSVs with columns described below. Attacks is the number of attacks.\r\n", 33 | "\r\n", 34 | " - nodefile:\r\n", 35 | " Node, SupplyDemand\r\n", 36 | "\r\n", 37 | " Every node must appear as a line in the nodefile. SupplyDemand describes the flow imbalance at the node.\r\n", 38 | "\r\n", 39 | " - arcfile:\r\n", 40 | " StartNode,EndNode,Cost,Attackable\r\n", 41 | "\r\n", 42 | " Every arc must appear in the arcfile. The data also describes the arc's cost and whether we can attack this arc.\r\n", 43 | " \"\"\"\r\n", 44 | " # Read in the node_data\r\n", 45 | " self.node_data = pandas.read_csv(nodefile)\r\n", 46 | " self.node_data.set_index(['Node'], inplace=True)\r\n", 47 | " self.node_data.sort_index(inplace=True)\r\n", 48 | " # Read in the arc_data\r\n", 49 | " self.arc_data = pandas.read_csv(arcfile)\r\n", 50 | " self.arc_data['xbar'] = 0\r\n", 51 | " self.arc_data.set_index(['StartNode','EndNode'], inplace=True)\r\n", 52 | " self.arc_data.sort_index(inplace=True)\r\n", 53 | "\r\n", 54 | " self.attacks = attacks\r\n", 55 | " \r\n", 56 | " self.node_set = self.node_data.index.unique()\r\n", 57 | " self.arc_set = self.arc_data.index.unique()\r\n", 58 | " \r\n", 59 | " # Compute nCmax\r\n", 60 | " self.nCmax = len(self.node_set) * self.arc_data['Cost'].max()\r\n", 61 | "\r\n", 62 | " self.createPrimal()\r\n", 63 | " self.createInterdictionDual()\r\n", 64 | "\r\n", 65 | "\r\n", 66 | " def createPrimal(self): \r\n", 67 | " \"\"\"Create the primal pyomo model. \r\n", 68 | " \r\n", 69 | " This is used to compute flows after interdiction. The interdiction is stored in arc_data.xbar.\"\"\"\r\n", 70 | "\r\n", 71 | " model = pe.ConcreteModel()\r\n", 72 | " # Tell pyomo to read in dual-variable information from the solver\r\n", 73 | " model.dual = pe.Suffix(direction=pe.Suffix.IMPORT) \r\n", 74 | "\r\n", 75 | " # Add the sets\r\n", 76 | " model.node_set = pe.Set( initialize=self.node_set )\r\n", 77 | " model.edge_set = pe.Set( initialize=self.arc_set, dimen=2)\r\n", 78 | "\r\n", 79 | " # Create the variables\r\n", 80 | " model.y = pe.Var(model.edge_set, domain=pe.NonNegativeReals) \r\n", 81 | " model.UnsatSupply = pe.Var(model.node_set, domain=pe.NonNegativeReals)\r\n", 82 | " model.UnsatDemand = pe.Var(model.node_set, domain=pe.NonNegativeReals)\r\n", 83 | " \r\n", 84 | " # Create the objective\r\n", 85 | " def obj_rule(model):\r\n", 86 | " return sum( (data['Cost']+data['xbar']*(2*self.nCmax+1))*model.y[e] for e,data in self.arc_data.iterrows()) + sum(self.nCmax*(model.UnsatSupply[n] + model.UnsatDemand[n]) for n,data in self.node_data.iterrows())\r\n", 87 | " model.OBJ = pe.Objective(rule=obj_rule, sense=pe.minimize)\r\n", 88 | "\r\n", 89 | " # Create the constraints, one for each node\r\n", 90 | " def flow_bal_rule(model, n):\r\n", 91 | " tmp = self.arc_data.reset_index()\r\n", 92 | " successors = tmp.ix[ tmp.StartNode == n, 'EndNode'].values\r\n", 93 | " predecessors = tmp.ix[ tmp.EndNode == n, 'StartNode'].values \r\n", 94 | " lhs = sum(model.y[(i,n)] for i in predecessors) - sum(model.y[(n,i)] for i in successors) \r\n", 95 | " imbalance = self.node_data['SupplyDemand'].get(n,0)\r\n", 96 | " supply_node = int(imbalance < 0)\r\n", 97 | " demand_node = int(imbalance > 0)\r\n", 98 | " rhs = (imbalance + model.UnsatSupply[n]*(supply_node) - model.UnsatDemand[n]*(demand_node))\r\n", 99 | " constr = (lhs == rhs)\r\n", 100 | " if isinstance(constr, bool):\r\n", 101 | " return pe.Constraint.Skip\r\n", 102 | " return constr\r\n", 103 | "\r\n", 104 | " model.FlowBalance = pe.Constraint(model.node_set, rule=flow_bal_rule)\r\n", 105 | " \r\n", 106 | " # Store the model\r\n", 107 | " self.primal = model\r\n", 108 | "\r\n", 109 | " def createInterdictionDual(self):\r\n", 110 | " # Create the model\r\n", 111 | " model = pe.ConcreteModel()\r\n", 112 | " \r\n", 113 | " # Add the sets\r\n", 114 | " model.node_set = pe.Set( initialize=self.node_set )\r\n", 115 | " model.edge_set = pe.Set( initialize=self.arc_set, dimen=2)\r\n", 116 | "\r\n", 117 | " # Create the variables\r\n", 118 | " model.rho = pe.Var(model.node_set, domain=pe.Reals)\r\n", 119 | " \r\n", 120 | " model.x = pe.Var(model.edge_set, domain=pe.Binary)\r\n", 121 | "\r\n", 122 | " # Create the objective\r\n", 123 | " def obj_rule(model):\r\n", 124 | " return sum(data['SupplyDemand']*model.rho[n] for n,data in self.node_data.iterrows())\r\n", 125 | "\r\n", 126 | " model.OBJ = pe.Objective(rule=obj_rule, sense=pe.maximize)\r\n", 127 | "\r\n", 128 | " # Create the constraints for y_ij\r\n", 129 | " def edge_constraint_rule(model, i, j):\r\n", 130 | " attackable = int(self.arc_data['Attackable'].get((i,j),0))\r\n", 131 | " return model.rho[j] - model.rho[i] <= self.arc_data['Cost'].get((i,j),0) + (2*self.nCmax+1)*model.x[(i,j)]*attackable\r\n", 132 | "\r\n", 133 | " model.DualEdgeConstraint = pe.Constraint(model.edge_set, rule=edge_constraint_rule)\r\n", 134 | " \r\n", 135 | " # Create constraints for the UnsatDemand variables \r\n", 136 | " def unsat_constraint_rule(model, n):\r\n", 137 | " imbalance = self.node_data['SupplyDemand'].get(n,0)\r\n", 138 | " supply_node = int(imbalance < 0)\r\n", 139 | " demand_node = int(imbalance > 0)\r\n", 140 | " if (supply_node):\r\n", 141 | " return -model.rho[n] <= self.nCmax\r\n", 142 | " if (demand_node):\r\n", 143 | " return model.rho[n] <= self.nCmax\r\n", 144 | " return pe.Constraint.Skip\r\n", 145 | "\r\n", 146 | " model.UnsatConstraint = pe.Constraint(model.node_set, rule=unsat_constraint_rule)\r\n", 147 | " \r\n", 148 | " # Create the interdiction budget constraint \r\n", 149 | " def block_limit_rule(model):\r\n", 150 | " model.attacks = self.attacks\r\n", 151 | " return pe.summation(model.x) <= model.attacks\r\n", 152 | "\r\n", 153 | " model.BlockLimit = pe.Constraint(rule=block_limit_rule)\r\n", 154 | "\r\n", 155 | " # Create, save the model\r\n", 156 | " self.Idual = model\r\n", 157 | "\r\n", 158 | " def solve(self, tee=False):\r\n", 159 | " solver = pyomo.opt.SolverFactory('gurobi')\r\n", 160 | "\r\n", 161 | " # Solve the dual first\r\n", 162 | " self.Idual.BlockLimit.construct()\r\n", 163 | " self.Idual.BlockLimit._constructed = False\r\n", 164 | " del self.Idual.BlockLimit._data[None] \r\n", 165 | " self.Idual.BlockLimit.reconstruct()\r\n", 166 | " self.Idual.preprocess()\r\n", 167 | " results = solver.solve(self.Idual, tee=tee, keepfiles=False, options_string=\"mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0\")\r\n", 168 | "\r\n", 169 | " # Check that we actually computed an optimal solution, load results\r\n", 170 | " if (results.solver.status != pyomo.opt.SolverStatus.ok):\r\n", 171 | " logging.warning('Check solver not ok?')\r\n", 172 | " if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): \r\n", 173 | " logging.warning('Check solver optimality?')\r\n", 174 | "\r\n", 175 | " self.Idual.solutions.load_from(results)\r\n", 176 | " # Now put interdictions into xbar and solve primal\r\n", 177 | " \r\n", 178 | " for e in self.arc_data.index:\r\n", 179 | " self.arc_data.ix[e,'xbar'] = self.Idual.x[e].value\r\n", 180 | "\r\n", 181 | " self.primal.OBJ.construct()\r\n", 182 | " self.primal.OBJ._constructed = False\r\n", 183 | " self.primal.OBJ._init_sense = pe.minimize\r\n", 184 | " del self.primal.OBJ._data[None] \r\n", 185 | " self.primal.OBJ.reconstruct()\r\n", 186 | " self.primal.preprocess()\r\n", 187 | " results = solver.solve(self.primal, tee=tee, keepfiles=False, options_string=\"mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0\")\r\n", 188 | "\r\n", 189 | " # Check that we actually computed an optimal solution, load results\r\n", 190 | " if (results.solver.status != pyomo.opt.SolverStatus.ok):\r\n", 191 | " logging.warning('Check solver not ok?')\r\n", 192 | " if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): \r\n", 193 | " logging.warning('Check solver optimality?')\r\n", 194 | "\r\n", 195 | " self.primal.solutions.load_from(results)\r\n", 196 | "\r\n", 197 | " def printSolution(self):\r\n", 198 | " print()\r\n", 199 | " print('Using %d attacks:' % self.attacks)\r\n", 200 | " print()\r\n", 201 | " edges = sorted(self.arc_set)\r\n", 202 | " for e in edges:\r\n", 203 | " if self.Idual.x[e].value > 0:\r\n", 204 | " print('Interdict arc %s -> %s'%(str(e[0]), str(e[1])))\r\n", 205 | " print()\r\n", 206 | " \r\n", 207 | " nodes = sorted(self.node_data.index)\r\n", 208 | " for n in nodes:\r\n", 209 | " remaining_supply = self.primal.UnsatSupply[n].value\r\n", 210 | " if remaining_supply > 0:\r\n", 211 | " print('Remaining supply on node %s: %.2f'%(str(n), remaining_supply))\r\n", 212 | " for n in nodes:\r\n", 213 | " remaining_demand = self.primal.UnsatDemand[n].value\r\n", 214 | " if remaining_demand > 0:\r\n", 215 | " print('Remaining demand on node %s: %.2f'%(str(n), remaining_demand))\r\n", 216 | " print()\r\n", 217 | " \r\n", 218 | " for e0,e1 in self.arc_set:\r\n", 219 | " flow = self.primal.y[(e0,e1)].value\r\n", 220 | " if flow > 0:\r\n", 221 | " print('Flow on arc %s -> %s: %.2f'%(str(e0), str(e1), flow))\r\n", 222 | " print()\r\n", 223 | "\r\n", 224 | " print('----------')\r\n", 225 | " print('Total cost = %.2f (primal) %.2f (dual)'%(self.primal.OBJ(), self.Idual.OBJ()))\r\n", 226 | "\r\n", 227 | "\r\n", 228 | "########################\r\n", 229 | "# Now lets do something\r\n", 230 | "########################\r\n", 231 | "\r\n", 232 | "if __name__ == '__main__':\r\n", 233 | " m = SPInterdiction('sample_nodes_data.csv', 'sample_arcs_data.csv')\r\n", 234 | " m.solve()\r\n", 235 | " m.printSolution()\r\n", 236 | " m.attacks = 1\r\n", 237 | " m.solve()\r\n", 238 | " m.printSolution()\r\n", 239 | " m.attacks = 2\r\n", 240 | " m.solve()\r\n", 241 | " m.printSolution()\r\n" 242 | ] 243 | } 244 | ], 245 | "source": [ 246 | "!cat sp_interdict.py" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 2, 252 | "metadata": {}, 253 | "outputs": [ 254 | { 255 | "name": "stdout", 256 | "output_type": "stream", 257 | "text": [ 258 | "\r\n", 259 | "Using 0 attacks:\r\n", 260 | "\r\n", 261 | "\r\n", 262 | "\r\n", 263 | "Flow on arc B -> End: 1.00\r\n", 264 | "Flow on arc C -> B: 1.00\r\n", 265 | "Flow on arc Start -> C: 1.00\r\n", 266 | "\r\n", 267 | "----------\r\n", 268 | "Total cost = 5.00 (primal) 5.00 (dual)\r\n", 269 | "\r\n", 270 | "Using 1 attacks:\r\n", 271 | "\r\n", 272 | "Interdict arc B -> End\r\n", 273 | "\r\n", 274 | "\r\n", 275 | "Flow on arc C -> D: 1.00\r\n", 276 | "Flow on arc D -> End: 1.00\r\n", 277 | "Flow on arc Start -> C: 1.00\r\n", 278 | "\r\n", 279 | "----------\r\n", 280 | "Total cost = 17.00 (primal) 17.00 (dual)\r\n", 281 | "\r\n", 282 | "Using 2 attacks:\r\n", 283 | "\r\n", 284 | "Interdict arc B -> End\r\n", 285 | "Interdict arc Start -> C\r\n", 286 | "\r\n", 287 | "Remaining supply on node Start: 1.00\r\n", 288 | "Remaining demand on node End: 1.00\r\n", 289 | "\r\n", 290 | "\r\n", 291 | "----------\r\n", 292 | "Total cost = 100.00 (primal) 100.00 (dual)\r\n" 293 | ] 294 | } 295 | ], 296 | "source": [ 297 | "!python sp_interdict.py" 298 | ] 299 | }, 300 | { 301 | "cell_type": "code", 302 | "execution_count": 3, 303 | "metadata": {}, 304 | "outputs": [], 305 | "source": [] 306 | } 307 | ], 308 | "metadata": { 309 | "kernelspec": { 310 | "display_name": "Python 3", 311 | "language": "python", 312 | "name": "python3" 313 | }, 314 | "language_info": { 315 | "codemirror_mode": { 316 | "name": "ipython", 317 | "version": 3 318 | }, 319 | "file_extension": ".py", 320 | "mimetype": "text/x-python", 321 | "name": "python", 322 | "nbconvert_exporter": "python", 323 | "pygments_lexer": "ipython3", 324 | "version": "3.6.1" 325 | } 326 | }, 327 | "nbformat": 4, 328 | "nbformat_minor": 1 329 | } -------------------------------------------------------------------------------- /network_interdiction/shortest_path/sp_interdict.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | import pandas 14 | import pyomo 15 | import pyomo.opt 16 | import pyomo.environ as pe 17 | import logging 18 | 19 | class SPInterdiction: 20 | """A class to compute shortest path interdictions.""" 21 | 22 | def __init__(self, nodefile, arcfile, attacks=0): 23 | """ 24 | All the files are CSVs with columns described below. Attacks is the number of attacks. 25 | 26 | - nodefile: 27 | Node, SupplyDemand 28 | 29 | Every node must appear as a line in the nodefile. SupplyDemand describes the flow imbalance at the node. 30 | 31 | - arcfile: 32 | StartNode,EndNode,Cost,Attackable 33 | 34 | Every arc must appear in the arcfile. The data also describes the arc's cost and whether we can attack this arc. 35 | """ 36 | # Read in the node_data 37 | self.node_data = pandas.read_csv(nodefile) 38 | self.node_data.set_index(['Node'], inplace=True) 39 | self.node_data.sort_index(inplace=True) 40 | # Read in the arc_data 41 | self.arc_data = pandas.read_csv(arcfile) 42 | self.arc_data['xbar'] = 0 43 | self.arc_data.set_index(['StartNode','EndNode'], inplace=True) 44 | self.arc_data.sort_index(inplace=True) 45 | 46 | self.attacks = attacks 47 | 48 | self.node_set = self.node_data.index.unique() 49 | self.arc_set = self.arc_data.index.unique() 50 | 51 | # Compute nCmax 52 | self.nCmax = len(self.node_set) * self.arc_data['Cost'].max() 53 | 54 | self.createPrimal() 55 | self.createInterdictionDual() 56 | 57 | 58 | def createPrimal(self): 59 | """Create the primal pyomo model. 60 | 61 | This is used to compute flows after interdiction. The interdiction is stored in arc_data.xbar.""" 62 | 63 | model = pe.ConcreteModel() 64 | # Tell pyomo to read in dual-variable information from the solver 65 | model.dual = pe.Suffix(direction=pe.Suffix.IMPORT) 66 | 67 | # Add the sets 68 | model.node_set = pe.Set( initialize=self.node_set ) 69 | model.edge_set = pe.Set( initialize=self.arc_set, dimen=2) 70 | 71 | # Create the variables 72 | model.y = pe.Var(model.edge_set, domain=pe.NonNegativeReals) 73 | model.UnsatSupply = pe.Var(model.node_set, domain=pe.NonNegativeReals) 74 | model.UnsatDemand = pe.Var(model.node_set, domain=pe.NonNegativeReals) 75 | 76 | # Create the objective 77 | def obj_rule(model): 78 | return sum( (data['Cost']+data['xbar']*(2*self.nCmax+1))*model.y[e] for e,data in self.arc_data.iterrows()) + sum(self.nCmax*(model.UnsatSupply[n] + model.UnsatDemand[n]) for n,data in self.node_data.iterrows()) 79 | model.OBJ = pe.Objective(rule=obj_rule, sense=pe.minimize) 80 | 81 | # Create the constraints, one for each node 82 | def flow_bal_rule(model, n): 83 | tmp = self.arc_data.reset_index() 84 | successors = tmp.ix[ tmp.StartNode == n, 'EndNode'].values 85 | predecessors = tmp.ix[ tmp.EndNode == n, 'StartNode'].values 86 | lhs = sum(model.y[(i,n)] for i in predecessors) - sum(model.y[(n,i)] for i in successors) 87 | imbalance = self.node_data['SupplyDemand'].get(n,0) 88 | supply_node = int(imbalance < 0) 89 | demand_node = int(imbalance > 0) 90 | rhs = (imbalance + model.UnsatSupply[n]*(supply_node) - model.UnsatDemand[n]*(demand_node)) 91 | constr = (lhs == rhs) 92 | if isinstance(constr, bool): 93 | return pe.Constraint.Skip 94 | return constr 95 | 96 | model.FlowBalance = pe.Constraint(model.node_set, rule=flow_bal_rule) 97 | 98 | # Store the model 99 | self.primal = model 100 | 101 | def createInterdictionDual(self): 102 | # Create the model 103 | model = pe.ConcreteModel() 104 | 105 | # Add the sets 106 | model.node_set = pe.Set( initialize=self.node_set ) 107 | model.edge_set = pe.Set( initialize=self.arc_set, dimen=2) 108 | 109 | # Create the variables 110 | model.rho = pe.Var(model.node_set, domain=pe.Reals) 111 | 112 | model.x = pe.Var(model.edge_set, domain=pe.Binary) 113 | 114 | # Create the objective 115 | def obj_rule(model): 116 | return sum(data['SupplyDemand']*model.rho[n] for n,data in self.node_data.iterrows()) 117 | 118 | model.OBJ = pe.Objective(rule=obj_rule, sense=pe.maximize) 119 | 120 | # Create the constraints for y_ij 121 | def edge_constraint_rule(model, i, j): 122 | attackable = int(self.arc_data['Attackable'].get((i,j),0)) 123 | return model.rho[j] - model.rho[i] <= self.arc_data['Cost'].get((i,j),0) + (2*self.nCmax+1)*model.x[(i,j)]*attackable 124 | 125 | model.DualEdgeConstraint = pe.Constraint(model.edge_set, rule=edge_constraint_rule) 126 | 127 | # Create constraints for the UnsatDemand variables 128 | def unsat_constraint_rule(model, n): 129 | imbalance = self.node_data['SupplyDemand'].get(n,0) 130 | supply_node = int(imbalance < 0) 131 | demand_node = int(imbalance > 0) 132 | if (supply_node): 133 | return -model.rho[n] <= self.nCmax 134 | if (demand_node): 135 | return model.rho[n] <= self.nCmax 136 | return pe.Constraint.Skip 137 | 138 | model.UnsatConstraint = pe.Constraint(model.node_set, rule=unsat_constraint_rule) 139 | 140 | # Create the interdiction budget constraint 141 | def block_limit_rule(model): 142 | model.attacks = self.attacks 143 | return pe.summation(model.x) <= model.attacks 144 | 145 | model.BlockLimit = pe.Constraint(rule=block_limit_rule) 146 | 147 | # Create, save the model 148 | self.Idual = model 149 | 150 | def solve(self, tee=False): 151 | solver = pyomo.opt.SolverFactory('gurobi') 152 | 153 | # Solve the dual first 154 | self.Idual.BlockLimit.construct() 155 | self.Idual.BlockLimit._constructed = False 156 | del self.Idual.BlockLimit._data[None] 157 | self.Idual.BlockLimit.reconstruct() 158 | self.Idual.preprocess() 159 | results = solver.solve(self.Idual, tee=tee, keepfiles=False, options_string="mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0") 160 | 161 | # Check that we actually computed an optimal solution, load results 162 | if (results.solver.status != pyomo.opt.SolverStatus.ok): 163 | logging.warning('Check solver not ok?') 164 | if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): 165 | logging.warning('Check solver optimality?') 166 | 167 | self.Idual.solutions.load_from(results) 168 | # Now put interdictions into xbar and solve primal 169 | 170 | for e in self.arc_data.index: 171 | self.arc_data.ix[e,'xbar'] = self.Idual.x[e].value 172 | 173 | self.primal.OBJ.construct() 174 | self.primal.OBJ._constructed = False 175 | self.primal.OBJ._init_sense = pe.minimize 176 | del self.primal.OBJ._data[None] 177 | self.primal.OBJ.reconstruct() 178 | self.primal.preprocess() 179 | results = solver.solve(self.primal, tee=tee, keepfiles=False, options_string="mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0") 180 | 181 | # Check that we actually computed an optimal solution, load results 182 | if (results.solver.status != pyomo.opt.SolverStatus.ok): 183 | logging.warning('Check solver not ok?') 184 | if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): 185 | logging.warning('Check solver optimality?') 186 | 187 | self.primal.solutions.load_from(results) 188 | 189 | def printSolution(self): 190 | print() 191 | print('Using %d attacks:' % self.attacks) 192 | print() 193 | edges = sorted(self.arc_set) 194 | for e in edges: 195 | if self.Idual.x[e].value > 0: 196 | print('Interdict arc %s -> %s'%(str(e[0]), str(e[1]))) 197 | print() 198 | 199 | nodes = sorted(self.node_data.index) 200 | for n in nodes: 201 | remaining_supply = self.primal.UnsatSupply[n].value 202 | if remaining_supply > 0: 203 | print('Remaining supply on node %s: %.2f'%(str(n), remaining_supply)) 204 | for n in nodes: 205 | remaining_demand = self.primal.UnsatDemand[n].value 206 | if remaining_demand > 0: 207 | print('Remaining demand on node %s: %.2f'%(str(n), remaining_demand)) 208 | print() 209 | 210 | for e0,e1 in self.arc_set: 211 | flow = self.primal.y[(e0,e1)].value 212 | if flow > 0: 213 | print('Flow on arc %s -> %s: %.2f'%(str(e0), str(e1), flow)) 214 | print() 215 | 216 | print('----------') 217 | print('Total cost = %.2f (primal) %.2f (dual)'%(self.primal.OBJ(), self.Idual.OBJ())) 218 | 219 | 220 | ######################## 221 | # Now lets do something 222 | ######################## 223 | 224 | if __name__ == '__main__': 225 | m = SPInterdiction('sample_nodes_data.csv', 'sample_arcs_data.csv') 226 | m.solve() 227 | m.printSolution() 228 | m.attacks = 1 229 | m.solve() 230 | m.printSolution() 231 | m.attacks = 2 232 | m.solve() 233 | m.printSolution() 234 | -------------------------------------------------------------------------------- /p_median/p-median.dat: -------------------------------------------------------------------------------- 1 | param m := 10; 2 | param n := 6; 3 | param p := 3; 4 | -------------------------------------------------------------------------------- /p_median/p-median.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | from pyomo.environ import * 14 | import random 15 | 16 | random.seed(1000) 17 | 18 | model = AbstractModel() 19 | 20 | # Number of candidate locations 21 | model.m = Param(within=PositiveIntegers) 22 | # Number of customers 23 | model.n = Param(within=PositiveIntegers) 24 | # Set of candidate locations 25 | model.M = RangeSet(1,model.m) 26 | # Set of customer nodes 27 | model.N = RangeSet(1,model.n) 28 | 29 | # Number of facilities 30 | model.p = Param(within=RangeSet(1,model.n)) 31 | # d[j] - demand of customer j 32 | model.d = Param(model.N, default=1.0) 33 | # c[i,j] - unit cost of satisfying customer j from facility i 34 | model.c = Param(model.M, model.N, initialize=lambda i, j, model : random.uniform(1.0,2.0), within=Reals) 35 | 36 | # x[i,j] - fraction of the demand of customer j that is supplied by facility i 37 | model.x = Var(model.M, model.N, bounds=(0.0,1.0)) 38 | # y[i] - a binary value that is 1 is a facility is located at location i 39 | model.y = Var(model.M, within=Binary) 40 | 41 | # Minimize the demand-weighted total cost 42 | def cost_(model): 43 | return sum(model.d[j]*model.c[i,j]*model.x[i,j] for i in model.M for j in model.N) 44 | model.cost = Objective(rule=cost_) 45 | 46 | # All of the demand for customer j must be satisfied 47 | def demand_(model, j): 48 | return sum(model.x[i,j] for i in model.M) == 1.0 49 | model.demand = Constraint(model.N, rule=demand_) 50 | 51 | # Exactly p facilities are located 52 | def facilities_(model): 53 | return sum(model.y[i] for i in model.M) == model.p 54 | model.facilities = Constraint(rule=facilities_) 55 | 56 | # Demand nodes can only be assigned to open facilities 57 | def openfac_(model, i, j): 58 | return model.x[i,j] <= model.y[i] 59 | model.openfac = Constraint(model.M, model.N, rule=openfac_) 60 | -------------------------------------------------------------------------------- /p_median/p_median.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# ___________________________________________________________________________\n#\n# Pyomo: Python Optimization Modeling Objects\n# Copyright (c) 2015-2025\n# National Technology and Engineering Solutions of Sandia, LLC\n# Under the terms of Contract DE-NA0003525 with National Technology and\n# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain\n# rights in this software.\n# This software is distributed under the 3-clause BSD License.\n# ___________________________________________________________________________" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# The $p$-Median Problem\n", 17 | "\n", 18 | "## Summary\n", 19 | "\n", 20 | "The goal of the $p$-median problem is to locating $p$ facilities to minimize the demand weighted average distance between demand nodes and the nearest of the selected facilities. Hakimi (1964, 1965) first considered this problem for the design of network switch centers. \n", 21 | "However, this problem has been used to model a wide range of applications, such as warehouse location, depot location, school districting and sensor placement.\n", 22 | "\n", 23 | "\n", 24 | "## Problem Statement\n", 25 | "\n", 26 | "The $p$-median problem can be formulated mathematically as an integer programming problem using the following model. \n", 27 | "\n", 28 | "### Sets\n", 29 | "\n", 30 | " $M$ = set of candidate locations \n", 31 | " $N$ = set of customer demand nodes\n", 32 | "\n", 33 | "### Parameters\n", 34 | "\n", 35 | " $p$ = number of facilities to locate \n", 36 | " $d_j$ = demand of customer $j$, $\\forall j \\in N$ \n", 37 | " $c_{ij}$ = unit cost of satisfying customer $j$ from facility $i$, $\\forall i \\in M, \\forall j \\in N$\n", 38 | " \n", 39 | "### Variables\n", 40 | " $x_{ij}$ = fraction of the demand of customer $j$ that is supplied by facility $i$, $\\forall i \\in M, \\forall j \\in N$ \n", 41 | " $y_i$ = a binary value that is $1$ is a facility is located at location $i$, $\\forall i \\in M$\n", 42 | "\n", 43 | "### Objective\n", 44 | "\n", 45 | "Minimize the demand-weighted total cost \n", 46 | " $\\min \\sum_{i \\in M} \\sum_{j \\in N} d_j c_{ij} x_{ij}$\n", 47 | "\n", 48 | "### Constraints\n", 49 | "\n", 50 | "All of the demand for customer $j$ must be satisfied \n", 51 | " $\\sum_{i \\in M} x_{ij} = 1$, $\\forall j \\in N$\n", 52 | "\n", 53 | "Exactly $p$ facilities are located \n", 54 | " $\\sum_{i \\in M} y_i = p$\n", 55 | " \n", 56 | "Demand nodes can only be assigned to open facilities \n", 57 | " $x_{ij} \\leq y_i$, $\\forall i \\in M, \\forall j \\in N$\n", 58 | " \n", 59 | "The assignment variables must be non-negative \n", 60 | " $x_{ij} \\geq 0$, $\\forall i \\in M, \\forall j \\in N$\n", 61 | "\n", 62 | "## Pyomo Formulation\n", 63 | "\n", 64 | "The following is an abstract Pyomo model for this problem:" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 1, 70 | "metadata": {}, 71 | "outputs": [ 72 | { 73 | "name": "stdout", 74 | "output_type": "stream", 75 | "text": [ 76 | "from pyomo.environ import *\r\n", 77 | "import random\r\n", 78 | "\r\n", 79 | "random.seed(1000)\r\n", 80 | "\r\n", 81 | "model = AbstractModel()\r\n", 82 | "\r\n", 83 | "# Number of candidate locations\r\n", 84 | "model.m = Param(within=PositiveIntegers)\r\n", 85 | "# Number of customers\r\n", 86 | "model.n = Param(within=PositiveIntegers)\r\n", 87 | "# Set of candidate locations\r\n", 88 | "model.M = RangeSet(1,model.m)\r\n", 89 | "# Set of customer nodes\r\n", 90 | "model.N = RangeSet(1,model.n)\r\n", 91 | "\r\n", 92 | "# Number of facilities\r\n", 93 | "model.p = Param(within=RangeSet(1,model.n))\r\n", 94 | "# d[j] - demand of customer j\r\n", 95 | "model.d = Param(model.N, default=1.0)\r\n", 96 | "# c[i,j] - unit cost of satisfying customer j from facility i\r\n", 97 | "model.c = Param(model.M, model.N, initialize=lambda i, j, model : random.uniform(1.0,2.0), within=Reals)\r\n", 98 | "\r\n", 99 | "# x[i,j] - fraction of the demand of customer j that is supplied by facility i\r\n", 100 | "model.x = Var(model.M, model.N, bounds=(0.0,1.0))\r\n", 101 | "# y[i] - a binary value that is 1 is a facility is located at location i\r\n", 102 | "model.y = Var(model.M, within=Binary)\r\n", 103 | "\r\n", 104 | "# Minimize the demand-weighted total cost\r\n", 105 | "def cost_(model):\r\n", 106 | " return sum(model.d[j]*model.c[i,j]*model.x[i,j] for i in model.M for j in model.N)\r\n", 107 | "model.cost = Objective(rule=cost_)\r\n", 108 | "\r\n", 109 | "# All of the demand for customer j must be satisfied\r\n", 110 | "def demand_(model, j):\r\n", 111 | " return sum(model.x[i,j] for i in model.M) == 1.0\r\n", 112 | "model.demand = Constraint(model.N, rule=demand_)\r\n", 113 | "\r\n", 114 | "# Exactly p facilities are located\r\n", 115 | "def facilities_(model):\r\n", 116 | " return sum(model.y[i] for i in model.M) == model.p\r\n", 117 | "model.facilities = Constraint(rule=facilities_)\r\n", 118 | "\r\n", 119 | "# Demand nodes can only be assigned to open facilities \r\n", 120 | "def openfac_(model, i, j):\r\n", 121 | " return model.x[i,j] <= model.y[i]\r\n", 122 | "model.openfac = Constraint(model.M, model.N, rule=openfac_)\r\n" 123 | ] 124 | } 125 | ], 126 | "source": [ 127 | "!cat p-median.py" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "****\n", 135 | "This model is simplified in several respects. First, the candidate locations and customer locations are treated as numeric ranges. Second, the demand values, $d_j$ are initialized with a default value of $1$. Finally, the cost values, $c_{ij}$ are randomly assigned.\n", 136 | "\n", 137 | "## Model Data\n", 138 | "\n", 139 | "This model is parameterized by three values: the number of facility locations, the number of customers, and the number of facilities. For example:" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": 2, 145 | "metadata": {}, 146 | "outputs": [ 147 | { 148 | "name": "stdout", 149 | "output_type": "stream", 150 | "text": [ 151 | "param m := 10;\r\n", 152 | "param n := 6;\r\n", 153 | "param p := 3;\r\n" 154 | ] 155 | } 156 | ], 157 | "source": [ 158 | "!cat p-median.dat" 159 | ] 160 | }, 161 | { 162 | "cell_type": "markdown", 163 | "metadata": {}, 164 | "source": [ 165 | "****\n", 166 | "\n", 167 | "## Solution\n", 168 | "\n", 169 | "Pyomo includes a `pyomo` command that automates the construction and optimization of models. The GLPK solver can be used in this simple example:" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 3, 175 | "metadata": {}, 176 | "outputs": [ 177 | { 178 | "name": "stdout", 179 | "output_type": "stream", 180 | "text": [ 181 | "[ 0.00] Setting up Pyomo environment\r\n", 182 | "[ 0.00] Applying Pyomo preprocessing actions\r\n", 183 | "[ 0.00] Creating model\r\n", 184 | "[ 0.02] Applying solver\r\n", 185 | "[ 0.06] Processing results\r\n", 186 | " Number of solutions: 1\r\n", 187 | " Solution Information\r\n", 188 | " Gap: 0.0\r\n", 189 | " Status: optimal\r\n", 190 | " Function Value: 6.431184939357673\r\n", 191 | " Solver results file: results.json\r\n", 192 | "[ 0.07] Applying Pyomo postprocessing actions\r\n", 193 | "[ 0.07] Pyomo Finished\r\n" 194 | ] 195 | } 196 | ], 197 | "source": [ 198 | "!pyomo solve --solver=glpk p-median.py p-median.dat" 199 | ] 200 | }, 201 | { 202 | "cell_type": "markdown", 203 | "metadata": {}, 204 | "source": [ 205 | "By default, the optimization results are stored in the file `results.yml`:" 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": 4, 211 | "metadata": {}, 212 | "outputs": [ 213 | { 214 | "name": "stdout", 215 | "output_type": "stream", 216 | "text": [ 217 | "# ==========================================================\r\n", 218 | "# = Solver Results =\r\n", 219 | "# ==========================================================\r\n", 220 | "# ----------------------------------------------------------\r\n", 221 | "# Problem Information\r\n", 222 | "# ----------------------------------------------------------\r\n", 223 | "Problem: \r\n", 224 | "- Name: unknown\r\n", 225 | " Lower bound: 6.43118493936\r\n", 226 | " Upper bound: 6.43118493936\r\n", 227 | " Number of objectives: 1\r\n", 228 | " Number of constraints: 68\r\n", 229 | " Number of variables: 71\r\n", 230 | " Number of nonzeros: 191\r\n", 231 | " Sense: minimize\r\n", 232 | "# ----------------------------------------------------------\r\n", 233 | "# Solver Information\r\n", 234 | "# ----------------------------------------------------------\r\n", 235 | "Solver: \r\n", 236 | "- Status: ok\r\n", 237 | " Termination condition: optimal\r\n", 238 | " Statistics: \r\n", 239 | " Branch and bound: \r\n", 240 | " Number of bounded subproblems: 1\r\n", 241 | " Number of created subproblems: 1\r\n", 242 | " Error rc: 0\r\n", 243 | " Time: 0.0117330551147\r\n", 244 | "# ----------------------------------------------------------\r\n", 245 | "# Solution Information\r\n", 246 | "# ----------------------------------------------------------\r\n", 247 | "Solution: \r\n", 248 | "- number of solutions: 1\r\n", 249 | " number of solutions displayed: 1\r\n", 250 | "- Gap: 0.0\r\n", 251 | " Status: optimal\r\n", 252 | " Message: None\r\n", 253 | " Objective:\r\n", 254 | " cost:\r\n", 255 | " Value: 6.43118493936\r\n", 256 | " Variable:\r\n", 257 | " x[6,5]:\r\n", 258 | " Value: 1\r\n", 259 | " y[3]:\r\n", 260 | " Value: 1\r\n", 261 | " x[6,2]:\r\n", 262 | " Value: 1\r\n", 263 | " x[9,6]:\r\n", 264 | " Value: 1\r\n", 265 | " y[9]:\r\n", 266 | " Value: 1\r\n", 267 | " x[3,4]:\r\n", 268 | " Value: 1\r\n", 269 | " y[6]:\r\n", 270 | " Value: 1\r\n", 271 | " x[6,3]:\r\n", 272 | " Value: 1\r\n", 273 | " x[6,1]:\r\n", 274 | " Value: 1\r\n", 275 | " Constraint: No values\r\n" 276 | ] 277 | } 278 | ], 279 | "source": [ 280 | "!cat results.yml" 281 | ] 282 | }, 283 | { 284 | "cell_type": "markdown", 285 | "metadata": {}, 286 | "source": [ 287 | "****\n", 288 | "\n", 289 | "This solution places facilities at locations 3, 6 and 9. Facility 3 meets the demand of customer 4, facility 6 meets the demand of customers 1, 2, 3 and 5, and facility 9 meets the demand of customer 6." 290 | ] 291 | }, 292 | { 293 | "cell_type": "markdown", 294 | "metadata": {}, 295 | "source": [ 296 | "## References\n", 297 | "\n", 298 | "* S.L. Hakimi (1964) Optimum location of switching centers and the absolute centers and medians of a graph. Oper Res 12:450\u2013459\n", 299 | "* S.L. Hakimi (1965) Optimum distribution of switching centers in a communication network and some related graph theoretic problems. Oper Res 13:462\u2013475" 300 | ] 301 | } 302 | ], 303 | "metadata": { 304 | "kernelspec": { 305 | "display_name": "Python 3", 306 | "language": "python", 307 | "name": "python3" 308 | }, 309 | "language_info": { 310 | "codemirror_mode": { 311 | "name": "ipython", 312 | "version": 3 313 | }, 314 | "file_extension": ".py", 315 | "mimetype": "text/x-python", 316 | "name": "python", 317 | "nbconvert_exporter": "python", 318 | "pygments_lexer": "ipython3", 319 | "version": "3.6.1" 320 | } 321 | }, 322 | "nbformat": 4, 323 | "nbformat_minor": 1 324 | } -------------------------------------------------------------------------------- /p_median/results.yml: -------------------------------------------------------------------------------- 1 | # ========================================================== 2 | # = Solver Results = 3 | # ========================================================== 4 | # ---------------------------------------------------------- 5 | # Problem Information 6 | # ---------------------------------------------------------- 7 | Problem: 8 | - Name: unknown 9 | Lower bound: 6.43118493936 10 | Upper bound: 6.43118493936 11 | Number of objectives: 1 12 | Number of constraints: 68 13 | Number of variables: 71 14 | Number of nonzeros: 191 15 | Sense: minimize 16 | # ---------------------------------------------------------- 17 | # Solver Information 18 | # ---------------------------------------------------------- 19 | Solver: 20 | - Status: ok 21 | Termination condition: optimal 22 | Statistics: 23 | Branch and bound: 24 | Number of bounded subproblems: 1 25 | Number of created subproblems: 1 26 | Error rc: 0 27 | Time: 0.0117330551147 28 | # ---------------------------------------------------------- 29 | # Solution Information 30 | # ---------------------------------------------------------- 31 | Solution: 32 | - number of solutions: 1 33 | number of solutions displayed: 1 34 | - Gap: 0.0 35 | Status: optimal 36 | Message: None 37 | Objective: 38 | cost: 39 | Value: 6.43118493936 40 | Variable: 41 | x[6,5]: 42 | Value: 1 43 | y[3]: 44 | Value: 1 45 | x[6,2]: 46 | Value: 1 47 | x[9,6]: 48 | Value: 1 49 | y[9]: 50 | Value: 1 51 | x[3,4]: 52 | Value: 1 53 | y[6]: 54 | Value: 1 55 | x[6,3]: 56 | Value: 1 57 | x[6,1]: 58 | Value: 1 59 | Constraint: No values 60 | -------------------------------------------------------------------------------- /pandas_min_cost_flow/arcs.csv: -------------------------------------------------------------------------------- 1 | Start,End,Cost,UpperBound,LowerBound 2 | A,B,1,0,-1 3 | B,C,1,-1,-1 4 | C,D,1,-1,-1 5 | D,E,1,-1,-1 6 | A,C,3,-1,-1 7 | A,D,4,-1,-1 8 | A,E,5,-1,-1 9 | -------------------------------------------------------------------------------- /pandas_min_cost_flow/min_cost_flow.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# ___________________________________________________________________________\n#\n# Pyomo: Python Optimization Modeling Objects\n# Copyright (c) 2015-2025\n# National Technology and Engineering Solutions of Sandia, LLC\n# Under the terms of Contract DE-NA0003525 with National Technology and\n# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain\n# rights in this software.\n# This software is distributed under the 3-clause BSD License.\n# ___________________________________________________________________________" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "metadata": {}, 16 | "outputs": [ 17 | { 18 | "name": "stdout", 19 | "output_type": "stream", 20 | "text": [ 21 | "import pyomo\r\n", 22 | "import pandas\r\n", 23 | "import pyomo.opt\r\n", 24 | "import pyomo.environ as pe\r\n", 25 | "\r\n", 26 | "class MinCostFlow:\r\n", 27 | " \"\"\"This class implements a standard min-cost-flow model. \r\n", 28 | " \r\n", 29 | " It takes as input two csv files, providing data for the nodes and the arcs of the network. The nodes file should have columns:\r\n", 30 | " \r\n", 31 | " Node, Imbalance\r\n", 32 | "\r\n", 33 | " that specify the node name and the flow imbalance at the node. The arcs file should have columns:\r\n", 34 | "\r\n", 35 | " Start, End, Cost, UpperBound, LowerBound\r\n", 36 | "\r\n", 37 | " that specify an arc start node, an arc end node, a cost for the arc, and upper and lower bounds for the flow.\"\"\"\r\n", 38 | " def __init__(self, nodesfile, arcsfile):\r\n", 39 | " \"\"\"Read in the csv data.\"\"\"\r\n", 40 | " # Read in the nodes file\r\n", 41 | " self.node_data = pandas.read_csv('nodes.csv')\r\n", 42 | " self.node_data.set_index(['Node'], inplace=True)\r\n", 43 | " self.node_data.sort_index(inplace=True)\r\n", 44 | " # Read in the arcs file\r\n", 45 | " self.arc_data = pandas.read_csv('arcs.csv')\r\n", 46 | " self.arc_data.set_index(['Start','End'], inplace=True)\r\n", 47 | " self.arc_data.sort_index(inplace=True)\r\n", 48 | "\r\n", 49 | " self.node_set = self.node_data.index.unique()\r\n", 50 | " self.arc_set = self.arc_data.index.unique()\r\n", 51 | "\r\n", 52 | " self.createModel()\r\n", 53 | "\r\n", 54 | " def createModel(self):\r\n", 55 | " \"\"\"Create the pyomo model given the csv data.\"\"\"\r\n", 56 | " self.m = pe.ConcreteModel()\r\n", 57 | "\r\n", 58 | " # Create sets\r\n", 59 | " self.m.node_set = pe.Set( initialize=self.node_set )\r\n", 60 | " self.m.arc_set = pe.Set( initialize=self.arc_set , dimen=2)\r\n", 61 | "\r\n", 62 | " # Create variables\r\n", 63 | " self.m.Y = pe.Var(self.m.arc_set, domain=pe.NonNegativeReals)\r\n", 64 | "\r\n", 65 | " # Create objective\r\n", 66 | " def obj_rule(m):\r\n", 67 | " return sum(m.Y[e] * self.arc_data.ix[e,'Cost'] for e in self.arc_set)\r\n", 68 | " self.m.OBJ = pe.Objective(rule=obj_rule, sense=pe.minimize)\r\n", 69 | " \r\n", 70 | " # Flow Balance rule\r\n", 71 | " def flow_bal_rule(m, n):\r\n", 72 | " arcs = self.arc_data.reset_index()\r\n", 73 | " preds = arcs[ arcs.End == n ]['Start']\r\n", 74 | " succs = arcs[ arcs.Start == n ]['End']\r\n", 75 | " return sum(m.Y[(p,n)] for p in preds) - sum(m.Y[(n,s)] for s in succs) == self.node_data.ix[n,'Imbalance']\r\n", 76 | " self.m.FlowBal = pe.Constraint(self.m.node_set, rule=flow_bal_rule)\r\n", 77 | "\r\n", 78 | " # Upper bounds rule\r\n", 79 | " def upper_bounds_rule(m, n1, n2):\r\n", 80 | " e = (n1,n2)\r\n", 81 | " if self.arc_data.ix[e, 'UpperBound'] < 0:\r\n", 82 | " return pe.Constraint.Skip\r\n", 83 | " return m.Y[e] <= self.arc_data.ix[e, 'UpperBound']\r\n", 84 | " self.m.UpperBound = pe.Constraint(self.m.arc_set, rule=upper_bounds_rule)\r\n", 85 | " \r\n", 86 | " # Lower bounds rule\r\n", 87 | " def lower_bounds_rule(m, n1, n2):\r\n", 88 | " e = (n1,n2)\r\n", 89 | " if self.arc_data.ix[e, 'LowerBound'] < 0:\r\n", 90 | " return pe.Constraint.Skip\r\n", 91 | " return m.Y[e] >= self.arc_data.ix[e, 'LowerBound']\r\n", 92 | " self.m.LowerBound = pe.Constraint(self.m.arc_set, rule=lower_bounds_rule)\r\n", 93 | "\r\n", 94 | " def solve(self):\r\n", 95 | " \"\"\"Solve the model.\"\"\"\r\n", 96 | " solver = pyomo.opt.SolverFactory('gurobi')\r\n", 97 | " results = solver.solve(self.m, tee=True, keepfiles=False, options_string=\"mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0\")\r\n", 98 | "\r\n", 99 | " if (results.solver.status != pyomo.opt.SolverStatus.ok):\r\n", 100 | " logging.warning('Check solver not ok?')\r\n", 101 | " if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): \r\n", 102 | " logging.warning('Check solver optimality?') \r\n", 103 | "\r\n", 104 | "\r\n", 105 | "if __name__ == '__main__':\r\n", 106 | " sp = MinCostFlow('nodes.csv', 'arcs.csv') \r\n", 107 | " sp.solve()\r\n", 108 | " print('\\n\\n---------------------------')\r\n", 109 | " print('Cost: ', sp.m.OBJ())\r\n" 110 | ] 111 | } 112 | ], 113 | "source": [ 114 | "!cat min_cost_flow.py" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 2, 120 | "metadata": {}, 121 | "outputs": [ 122 | { 123 | "name": "stdout", 124 | "output_type": "stream", 125 | "text": [ 126 | "No parameters matching 'mip_tolerances_integrality' found\r\n", 127 | "No parameters matching 'mip_tolerances_mipgap' found\r\n", 128 | "Optimize a model with 7 rows, 8 columns and 16 nonzeros\r\n", 129 | "Coefficient statistics:\r\n", 130 | " Matrix range [1e+00, 1e+00]\r\n", 131 | " Objective range [1e+00, 5e+00]\r\n", 132 | " Bounds range [0e+00, 0e+00]\r\n", 133 | " RHS range [1e+00, 1e+00]\r\n", 134 | "Presolve removed 5 rows and 7 columns\r\n", 135 | "Presolve time: 0.00s\r\n", 136 | "Presolved: 2 rows, 1 columns, 2 nonzeros\r\n", 137 | "\r\n", 138 | "Iteration Objective Primal Inf. Dual Inf. Time\r\n", 139 | " 0 5.0000000e+00 0.000000e+00 0.000000e+00 0s\r\n", 140 | " 0 5.0000000e+00 0.000000e+00 0.000000e+00 0s\r\n", 141 | "\r\n", 142 | "Solved in 0 iterations and 0.00 seconds\r\n", 143 | "Optimal objective 5.000000000e+00\r\n", 144 | "\r\n", 145 | "\r\n", 146 | "---------------------------\r\n", 147 | "Cost: 5.0\r\n" 148 | ] 149 | } 150 | ], 151 | "source": [ 152 | "!python min_cost_flow.py" 153 | ] 154 | } 155 | ], 156 | "metadata": { 157 | "kernelspec": { 158 | "display_name": "Python 3", 159 | "language": "python", 160 | "name": "python3" 161 | }, 162 | "language_info": { 163 | "codemirror_mode": { 164 | "name": "ipython", 165 | "version": 3 166 | }, 167 | "file_extension": ".py", 168 | "mimetype": "text/x-python", 169 | "name": "python", 170 | "nbconvert_exporter": "python", 171 | "pygments_lexer": "ipython3", 172 | "version": "3.6.1" 173 | } 174 | }, 175 | "nbformat": 4, 176 | "nbformat_minor": 1 177 | } -------------------------------------------------------------------------------- /pandas_min_cost_flow/min_cost_flow.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | import pyomo 14 | import pandas 15 | import pyomo.opt 16 | import pyomo.environ as pe 17 | 18 | class MinCostFlow: 19 | """This class implements a standard min-cost-flow model. 20 | 21 | It takes as input two csv files, providing data for the nodes and the arcs of the network. The nodes file should have columns: 22 | 23 | Node, Imbalance 24 | 25 | that specify the node name and the flow imbalance at the node. The arcs file should have columns: 26 | 27 | Start, End, Cost, UpperBound, LowerBound 28 | 29 | that specify an arc start node, an arc end node, a cost for the arc, and upper and lower bounds for the flow.""" 30 | def __init__(self, nodesfile, arcsfile): 31 | """Read in the csv data.""" 32 | # Read in the nodes file 33 | self.node_data = pandas.read_csv('nodes.csv') 34 | self.node_data.set_index(['Node'], inplace=True) 35 | self.node_data.sort_index(inplace=True) 36 | # Read in the arcs file 37 | self.arc_data = pandas.read_csv('arcs.csv') 38 | self.arc_data.set_index(['Start','End'], inplace=True) 39 | self.arc_data.sort_index(inplace=True) 40 | 41 | self.node_set = self.node_data.index.unique() 42 | self.arc_set = self.arc_data.index.unique() 43 | 44 | self.createModel() 45 | 46 | def createModel(self): 47 | """Create the pyomo model given the csv data.""" 48 | self.m = pe.ConcreteModel() 49 | 50 | # Create sets 51 | self.m.node_set = pe.Set( initialize=self.node_set ) 52 | self.m.arc_set = pe.Set( initialize=self.arc_set , dimen=2) 53 | 54 | # Create variables 55 | self.m.Y = pe.Var(self.m.arc_set, domain=pe.NonNegativeReals) 56 | 57 | # Create objective 58 | def obj_rule(m): 59 | return sum(m.Y[e] * self.arc_data.ix[e,'Cost'] for e in self.arc_set) 60 | self.m.OBJ = pe.Objective(rule=obj_rule, sense=pe.minimize) 61 | 62 | # Flow Balance rule 63 | def flow_bal_rule(m, n): 64 | arcs = self.arc_data.reset_index() 65 | preds = arcs[ arcs.End == n ]['Start'] 66 | succs = arcs[ arcs.Start == n ]['End'] 67 | return sum(m.Y[(p,n)] for p in preds) - sum(m.Y[(n,s)] for s in succs) == self.node_data.ix[n,'Imbalance'] 68 | self.m.FlowBal = pe.Constraint(self.m.node_set, rule=flow_bal_rule) 69 | 70 | # Upper bounds rule 71 | def upper_bounds_rule(m, n1, n2): 72 | e = (n1,n2) 73 | if self.arc_data.ix[e, 'UpperBound'] < 0: 74 | return pe.Constraint.Skip 75 | return m.Y[e] <= self.arc_data.ix[e, 'UpperBound'] 76 | self.m.UpperBound = pe.Constraint(self.m.arc_set, rule=upper_bounds_rule) 77 | 78 | # Lower bounds rule 79 | def lower_bounds_rule(m, n1, n2): 80 | e = (n1,n2) 81 | if self.arc_data.ix[e, 'LowerBound'] < 0: 82 | return pe.Constraint.Skip 83 | return m.Y[e] >= self.arc_data.ix[e, 'LowerBound'] 84 | self.m.LowerBound = pe.Constraint(self.m.arc_set, rule=lower_bounds_rule) 85 | 86 | def solve(self): 87 | """Solve the model.""" 88 | solver = pyomo.opt.SolverFactory('gurobi') 89 | results = solver.solve(self.m, tee=True, keepfiles=False, options_string="mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0") 90 | 91 | if (results.solver.status != pyomo.opt.SolverStatus.ok): 92 | logging.warning('Check solver not ok?') 93 | if (results.solver.termination_condition != pyomo.opt.TerminationCondition.optimal): 94 | logging.warning('Check solver optimality?') 95 | 96 | 97 | if __name__ == '__main__': 98 | sp = MinCostFlow('nodes.csv', 'arcs.csv') 99 | sp.solve() 100 | print('\n\n---------------------------') 101 | print('Cost: ', sp.m.OBJ()) 102 | -------------------------------------------------------------------------------- /pandas_min_cost_flow/nodes.csv: -------------------------------------------------------------------------------- 1 | Node,Imbalance 2 | A,-1 3 | B,0 4 | C,0 5 | D,0 6 | E,1 7 | -------------------------------------------------------------------------------- /row_generation_mst/mst.csv: -------------------------------------------------------------------------------- 1 | startNode,destNode,dist 2 | A,B,1 3 | A,E,3 4 | A,D,4 5 | B,E,2 6 | B,D,4 7 | D,E,4 8 | E,C,4 9 | E,F,7 10 | C,F,5 11 | -------------------------------------------------------------------------------- /row_generation_mst/mst.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# ___________________________________________________________________________\n#\n# Pyomo: Python Optimization Modeling Objects\n# Copyright (c) 2015-2025\n# National Technology and Engineering Solutions of Sandia, LLC\n# Under the terms of Contract DE-NA0003525 with National Technology and\n# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain\n# rights in this software.\n# This software is distributed under the 3-clause BSD License.\n# ___________________________________________________________________________" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 1, 15 | "metadata": {}, 16 | "outputs": [ 17 | { 18 | "name": "stdout", 19 | "output_type": "stream", 20 | "text": [ 21 | "import pyomo\r\n", 22 | "import pyomo.opt\r\n", 23 | "import pyomo.environ as pe\r\n", 24 | "import pandas\r\n", 25 | "import networkx\r\n", 26 | "\r\n", 27 | "class MSTRowGeneration:\r\n", 28 | " \"\"\"A class to find Minimum Spanning Tree using a row-generation algorithm.\"\"\"\r\n", 29 | "\r\n", 30 | " def __init__(self, nfile):\r\n", 31 | " \"\"\"The input is a CSV file describing the undirected network's edges.\"\"\"\r\n", 32 | " self.df = pandas.read_csv(nfile)\r\n", 33 | "\r\n", 34 | " self.createRelaxedModel()\r\n", 35 | "\r\n", 36 | " def createRelaxedModel(self):\r\n", 37 | " \"\"\"Create the relaxed model, without any subtour elimination constraints.\"\"\"\r\n", 38 | " df = self.df\r\n", 39 | " node_set = set( list( df.startNode ) + list(df.destNode) )\r\n", 40 | "\r\n", 41 | " # Create the model and sets\r\n", 42 | " m = pe.ConcreteModel()\r\n", 43 | "\r\n", 44 | " df.set_index(['startNode','destNode'], inplace=True)\r\n", 45 | " edge_set = df.index.unique()\r\n", 46 | "\r\n", 47 | " m.edge_set = pe.Set(initialize=edge_set, dimen=2)\r\n", 48 | " m.node_set = pe.Set(initialize=node_set)\r\n", 49 | " \r\n", 50 | " # Define variables\r\n", 51 | " m.Y = pe.Var(m.edge_set, domain=pe.Binary)\r\n", 52 | "\r\n", 53 | " # Objective\r\n", 54 | " def obj_rule(m):\r\n", 55 | " return sum( m.Y[e] * df.ix[e,'dist'] for e in m.edge_set)\r\n", 56 | " m.OBJ = pe.Objective(rule=obj_rule, sense=pe.minimize)\r\n", 57 | "\r\n", 58 | " # Add the n-1 constraint\r\n", 59 | " def simple_const_rule(m):\r\n", 60 | " return sum( m.Y[e] for e in m.edge_set ) == len(node_set) - 1\r\n", 61 | " m.simpleConst = pe.Constraint(rule = simple_const_rule)\r\n", 62 | " \r\n", 63 | " # Empty constraint list for subtour elimination constraints\r\n", 64 | " # This is where the generated rows will go\r\n", 65 | " m.ccConstraints = pe.ConstraintList()\r\n", 66 | "\r\n", 67 | " self.m = m\r\n", 68 | "\r\n", 69 | " def convertYsToNetworkx(self):\r\n", 70 | " \"\"\"Convert the model's Y variables into a networkx object.\"\"\"\r\n", 71 | " ans = networkx.Graph()\r\n", 72 | " edges = [e for e in self.m.edge_set if self.m.Y[e].value > .99]\r\n", 73 | " ans.add_edges_from(edges)\r\n", 74 | " return ans\r\n", 75 | "\r\n", 76 | " def solve(self):\r\n", 77 | " \"\"\"Solve for the MST, using row generation for subtour elimination constraints.\"\"\"\r\n", 78 | " def createConstForCC(m, cc):\r\n", 79 | " cc = dict.fromkeys(cc)\r\n", 80 | " return sum( m.Y[e] for e in m.edge_set if ((e[0] in cc) and (e[1] in cc))) <= len(cc) - 1\r\n", 81 | " \r\n", 82 | " if not hasattr(self, 'solver'):\r\n", 83 | " solver = pyomo.opt.SolverFactory('gurobi')\r\n", 84 | "\r\n", 85 | " done = False\r\n", 86 | " while not done:\r\n", 87 | " # Solve once and add subtour elimination constraints if necessary\r\n", 88 | " # Finish when there are no more subtours\r\n", 89 | " results = solver.solve(self.m, tee=False, keepfiles=False, options_string=\"mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0\")\r\n", 90 | " # Construct a graph from the answer, and look for subtours\r\n", 91 | " graph = self.convertYsToNetworkx()\r\n", 92 | " ccs = list(networkx.connected_component_subgraphs(graph))\r\n", 93 | " for cc in ccs:\r\n", 94 | " print('Adding constraint for connected component:')\r\n", 95 | " print(cc.nodes())\r\n", 96 | " print(createConstForCC(self.m, cc))\r\n", 97 | " print('--------------\\n')\r\n", 98 | " self.m.ccConstraints.add( createConstForCC(self.m, cc) )\r\n", 99 | " if ccs[0].number_of_nodes() == len(self.m.node_set):\r\n", 100 | " done = True\r\n", 101 | "\r\n", 102 | "mst = MSTRowGeneration('mst.csv')\r\n", 103 | "mst.solve()\r\n", 104 | "\r\n", 105 | "mst.m.Y.pprint()\r\n", 106 | "print(mst.m.OBJ())\r\n" 107 | ] 108 | } 109 | ], 110 | "source": [ 111 | "!cat mst.py" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 2, 117 | "metadata": {}, 118 | "outputs": [ 119 | { 120 | "name": "stdout", 121 | "output_type": "stream", 122 | "text": [ 123 | "Adding constraint for connected component:\r\n", 124 | "['E', 'A', 'B', 'D']\r\n", 125 | "Y[D,E] + Y[A,D] + Y[B,E] + Y[B,D] + Y[A,E] + Y[A,B] <= 3.0\r\n", 126 | "--------------\r\n", 127 | "\r\n", 128 | "Adding constraint for connected component:\r\n", 129 | "['F', 'A', 'B', 'E', 'C']\r\n", 130 | "Y[B,E] + Y[C,F] + Y[E,F] + Y[E,C] + Y[A,E] + Y[A,B] <= 4.0\r\n", 131 | "--------------\r\n", 132 | "\r\n", 133 | "Adding constraint for connected component:\r\n", 134 | "['F', 'A', 'B', 'E', 'C', 'D']\r\n", 135 | "Y[D,E] + Y[A,D] + Y[B,E] + Y[C,F] + Y[E,F] + Y[E,C] + Y[B,D] + Y[A,E] + Y[A,B] <= 5.0\r\n", 136 | "--------------\r\n", 137 | "\r\n", 138 | "Y : Size=9, Index=edge_set\r\n", 139 | " Key : Lower : Value : Upper : Fixed : Stale : Domain\r\n", 140 | " ('A', 'B') : 0 : 1.0 : 1 : False : False : Binary\r\n", 141 | " ('A', 'D') : 0 : 0.0 : 1 : False : False : Binary\r\n", 142 | " ('A', 'E') : 0 : 0.0 : 1 : False : False : Binary\r\n", 143 | " ('B', 'D') : 0 : 0.0 : 1 : False : False : Binary\r\n", 144 | " ('B', 'E') : 0 : 1.0 : 1 : False : False : Binary\r\n", 145 | " ('C', 'F') : 0 : 1.0 : 1 : False : False : Binary\r\n", 146 | " ('D', 'E') : 0 : 1.0 : 1 : False : False : Binary\r\n", 147 | " ('E', 'C') : 0 : 1.0 : 1 : False : False : Binary\r\n", 148 | " ('E', 'F') : 0 : -0.0 : 1 : False : False : Binary\r\n", 149 | "16.0\r\n" 150 | ] 151 | } 152 | ], 153 | "source": [ 154 | "!python mst.py" 155 | ] 156 | } 157 | ], 158 | "metadata": { 159 | "kernelspec": { 160 | "display_name": "Python 3", 161 | "language": "python", 162 | "name": "python3" 163 | }, 164 | "language_info": { 165 | "codemirror_mode": { 166 | "name": "ipython", 167 | "version": 3 168 | }, 169 | "file_extension": ".py", 170 | "mimetype": "text/x-python", 171 | "name": "python", 172 | "nbconvert_exporter": "python", 173 | "pygments_lexer": "ipython3", 174 | "version": "3.6.1" 175 | } 176 | }, 177 | "nbformat": 4, 178 | "nbformat_minor": 1 179 | } -------------------------------------------------------------------------------- /row_generation_mst/mst.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | import pyomo 14 | import pyomo.opt 15 | import pyomo.environ as pe 16 | import pandas 17 | import networkx 18 | 19 | class MSTRowGeneration: 20 | """A class to find Minimum Spanning Tree using a row-generation algorithm.""" 21 | 22 | def __init__(self, nfile): 23 | """The input is a CSV file describing the undirected network's edges.""" 24 | self.df = pandas.read_csv(nfile) 25 | 26 | self.createRelaxedModel() 27 | 28 | def createRelaxedModel(self): 29 | """Create the relaxed model, without any subtour elimination constraints.""" 30 | df = self.df 31 | node_set = set( list( df.startNode ) + list(df.destNode) ) 32 | 33 | # Create the model and sets 34 | m = pe.ConcreteModel() 35 | 36 | df.set_index(['startNode','destNode'], inplace=True) 37 | edge_set = df.index.unique() 38 | 39 | m.edge_set = pe.Set(initialize=edge_set, dimen=2) 40 | m.node_set = pe.Set(initialize=node_set) 41 | 42 | # Define variables 43 | m.Y = pe.Var(m.edge_set, domain=pe.Binary) 44 | 45 | # Objective 46 | def obj_rule(m): 47 | return sum( m.Y[e] * df.ix[e,'dist'] for e in m.edge_set) 48 | m.OBJ = pe.Objective(rule=obj_rule, sense=pe.minimize) 49 | 50 | # Add the n-1 constraint 51 | def simple_const_rule(m): 52 | return sum( m.Y[e] for e in m.edge_set ) == len(node_set) - 1 53 | m.simpleConst = pe.Constraint(rule = simple_const_rule) 54 | 55 | # Empty constraint list for subtour elimination constraints 56 | # This is where the generated rows will go 57 | m.ccConstraints = pe.ConstraintList() 58 | 59 | self.m = m 60 | 61 | def convertYsToNetworkx(self): 62 | """Convert the model's Y variables into a networkx object.""" 63 | ans = networkx.Graph() 64 | edges = [e for e in self.m.edge_set if self.m.Y[e].value > .99] 65 | ans.add_edges_from(edges) 66 | return ans 67 | 68 | def solve(self): 69 | """Solve for the MST, using row generation for subtour elimination constraints.""" 70 | def createConstForCC(m, cc): 71 | cc = dict.fromkeys(cc) 72 | return sum( m.Y[e] for e in m.edge_set if ((e[0] in cc) and (e[1] in cc))) <= len(cc) - 1 73 | 74 | if not hasattr(self, 'solver'): 75 | solver = pyomo.opt.SolverFactory('gurobi') 76 | 77 | done = False 78 | while not done: 79 | # Solve once and add subtour elimination constraints if necessary 80 | # Finish when there are no more subtours 81 | results = solver.solve(self.m, tee=False, keepfiles=False, options_string="mip_tolerances_integrality=1e-9 mip_tolerances_mipgap=0") 82 | # Construct a graph from the answer, and look for subtours 83 | graph = self.convertYsToNetworkx() 84 | ccs = list(networkx.connected_component_subgraphs(graph)) 85 | for cc in ccs: 86 | print('Adding constraint for connected component:') 87 | print(cc.nodes()) 88 | print(createConstForCC(self.m, cc)) 89 | print('--------------\n') 90 | self.m.ccConstraints.add( createConstForCC(self.m, cc) ) 91 | if ccs[0].number_of_nodes() == len(self.m.node_set): 92 | done = True 93 | 94 | mst = MSTRowGeneration('mst.csv') 95 | mst.solve() 96 | 97 | mst.m.Y.pprint() 98 | print(mst.m.OBJ()) 99 | -------------------------------------------------------------------------------- /test_notebooks.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | # 14 | # Jupyter notebook testing logic adapted from 15 | # https://gist.github.com/lheagy/f216db7220713329eb3fc1c2cd3c7826 16 | # 17 | # The MIT License (MIT) 18 | # 19 | # Copyright (c) 2016 Lindsey Heagy 20 | # 21 | # Permission is hereby granted, free of charge, to any person obtaining a copy 22 | # of this software and associated documentation files (the "Software"), to deal 23 | # in the Software without restriction, including without limitation the rights 24 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | # copies of the Software, and to permit persons to whom the Software is 26 | # furnished to do so, subject to the following conditions: 27 | # 28 | # The above copyright notice and this permission notice shall be included in all 29 | # copies or substantial portions of the Software. 30 | # 31 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | # SOFTWARE. 38 | # Raw 39 | 40 | 41 | import unittest 42 | import sys 43 | import os 44 | import subprocess 45 | try: 46 | import jupyter 47 | jupyter_available = True 48 | except: 49 | jupyter_available = False 50 | try: 51 | import pandas 52 | pandas_available = True 53 | except: 54 | pandas_available = False 55 | try: 56 | import networkx 57 | networkx_available = True 58 | except: 59 | networkx_available = False 60 | 61 | timeout=120 62 | 63 | requires_pandas = set(['max_flow_interdict', 'min_cost_flow_interdict', 'multi_commodity_flow_interdict', 'sp_interdict', 'min_cost_flow', 'mst']) 64 | requires_networkx = set(['mst']) 65 | 66 | 67 | # Testing for the notebooks - use nbconvert to execute all cells of the 68 | # notebook 69 | 70 | # For testing on TravisCI, be sure to include a requirements.txt that 71 | # includes jupyter so that you run on the most up-to-date version. 72 | 73 | 74 | # Where are the notebooks? 75 | TESTDIR = os.path.dirname(os.path.abspath(__file__)) 76 | #NBDIR = os.path.sep.join(TESTDIR.split(os.path.sep)[:-2] + ['notebooks/']) # where are the notebooks? 77 | 78 | def setUp(): 79 | nbpaths = [] # list of notebooks, with file paths 80 | nbnames = [] # list of notebook names (for making the tests) 81 | 82 | print(TESTDIR) 83 | # walk the test directory and find all notebooks 84 | for dirname, dirnames, filenames in os.walk(TESTDIR): 85 | for filename in filenames: 86 | if filename.endswith('.ipynb') and not filename.endswith('-checkpoint.ipynb'): 87 | nbpaths.append(os.path.abspath(dirname) + os.path.sep + filename) # get abspath of notebook 88 | nbnames.append(''.join(filename[:-6])) # strip off the file extension 89 | return nbpaths, nbnames 90 | 91 | 92 | def get(nbname, nbpath): 93 | 94 | # use nbconvert to execute the notebook 95 | def test_func(self): 96 | print('\n--------------- Testing {0} ---------------'.format(nbname)) 97 | print(' {0}'.format(nbpath)) 98 | if not jupyter_available: 99 | self.skipTest("Jupyter unavailable") 100 | if nbname in requires_pandas and not pandas_available: 101 | self.skipTest("Pandas unavailable") 102 | if nbname in requires_networkx and not networkx_available: 103 | self.skipTest("Networkx unavailable") 104 | 105 | # execute the notebook using nbconvert to generate html 106 | dir_=os.path.dirname(nbpath) 107 | os.chdir(dir_) 108 | nbexe = subprocess.Popen( 109 | [ 'jupyter', 'nbconvert', '{0}'.format(nbpath), 110 | '--execute', 111 | '--inplace', 112 | '--ExecutePreprocessor.kernel_name=python%s' % ( 113 | {2:"",3:"3"}[sys.version_info[0]], ), 114 | '--ExecutePreprocessor.timeout='+str(timeout)], 115 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, 116 | stderr=subprocess.PIPE) 117 | output, err = nbexe.communicate() 118 | check = nbexe.returncode 119 | if check == 0: 120 | print('\n ..... {0} Passed ..... \n'.format(nbname)) 121 | # if passed remove the generated html file 122 | #subprocess.call(['rm', '{0}.html'.format( os.path.sep.join(os.getcwd().split(os.path.sep) + [nbpath.split(os.path.sep)[-1][:-6]]))]) 123 | else: 124 | print('\n <<<<< {0} FAILED >>>>> \n'.format(nbname)) 125 | print('Captured Output: \n {0}'.format(err)) 126 | 127 | self.assertEqual(check, 0) 128 | 129 | return test_func 130 | 131 | 132 | class TestNotebooks(unittest.TestCase): 133 | pass 134 | 135 | nbpaths, nbnames = setUp() 136 | # Check for duplicates 137 | tmp = set() 138 | for name in nbnames: 139 | if name in tmp: 140 | raise IOError("ERROR: duplicate test name %s" % name) 141 | tmp.add(name) 142 | 143 | # build test for each notebook 144 | for i, nb in enumerate(nbnames): 145 | #print((i,nb,nbpaths[i])) 146 | setattr(TestNotebooks, 'test_'+nb, get(nb, nbpaths[i])) 147 | 148 | 149 | if __name__ == '__main__': 150 | unittest.main() 151 | 152 | -------------------------------------------------------------------------------- /transport/results.yml: -------------------------------------------------------------------------------- 1 | # ========================================================== 2 | # = Solver Results = 3 | # ========================================================== 4 | # ---------------------------------------------------------- 5 | # Problem Information 6 | # ---------------------------------------------------------- 7 | Problem: 8 | - Name: unknown 9 | Lower bound: 153.675 10 | Upper bound: 153.675 11 | Number of objectives: 1 12 | Number of constraints: 6 13 | Number of variables: 7 14 | Number of nonzeros: 13 15 | Sense: minimize 16 | # ---------------------------------------------------------- 17 | # Solver Information 18 | # ---------------------------------------------------------- 19 | Solver: 20 | - Status: ok 21 | Termination condition: optimal 22 | Statistics: 23 | Branch and bound: 24 | Number of bounded subproblems: 0 25 | Number of created subproblems: 0 26 | Error rc: 0 27 | Time: 0.008376121521 28 | # ---------------------------------------------------------- 29 | # Solution Information 30 | # ---------------------------------------------------------- 31 | Solution: 32 | - number of solutions: 1 33 | number of solutions displayed: 1 34 | - Gap: 0.0 35 | Status: feasible 36 | Message: None 37 | Objective: 38 | objective: 39 | Value: 153.675 40 | Variable: 41 | x[seattle,chicago]: 42 | Value: 300 43 | x[san-diego,topeka]: 44 | Value: 275 45 | x[san-diego,new-york]: 46 | Value: 325 47 | Constraint: No values 48 | -------------------------------------------------------------------------------- /transport/transport.py: -------------------------------------------------------------------------------- 1 | # ___________________________________________________________________________ 2 | # 3 | # Pyomo: Python Optimization Modeling Objects 4 | # Copyright (c) 2015-2025 5 | # National Technology and Engineering Solutions of Sandia, LLC 6 | # Under the terms of Contract DE-NA0003525 with National Technology and 7 | # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 8 | # rights in this software. 9 | # This software is distributed under the 3-clause BSD License. 10 | # ___________________________________________________________________________ 11 | 12 | 13 | #!/usr/bin/env python 14 | # -*- coding: utf-8 -*- 15 | 16 | # Import 17 | from pyomo.environ import * 18 | 19 | # Creation of a Concrete Model 20 | model = ConcreteModel() 21 | 22 | ## Define sets ## 23 | # Sets 24 | # i canning plants / seattle, san-diego / 25 | # j markets / new-york, chicago, topeka / ; 26 | model.i = Set(initialize=['seattle','san-diego'], doc='Canning plants') 27 | model.j = Set(initialize=['new-york','chicago', 'topeka'], doc='Markets') 28 | 29 | ## Define parameters ## 30 | # Parameters 31 | # a(i) capacity of plant i in cases 32 | # / seattle 350 33 | # san-diego 600 / 34 | # b(j) demand at market j in cases 35 | # / new-york 325 36 | # chicago 300 37 | # topeka 275 / ; 38 | model.a = Param(model.i, initialize={'seattle':350,'san-diego':600}, doc='Capacity of plant i in cases') 39 | model.b = Param(model.j, initialize={'new-york':325,'chicago':300,'topeka':275}, doc='Demand at market j in cases') 40 | # Table d(i,j) distance in thousands of miles 41 | # new-york chicago topeka 42 | # seattle 2.5 1.7 1.8 43 | # san-diego 2.5 1.8 1.4 ; 44 | dtab = { 45 | ('seattle', 'new-york') : 2.5, 46 | ('seattle', 'chicago') : 1.7, 47 | ('seattle', 'topeka') : 1.8, 48 | ('san-diego','new-york') : 2.5, 49 | ('san-diego','chicago') : 1.8, 50 | ('san-diego','topeka') : 1.4, 51 | } 52 | model.d = Param(model.i, model.j, initialize=dtab, doc='Distance in thousands of miles') 53 | # Scalar f freight in dollars per case per thousand miles /90/ ; 54 | model.f = Param(initialize=90, doc='Freight in dollars per case per thousand miles') 55 | # Parameter c(i,j) transport cost in thousands of dollars per case ; 56 | # c(i,j) = f * d(i,j) / 1000 ; 57 | def c_init(model, i, j): 58 | return model.f * model.d[i,j] / 1000 59 | model.c = Param(model.i, model.j, initialize=c_init, doc='Transport cost in thousands of dollar per case') 60 | 61 | ## Define variables ## 62 | # Variables 63 | # x(i,j) shipment quantities in cases 64 | # z total transportation costs in thousands of dollars ; 65 | # Positive Variable x ; 66 | model.x = Var(model.i, model.j, bounds=(0.0,None), doc='Shipment quantities in case') 67 | 68 | ## Define constraints ## 69 | # supply(i) observe supply limit at plant i 70 | # supply(i) .. sum (j, x(i,j)) =l= a(i) 71 | def supply_rule(model, i): 72 | return sum(model.x[i,j] for j in model.j) <= model.a[i] 73 | model.supply = Constraint(model.i, rule=supply_rule, doc='Observe supply limit at plant i') 74 | # demand(j) satisfy demand at market j ; 75 | # demand(j) .. sum(i, x(i,j)) =g= b(j); 76 | def demand_rule(model, j): 77 | return sum(model.x[i,j] for i in model.i) >= model.b[j] 78 | model.demand = Constraint(model.j, rule=demand_rule, doc='Satisfy demand at market j') 79 | 80 | ## Define Objective and solve ## 81 | # cost define objective function 82 | # cost .. z =e= sum((i,j), c(i,j)*x(i,j)) ; 83 | # Model transport /all/ ; 84 | # Solve transport using lp minimizing z ; 85 | def objective_rule(model): 86 | return sum(model.c[i,j]*model.x[i,j] for i in model.i for j in model.j) 87 | model.objective = Objective(rule=objective_rule, sense=minimize, doc='Define objective function') 88 | 89 | 90 | ## Display of the output ## 91 | # Display x.l, x.m ; 92 | def pyomo_postprocess(options=None, instance=None, results=None): 93 | model.x.display() 94 | 95 | # This is an optional code path that allows the script to be run outside of 96 | # pyomo command-line. For example: python transport.py 97 | if __name__ == '__main__': 98 | # This emulates what the pyomo command-line tools does 99 | from pyomo.opt import SolverFactory 100 | import pyomo.environ 101 | opt = SolverFactory("glpk") 102 | results = opt.solve(model) 103 | #sends results to stdout 104 | results.write() 105 | print("\nDisplaying Solution\n" + '-'*60) 106 | pyomo_postprocess(None, model, results) 107 | 108 | --------------------------------------------------------------------------------