├── Articles ├── 10-times-faster-running-cases-in-parallel │ ├── data │ │ ├── ShelfCases-6-0.xlsx │ │ ├── pallets-200.xlsx │ │ ├── pallets-2000.xlsx │ │ └── pallets-20000.xlsx │ ├── model-4-mpi.py │ ├── model-4-multi.py │ ├── model-4-serial.py │ └── readme.md ├── Academics │ ├── districting-data │ │ ├── OK_county.cpg │ │ ├── OK_county.dbf │ │ ├── OK_county.json │ │ ├── OK_county.prj │ │ ├── OK_county.shp │ │ └── OK_county.shx │ ├── districting_pyomo.ipynb │ └── readme.md ├── Allocate-people-to-balanced-teams │ ├── readme.md │ └── teamallocation.xlsx ├── Aryabhata │ ├── Aryabhata_1.ipynb │ ├── Aryabhata_2.ipynb │ ├── Aryabhata_3.ipynb │ ├── Aryabhata_4.ipynb │ ├── Aryabhata_5.ipynb │ └── readme.md ├── Cables │ ├── Cables_Model_1_Enumeration.ipynb │ ├── Cables_Model_2_random_search_multi.py │ ├── Cables_Model_3_local_search.py │ ├── Cables_Model_4_ortools.py │ ├── Cables_Model_5a_Pyomo_single.py │ ├── Cables_Model_5b_Pyomo_multi.py │ ├── data │ │ ├── data_08.py │ │ ├── data_09.py │ │ ├── data_10.py │ │ ├── data_11.py │ │ ├── data_12.py │ │ ├── data_13.py │ │ ├── data_14.py │ │ ├── data_15.py │ │ ├── data_16.py │ │ ├── data_17.py │ │ ├── data_18.py │ │ ├── data_19.py │ │ ├── data_20.py │ │ ├── data_21.py │ │ ├── data_22.py │ │ ├── data_23.py │ │ ├── data_24.py │ │ └── readme.md │ └── readme.md ├── Copilot_crop_rotation │ ├── crop_rotation.py │ └── readme.md ├── Crossword │ ├── Crossword-1.ipynb │ ├── Crossword-2.ipynb │ ├── components │ │ ├── data-model-1.ipynb │ │ ├── data-model-2.ipynb │ │ ├── formulation-model-1.ipynb │ │ ├── formulation-model-2.ipynb │ │ ├── imports-1.ipynb │ │ ├── main-model-1.ipynb │ │ ├── main-model-2.ipynb │ │ ├── output-model-1.ipynb │ │ ├── readme.md │ │ ├── solver-1.ipynb │ │ └── utilities-1.ipynb │ ├── grid │ │ ├── grid-11-1.xlsx │ │ ├── grid-11-2.xlsx │ │ ├── grid-15-1.xlsx │ │ ├── grid-15-2.xlsx │ │ ├── grid-15-3.xlsx │ │ ├── grid-21-1.xlsx │ │ ├── grid-29-1.xlsx │ │ ├── grid-29-2.xlsx │ │ ├── grid-4-ws.xlsx │ │ ├── grid-5-ws.xlsx │ │ ├── grid-6-ws.xlsx │ │ ├── grid-7-1.xlsx │ │ ├── grid-7-2.xlsx │ │ ├── grid-7-3.xlsx │ │ ├── grid-7-ws.xlsx │ │ ├── grid-9x15.xlsx │ │ ├── grid-x-1.xlsx │ │ └── readme.md │ ├── lexicon │ │ ├── gutenberg.xlsx │ │ ├── large.xlsx │ │ └── readme.md │ └── readme.md ├── Facility-location-optimization-in-Excel │ ├── facilitylocation.xlsx │ └── readme.md ├── Fantasy-sports-Pick-the-best-team │ ├── fantasysports.xlsx │ └── readme.md ├── Green-manufacturing-via-cutting-patterns │ ├── cuttingpatterns.xlsm │ ├── readme.md │ └── woodcutting.xlsx ├── Job-sequencing-to-minimize-completion-time │ ├── jobsequence.xlsx │ └── readme.md ├── Julia-JuMP-vs-Python-Pyomo │ ├── production-mix-julia.ipynb │ ├── productiondata.json │ └── readme.md ├── Logic-constraints │ ├── logicconstraints.xlsx │ └── readme.md ├── Muffin-problem │ ├── muffin_problem.py │ └── readme.md ├── On-the-shoulders-of-giants │ ├── deskallocation.xlsx │ └── readme.md ├── One-dimensional-packing-Wire-cutting │ ├── readme.md │ └── wirecutting.xlsx ├── Optimization-in-Excel-vs-Python │ ├── flight_crew_schedule.ipynb │ ├── flight_schedule.csv │ ├── readme.md │ └── scheduleflights.xlsx ├── Paper-coverage │ ├── components │ │ ├── data-model-1.ipynb │ │ ├── data-model-2.ipynb │ │ ├── data-model-3.ipynb │ │ ├── data-model-4.ipynb │ │ ├── data-model-5a-full.ipynb │ │ ├── data-model-5b-full.ipynb │ │ ├── data-model-5c-full.ipynb │ │ ├── formulation-model-1.ipynb │ │ ├── formulation-model-2.ipynb │ │ ├── formulation-model-5a.ipynb │ │ ├── formulation-model-5b.ipynb │ │ ├── formulation-model-5c.ipynb │ │ ├── imports.ipynb │ │ ├── main-model-1.ipynb │ │ ├── main-model-2.ipynb │ │ ├── main-model-4.ipynb │ │ ├── output-model-1.ipynb │ │ ├── output-model-2.ipynb │ │ ├── output-model-4.ipynb │ │ ├── output-model-5c.ipynb │ │ ├── solver.ipynb │ │ └── utilities.ipynb │ ├── data │ │ ├── data-100-actual-sorted.xlsx │ │ ├── data-100-actual-unsorted.xlsx │ │ ├── data-1000-actual-extended-sorted.xlsx │ │ ├── data-20-sorted.xlsx │ │ ├── data-20-unsorted.xlsx │ │ ├── data-200-actual-extended-sorted.xlsx │ │ └── data-5-example-sorted.xlsx │ ├── paper-coverage-model-1-non-linear.ipynb │ ├── paper-coverage-model-2-linear-partial-set.ipynb │ ├── paper-coverage-model-3-linear-full-set.ipynb │ ├── paper-coverage-model-4-column-generation.ipynb │ ├── paper-coverage-model-5a-BigM.ipynb │ ├── paper-coverage-model-5b-BigM.ipynb │ ├── paper-coverage-model-5c-GDP.ipynb │ └── readme.md ├── Pivot-irrigation │ ├── pivot.py │ └── readme.md ├── Potatoes │ ├── data │ │ ├── data_scenario_1.py │ │ ├── data_scenario_2.py │ │ ├── data_scenario_3.py │ │ ├── data_scenario_4.py │ │ └── data_scenario_current.py │ ├── potato_lmp.ipynb │ └── readme.md ├── Price-breaks-in-a-linear-programming-model │ ├── price-breaks.xlsx │ └── readme.md ├── Production-mix-via-graphical-LP │ ├── productionmix.xlsx │ └── readme.md ├── Production-mix │ ├── Model-01-Pyomo-concrete │ │ ├── production-model-1.ipynb │ │ ├── production-model-1.py │ │ └── readme.md │ ├── Model-02-Pyomo-separate-data │ │ ├── production-model-2.ipynb │ │ ├── production-model-2.py │ │ └── readme.md │ ├── Model-03-Pyomo-external-data │ │ ├── production-model-3.ipynb │ │ ├── production-model-3.py │ │ ├── productiondata3.py │ │ └── readme.md │ ├── Model-04-Pyomo-json-file │ │ ├── production-model-4.ipynb │ │ ├── production-model-4.py │ │ ├── productiondata4.json │ │ └── readme.md │ ├── Model-05-Pyomo-using-def │ │ ├── production-model-5.ipynb │ │ ├── production-model-5.py │ │ ├── productiondata5.json │ │ └── readme.md │ ├── Model-06-Pyomo-abstract │ │ ├── production-model-6.ipynb │ │ ├── production-model-6.py │ │ ├── productiondata6.dat │ │ └── readme.md │ ├── Model-07-PuLP │ │ ├── production-model-7.ipynb │ │ ├── production-model-7.py │ │ ├── productiondata7.json │ │ └── readme.md │ ├── Model-08-OR-Tools │ │ ├── production-model-8.ipynb │ │ ├── production-model-8.py │ │ ├── productiondata8.json │ │ └── readme.md │ ├── Model-09-Gekko │ │ ├── Production-model-9.ipynb │ │ ├── production-model-9.py │ │ ├── productiondata9.json │ │ └── readme.md │ ├── Model-10-CVXPY │ │ ├── production-model-10.ipynb │ │ ├── production-model-10.py │ │ ├── productiondata10.json │ │ └── readme.md │ ├── Model-11-SciPy │ │ ├── production-model-11.ipynb │ │ ├── production-model-11.py │ │ ├── productiondata11.json │ │ └── readme.md │ └── readme.md ├── Project-crashing-Time-cost-trade-off │ ├── projectcrashing.xlsx │ └── readme.md ├── Python-embedded-in-Excel-First-impressions │ ├── Production-Mix-in-Python.xlsx │ └── readme.md ├── RacksShelves │ ├── components │ │ ├── data-model-1.ipynb │ │ ├── data-model-3.ipynb │ │ ├── formulation-model-1.ipynb │ │ ├── formulation-model-2.ipynb │ │ ├── formulation-model-3.ipynb │ │ ├── imports.ipynb │ │ ├── main-1.ipynb │ │ ├── main-2.ipynb │ │ ├── main-3.ipynb │ │ ├── results-model-1.ipynb │ │ ├── results-model-3.ipynb │ │ ├── solver.ipynb │ │ └── utilities.ipynb │ ├── couenne.opt │ ├── data │ │ ├── pallets-200.xlsx │ │ ├── pallets-2000.xlsx │ │ └── pallets-20000.xlsx │ ├── racks-shelves-model-1-non-linear.ipynb │ ├── racks-shelves-model-2-linear.ipynb │ ├── racks-shelves-model-3-linear-enumerate.ipynb │ └── readme.md ├── Refactor-Python-models-into-modules │ ├── Original │ │ └── StudenttoProf.ipynb │ ├── Refactored │ │ ├── Refactored-student-professors.ipynb │ │ ├── data.py │ │ ├── models.py │ │ ├── plots.py │ │ └── utilities.py │ ├── Shallow-and-deep-copying-examples.ipynb │ └── readme.md ├── Role-mining │ ├── data │ │ ├── domino.txt │ │ ├── firewall2.txt │ │ ├── hc.txt │ │ └── sacmat relations.zip │ ├── model_1_pyomo.py │ ├── model_2_cpsat.py │ └── readme.md ├── Schedule-staff-with-enumerated-shifts │ ├── Schedule-staff-with-enumerated-shifts.ipynb │ ├── Schedule-staff-with-enumerated-shifts.py │ ├── Schedule-staff-with-enumerated-shifts.xlsx │ └── readme.md ├── Sorting │ ├── readme.md │ └── sorting_reals.py ├── Symmetry-less-1D-bin-packing │ ├── readme.md │ └── symmetrylessbinpacking.xlsx ├── Taking-a-dip-in-the-MIP-solution-pool │ ├── cplex-pool-subsetsum.opt │ ├── cplex-write-pool-subsetsum.txt │ ├── model-subsetsum.lp │ ├── readme.md │ └── subsetsum.xlsx ├── Vaccination-plan-for-Hong-Kong │ ├── readme.md │ └── vaccineplanning.xlsx ├── Warehouse-optimization │ ├── Frequency-full.xlsx │ ├── Frequency-small.xlsx │ ├── Layout-A.xlsx │ ├── Layout-B.xlsx │ ├── Layout-C.xlsx │ ├── Layout-D.xlsx │ ├── Layout-E.xlsx │ ├── Layout-F.xlsx │ ├── Layout-G.xlsx │ ├── Layout-H.xlsx │ ├── Layout-I.xlsx │ ├── Layout-small.xlsx │ ├── Warehouse-layout.ipynb │ └── readme.md ├── We-need-more-power-NEOS-Server │ ├── equalteams.xlsm │ └── readme.md └── Wrong-hill │ ├── 3D-function.ipynb │ ├── Non-linear-Model-1-standard.ipynb │ ├── Non-linear-Model-2-iterations.ipynb │ ├── Non-linear-Model-3-search.ipynb │ ├── Non-linear-Model-4-multistart.ipynb │ ├── components │ ├── imports.ipynb │ ├── objective-functions.ipynb │ ├── readme.md │ ├── solver-4.ipynb │ └── solver.ipynb │ └── readme.md ├── LICENSE └── README.md /Articles/10-times-faster-running-cases-in-parallel/data/ShelfCases-6-0.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/10-times-faster-running-cases-in-parallel/data/ShelfCases-6-0.xlsx -------------------------------------------------------------------------------- /Articles/10-times-faster-running-cases-in-parallel/data/pallets-200.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/10-times-faster-running-cases-in-parallel/data/pallets-200.xlsx -------------------------------------------------------------------------------- /Articles/10-times-faster-running-cases-in-parallel/data/pallets-2000.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/10-times-faster-running-cases-in-parallel/data/pallets-2000.xlsx -------------------------------------------------------------------------------- /Articles/10-times-faster-running-cases-in-parallel/data/pallets-20000.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/10-times-faster-running-cases-in-parallel/data/pallets-20000.xlsx -------------------------------------------------------------------------------- /Articles/10-times-faster-running-cases-in-parallel/readme.md: -------------------------------------------------------------------------------- 1 | ## 10 times faster, running cases in parallel 2 | In this article, we explore running optimization model cases in parallel. Specifically, we use the Python multiprocessing and mpi4py libraries to fully use the many CPU cores/threads in modern computers. 3 | 4 | Our goals are to: 5 | - Illustrate how to apply the multiprocessing and mpi4py libraries to running optimization model cases in parallel. 6 | - Measure the performance of running cases in parallel compared with serially. 7 | - Compare the performance of an old 4 core / 4 thread CPU with a new 20 core / 28 thread CPU, using the HiGHS solver. 8 | 9 | Blog article: [10 times faster, running cases in parallel](https://www.solvermax.com/blog/10-times-faster-running-cases-in-parallel) 10 | -------------------------------------------------------------------------------- /Articles/Academics/districting-data/OK_county.cpg: -------------------------------------------------------------------------------- 1 | UTF-8 -------------------------------------------------------------------------------- /Articles/Academics/districting-data/OK_county.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /Articles/Academics/districting-data/OK_county.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Academics/districting-data/OK_county.shp -------------------------------------------------------------------------------- /Articles/Academics/districting-data/OK_county.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Academics/districting-data/OK_county.shx -------------------------------------------------------------------------------- /Articles/Academics/readme.md: -------------------------------------------------------------------------------- 1 | ## Academics, please publish your data and code 2 | Academic research papers can be a valuable source of material for creating and improving real world optimization models. But we wish that academics would publish working code and data to accompany their papers. 3 | 4 | In this article: 5 | - Firstly, we briefly look at some reasons why academics might be reluctant to publish their data and code. 6 | - Then we replicate, modify, and explore a [published model](https://github.com/AustinLBuchanan/Districting-Examples-2020/tree/main) that has been done well, with the data and program code publicly available. 7 | 8 | Blog article: [Academics, please publish your data and code](https://www.solvermax.com/blog/academics-please-publish-your-data-and-code) 9 | -------------------------------------------------------------------------------- /Articles/Allocate-people-to-balanced-teams/readme.md: -------------------------------------------------------------------------------- 1 | ## Allocate people to balanced teams 2 | Allocating people to teams is a common task in both sport and business. Often we want the teams to be as balanced as possible. But making the allocation can be a difficult problem, especially if there are many people or we need to form several teams. 3 | 4 | In this article we build and analyze a model that allocates 32 people to four teams, with the objective of each team having an aggregate rating that is as balanced as possible. The task is complicated by having three types of position in the teams, with a diversity of ratings across the available people. 5 | 6 | The model is built in Excel and solved using either Solver or OpenSolver. 7 | 8 | Blog article: [Allocate people to balanced teams](https://www.solvermax.com/blog/allocate-people-to-balanced-teams) 9 | -------------------------------------------------------------------------------- /Articles/Allocate-people-to-balanced-teams/teamallocation.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Allocate-people-to-balanced-teams/teamallocation.xlsx -------------------------------------------------------------------------------- /Articles/Aryabhata/readme.md: -------------------------------------------------------------------------------- 1 | ## Optimal rational approximation using SciPy 2 | In this article we solve a non-linear curve fitting problem using the SciPy library. SciPy is especially well-suiting to solving this type of problem, as it includes a variety of functions for fitting and optimizing non-linear functions. 3 | 4 | Along the way, we illustrate how to use SciPy's curve_fit and minimize functions for this type of task. In addition, we look at some good practices to apply when solving this type of problem, including: 5 | - Using different criteria for defining the best fit, such as sum of squared differences and largest absolute error. 6 | - Examining use of the full_output option when using the curve_fit function, to get more information about the result. 7 | - Examining the success and message values of the minimize function result to ensure that the solver converges correctly. 8 | - Trying a variety of minimize solution methods to see which one works best in our specific situation. 9 | - Fine-tuning the solution by changing the convergence tolerance. 10 | 11 | Blog article: [Optimal rational approximation using SciPy](https://www.solvermax.com/blog/optimal-rational-approximation-using-scipy) 12 | -------------------------------------------------------------------------------- /Articles/Cables/data/data_08.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1] 20 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_09.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2] 21 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_10.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1] 22 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_11.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2] 23 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_12.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2] 24 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_13.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2], 24 | [M, F, 3], 25 | [M, I, 2] 26 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_14.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2], 24 | [M, F, 3], 25 | [M, I, 2], 26 | [N, B, 1], 27 | [N, D, 3] 28 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_15.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2], 24 | [M, F, 3], 25 | [M, I, 2], 26 | [N, B, 1], 27 | [N, D, 3], 28 | [O, J, 1] 29 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_16.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2], 24 | [M, F, 3], 25 | [M, I, 2], 26 | [N, B, 1], 27 | [N, D, 3], 28 | [O, J, 1], 29 | [P, C, 5], 30 | [P, K, 1] 31 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_17.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2], 24 | [M, F, 3], 25 | [M, I, 2], 26 | [N, B, 1], 27 | [N, D, 3], 28 | [O, J, 1], 29 | [P, C, 5], 30 | [P, K, 1], 31 | [Q, B, 1] 32 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_18.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2], 24 | [M, F, 3], 25 | [M, I, 2], 26 | [N, B, 1], 27 | [N, D, 3], 28 | [O, J, 1], 29 | [P, C, 5], 30 | [P, K, 1], 31 | [Q, B, 1], 32 | [R, H, 1] 33 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_19.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2], 24 | [M, F, 3], 25 | [M, I, 2], 26 | [N, B, 1], 27 | [N, D, 3], 28 | [O, J, 1], 29 | [P, C, 5], 30 | [P, K, 1], 31 | [Q, B, 1], 32 | [R, H, 1], 33 | [S, E, 1] 34 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_20.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2], 24 | [M, F, 3], 25 | [M, I, 2], 26 | [N, B, 1], 27 | [N, D, 3], 28 | [O, J, 1], 29 | [P, C, 5], 30 | [P, K, 1], 31 | [Q, B, 1], 32 | [R, H, 1], 33 | [S, E, 1], 34 | [T, J, 1] 35 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_21.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2], 24 | [M, F, 3], 25 | [M, I, 2], 26 | [N, B, 1], 27 | [N, D, 3], 28 | [O, J, 1], 29 | [P, C, 5], 30 | [P, K, 1], 31 | [Q, B, 1], 32 | [R, H, 1], 33 | [S, E, 1], 34 | [T, J, 1], 35 | [U, D, 1] 36 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_22.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2], 24 | [M, F, 3], 25 | [M, I, 2], 26 | [N, B, 1], 27 | [N, D, 3], 28 | [O, J, 1], 29 | [P, C, 5], 30 | [P, K, 1], 31 | [Q, B, 1], 32 | [R, H, 1], 33 | [S, E, 1], 34 | [T, J, 1], 35 | [U, D, 1], 36 | [V, N, 1] 37 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_23.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2], 24 | [M, F, 3], 25 | [M, I, 2], 26 | [N, B, 1], 27 | [N, D, 3], 28 | [O, J, 1], 29 | [P, C, 5], 30 | [P, K, 1], 31 | [Q, B, 1], 32 | [R, H, 1], 33 | [S, E, 1], 34 | [T, J, 1], 35 | [U, D, 1], 36 | [V, N, 1], 37 | [W, P, 1] 38 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/data_24.py: -------------------------------------------------------------------------------- 1 | # Machine names must be consecutive letters, starting with A 2 | A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = list(range(26)) # Machine names 3 | 4 | cable_struct = [[A, B, 2], # machine_from, machine_to, cables 5 | [A, C, 1], 6 | [A, F, 3], 7 | [A, H, 1], 8 | [B, D, 1], 9 | [B, F, 2], 10 | [B, G, 1], 11 | [C, B, 2], 12 | [C, H, 1], 13 | [D, E, 1], 14 | [D, F, 2], 15 | [E, B, 1], 16 | [F, G, 1], 17 | [F, H, 2], 18 | [G, A, 1], 19 | [H, B, 1], 20 | [I, G, 2], 21 | [J, E, 1], 22 | [K, H, 2], 23 | [L, C, 2], 24 | [M, F, 3], 25 | [M, I, 2], 26 | [N, B, 1], 27 | [N, D, 3], 28 | [O, J, 1], 29 | [P, C, 5], 30 | [P, K, 1], 31 | [Q, B, 1], 32 | [R, H, 1], 33 | [S, E, 1], 34 | [T, J, 1], 35 | [U, D, 1], 36 | [V, N, 1], 37 | [W, P, 1], 38 | [X, C, 1] 39 | ] -------------------------------------------------------------------------------- /Articles/Cables/data/readme.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Articles/Cables/readme.md: -------------------------------------------------------------------------------- 1 | ## Well, that escalated quickly 2 | In this series of articles, we look at a simple situation that requires deciding the best order for positioning devices in a rack. 3 | 4 | We use four methods for solving this problem: 5 | - Model 1. Enumerate all possible position orders. 6 | - Model 2. Search randomly for a specified time. 7 | - Model 3. Local search for a specified time. 8 | - Model 4. Constraint programming using OR-Tools. 9 | - Model 5. Mixed integer linear programming using Pyomo. 10 | 11 | Blog article: [Well, that escalated quickly](https://www.solvermax.com/blog/well-that-escalated-quickly-enumeration) 12 | -------------------------------------------------------------------------------- /Articles/Copilot_crop_rotation/readme.md: -------------------------------------------------------------------------------- 1 | ## Crop rotation: Can AI code an entire optimization model? 2 | In this article we pursue an ambitious goal: Create an entire, non-trivial optimization model using Artificial Intelligence (AI). 3 | 4 | We report our experience of using Copilot to write a model for us. Rather than presenting a verbatim transcript, which is very long, we focus on what went well and what didn't go well as the model evolves. We also summarize general lessons from the process. 5 | 6 | Blog article: https://www.solvermax.com/blog/can-ai-code-an-entire-optimization-model 7 | -------------------------------------------------------------------------------- /Articles/Crossword/components/formulation-model-1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "1ab9c709-a751-4942-9f89-622fe70a09bb", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Define model\n", 17 | "def DefineModel(Model):\n", 18 | " Model.Allocation = pyo.Var(Model.Candidate, Model.GridWords, within = pyo.Binary, initialize = 0) # Allocate candidate words to grid\n", 19 | "\n", 20 | " def rule_PosOnce(Model, g): # Allocate exactly one candidate to each grid word position\n", 21 | " return sum(Model.Allocation[c, g] for c in Model.Candidate) == 1\n", 22 | " Model.EachPositionOnce = pyo.Constraint(Model.GridWords, rule = rule_PosOnce)\n", 23 | " \n", 24 | " def rule_WordOnce(Model, c): # Allocate each word to a grid position at most once. Optional constraint\n", 25 | " return sum(Model.Allocation[c, g] for g in Model.GridWords) <= 1\n", 26 | " Model.EachWordOnce = pyo.Constraint(Model.Candidate, rule = rule_WordOnce)\n", 27 | "\n", 28 | " def rule_Fit(Model, g): # Ensure word exactly fills its allocated grid space\n", 29 | " return sum(Model.Allocation[c, g] * Model.Length[c] for c in Model.Candidate) == Model.GridLengths[g]\n", 30 | " Model.WordsFit = pyo.Constraint(Model.GridWords, rule = rule_Fit)\n", 31 | "\n", 32 | " def rule_Intersection(Model, g1, g2, w, h): # The intersection of grid words must have the same letter\n", 33 | " if pyo.value(Model.AcrossRef[w, h]) != 0 and pyo.value(Model.DownRef[w, h]) != 0 and g1 == pyo.value(Model.AcrossRef[w, h]) - 1 \\\n", 34 | " and g2 == pyo.value(Model.DownRef[w, h]) - 1:\n", 35 | " return sum(Model.Allocation[c, g1] * Model.Word[c, pyo.value(Model.AcrossPos[w, h]) - 1] for c in Model.Candidate) == \\\n", 36 | " sum(Model.Allocation[c, g2] * Model.Word[c, pyo.value(Model.DownPos[w, h]) - 1] for c in Model.Candidate)\n", 37 | " else:\n", 38 | " return pyo.Constraint.Skip\n", 39 | " Model.Crossover = pyo.Constraint(Model.GridWords, Model.GridWords, Model.GridWidth, Model.GridHeight, rule = rule_Intersection)\n", 40 | "\n", 41 | " def rule_Obj(Model):\n", 42 | " return sum(sum(Model.Allocation[c, g] for g in Model.GridWords) * Model.Frequency[c] for c in Model.Candidate)\n", 43 | " Model.Obj = pyo.Objective(rule = rule_Obj, sense = pyo.maximize)" 44 | ] 45 | } 46 | ], 47 | "metadata": { 48 | "kernelspec": { 49 | "display_name": "Python 3 (ipykernel)", 50 | "language": "python", 51 | "name": "python3" 52 | }, 53 | "language_info": { 54 | "codemirror_mode": { 55 | "name": "ipython", 56 | "version": 3 57 | }, 58 | "file_extension": ".py", 59 | "mimetype": "text/x-python", 60 | "name": "python", 61 | "nbconvert_exporter": "python", 62 | "pygments_lexer": "ipython3", 63 | "version": "3.9.13" 64 | } 65 | }, 66 | "nbformat": 4, 67 | "nbformat_minor": 5 68 | } 69 | -------------------------------------------------------------------------------- /Articles/Crossword/components/imports-1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "7ae23084-887a-4777-a63c-aecfbb663992", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Import dependencies\n", 17 | "import pyomo.environ as pyo\n", 18 | "import pandas as pd\n", 19 | "import numpy as np\n", 20 | "import time as tm\n", 21 | "import os.path\n", 22 | "from openpyxl import load_workbook\n", 23 | "from openpyxl.utils.cell import range_boundaries\n", 24 | "import random as rnd" 25 | ] 26 | } 27 | ], 28 | "metadata": { 29 | "kernelspec": { 30 | "display_name": "Python 3 (ipykernel)", 31 | "language": "python", 32 | "name": "python3" 33 | }, 34 | "language_info": { 35 | "codemirror_mode": { 36 | "name": "ipython", 37 | "version": 3 38 | }, 39 | "file_extension": ".py", 40 | "mimetype": "text/x-python", 41 | "name": "python", 42 | "nbconvert_exporter": "python", 43 | "pygments_lexer": "ipython3", 44 | "version": "3.9.13" 45 | } 46 | }, 47 | "nbformat": 4, 48 | "nbformat_minor": 5 49 | } 50 | -------------------------------------------------------------------------------- /Articles/Crossword/components/main-model-1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "4cfff072-8d5c-4e76-9072-3887ec111953", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def Main():\n", 11 | " Timer('Start');\n", 12 | " Finished = False\n", 13 | " Iteration = 0\n", 14 | " Seed = RandomSeed\n", 15 | "\n", 16 | " while not Finished:\n", 17 | " rnd.seed(Seed)\n", 18 | " Model = pyo.ConcreteModel(name = ModelName)\n", 19 | " Model.Engine = SolverName\n", 20 | " Model.TimeLimit = TimeLimit\n", 21 | " if Iteration == 0:\n", 22 | " print(Model.name) # Print the model name only once\n", 23 | " print('Grid: ', GridFile.rsplit('\\\\', 1)[-1])\n", 24 | " print('Lexicon: ', WordFile.rsplit('\\\\', 1)[-1])\n", 25 | " print('\\nIteration: ', Iteration + 1, ' of ', MaxIterations)\n", 26 | " Rank, Frequency, Word, GridWords, AcrossRef, AcrossPos, DownRef, DownPos = GetData(WordFile, WordWorksheet, GridFile, GridWorksheet)\n", 27 | " DefineModelData(Model, Rank, Frequency, Word, GridWords, AcrossRef, AcrossPos, DownRef, DownPos)\n", 28 | " Solver, Model = SetUpSolver(Model)\n", 29 | " Timer('Setup');\n", 30 | " DefineModel(Model)\n", 31 | " WriteModelToFile(WriteFile, Model) # Write model to file, if required\n", 32 | " Timer('Create model');\n", 33 | " print('Calling solver...')\n", 34 | " Results, Model = CallSolver(Solver, Model)\n", 35 | " Timer('Solved');\n", 36 | " \n", 37 | " if (Results.solver.status == pyo.SolverStatus.ok) or (Solver._solver_model.SolCount >= 1): # Feasible/optimal or found at least one feasible solution\n", 38 | " print('Solution for random seed:', Seed, '\\n')\n", 39 | " Model.solutions.load_from(Results) # Defer loading results until now, in case there is no solution to load\n", 40 | " WriteOutput(Model, Results)\n", 41 | " if StopOnFirst: # Ignore iteration count and stop on first solution\n", 42 | " Finished = True\n", 43 | " else:\n", 44 | " print('No solution for random seed:', Seed, '(', Results.solver.termination_condition, ')', '\\n')\n", 45 | " Iteration += 1 \n", 46 | " Seed += 1\n", 47 | " if Iteration >= MaxIterations: # If not stopping on first iteration, then stop after specified number of iterations\n", 48 | " Finished = True\n", 49 | " Timer('Finish')\n", 50 | " WriteCheckpoints()" 51 | ] 52 | } 53 | ], 54 | "metadata": { 55 | "kernelspec": { 56 | "display_name": "Python 3 (ipykernel)", 57 | "language": "python", 58 | "name": "python3" 59 | }, 60 | "language_info": { 61 | "codemirror_mode": { 62 | "name": "ipython", 63 | "version": 3 64 | }, 65 | "file_extension": ".py", 66 | "mimetype": "text/x-python", 67 | "name": "python", 68 | "nbconvert_exporter": "python", 69 | "pygments_lexer": "ipython3", 70 | "version": "3.9.13" 71 | } 72 | }, 73 | "nbformat": 4, 74 | "nbformat_minor": 5 75 | } 76 | -------------------------------------------------------------------------------- /Articles/Crossword/components/main-model-2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "4cfff072-8d5c-4e76-9072-3887ec111953", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def Main():\n", 11 | " Timer('Start');\n", 12 | " Finished = False\n", 13 | " Iteration = 0\n", 14 | " Seed = RandomSeed\n", 15 | "\n", 16 | " while not Finished:\n", 17 | " rnd.seed(Seed)\n", 18 | " Model = pyo.ConcreteModel(name = ModelName)\n", 19 | " Model.Engine = SolverName\n", 20 | " Model.TimeLimit = TimeLimit\n", 21 | " if Iteration == 0:\n", 22 | " print(Model.name) # Print the model name only once\n", 23 | " print('Grid: ', GridFile.rsplit('\\\\', 1)[-1])\n", 24 | " print('Lexicon: ', WordFile.rsplit('\\\\', 1)[-1])\n", 25 | " print('\\nIteration: ', Iteration + 1, ' of ', MaxIterations)\n", 26 | " Rank, Frequency, Word, GridWords, NumIntersections, AcrossRef, AcrossPos, DownRef, DownPos, UseFixWords, FixWords = GetData(WordFile, WordWorksheet, GridFile, GridWorksheet)\n", 27 | " DefineModelData(Model, Rank, Frequency, Word, GridWords, NumIntersections, AcrossRef, AcrossPos, DownRef, DownPos, UseFixWords, FixWords)\n", 28 | " Solver, Model = SetUpSolver(Model)\n", 29 | " Timer('Setup');\n", 30 | " DefineModel(Model)\n", 31 | " WriteModelToFile(WriteFile, Model) # Write model to file, if required\n", 32 | " Timer('Create model');\n", 33 | " print('Calling solver...')\n", 34 | " Results, Model = CallSolver(Solver, Model)\n", 35 | " Timer('Solved');\n", 36 | " \n", 37 | " if SolverName == 'gurobi_direct':\n", 38 | " SolutionFound = (Results.solver.status == pyo.SolverStatus.ok) or (Solver._solver_model.SolCount >= 1) # Feasible/optimal or found at least one feasible solution\n", 39 | " else:\n", 40 | " SolutionFound = (Results.solver.status == pyo.SolverStatus.ok)\n", 41 | " \n", 42 | " if SolutionFound:\n", 43 | " print('Solution for random seed:', Seed, '\\n')\n", 44 | " Model.solutions.load_from(Results) # Defer loading results until now, in case there is no solution to load\n", 45 | " WriteOutput(Model, Results)\n", 46 | " if StopOnFirst: # Ignore iteration count and stop on first solution\n", 47 | " Finished = True\n", 48 | " else:\n", 49 | " print('No solution for random seed:', Seed, '(', Results.solver.termination_condition, ')', '\\n')\n", 50 | " Iteration += 1 \n", 51 | " Seed += 1\n", 52 | " if Iteration >= MaxIterations: # If not stopping on first iteration, then stop after specified number of iterations\n", 53 | " Finished = True\n", 54 | " Timer('Finish')\n", 55 | " WriteCheckpoints()" 56 | ] 57 | } 58 | ], 59 | "metadata": { 60 | "kernelspec": { 61 | "display_name": "Python 3 (ipykernel)", 62 | "language": "python", 63 | "name": "python3" 64 | }, 65 | "language_info": { 66 | "codemirror_mode": { 67 | "name": "ipython", 68 | "version": 3 69 | }, 70 | "file_extension": ".py", 71 | "mimetype": "text/x-python", 72 | "name": "python", 73 | "nbconvert_exporter": "python", 74 | "pygments_lexer": "ipython3", 75 | "version": "3.9.13" 76 | } 77 | }, 78 | "nbformat": 4, 79 | "nbformat_minor": 5 80 | } 81 | -------------------------------------------------------------------------------- /Articles/Crossword/components/output-model-1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "e7c51632-972c-4475-9775-cf3e6cd62d62", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def WriteOutput(Model, Results):\n", 11 | " Obj = pyo.value(Model.Obj())\n", 12 | " print(f'Objective: {Obj:,.2f}')\n", 13 | " print('Intersections: ', len(Model.Crossover)) # Number of across/down word intersections\n", 14 | " print('\\nWords:')\n", 15 | " for g in Model.GridWords:\n", 16 | " for c in Model.Candidate:\n", 17 | " if np.isclose(pyo.value(Model.Allocation[c, g]), 1): # Allow for small deviations from binary values\n", 18 | " CurrWord = \"\"\n", 19 | " for i in range(0, pyo.value(Model.Length[c])):\n", 20 | " CurrWord += chr(pyo.value(Model.Word[c, i])) # Get characters of allocated words\n", 21 | " print(g + 1, \": \", CurrWord)" 22 | ] 23 | } 24 | ], 25 | "metadata": { 26 | "kernelspec": { 27 | "display_name": "Python 3 (ipykernel)", 28 | "language": "python", 29 | "name": "python3" 30 | }, 31 | "language_info": { 32 | "codemirror_mode": { 33 | "name": "ipython", 34 | "version": 3 35 | }, 36 | "file_extension": ".py", 37 | "mimetype": "text/x-python", 38 | "name": "python", 39 | "nbconvert_exporter": "python", 40 | "pygments_lexer": "ipython3", 41 | "version": "3.9.13" 42 | } 43 | }, 44 | "nbformat": 4, 45 | "nbformat_minor": 5 46 | } 47 | -------------------------------------------------------------------------------- /Articles/Crossword/components/readme.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Articles/Crossword/components/utilities-1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "ab1d11b1-6b18-4141-a7d2-b94bbbc54051", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Record time checkpoints\n", 17 | "# Requires global variable: Checkpoints = []\n", 18 | "def Timer(Point): # String label for current checkpoint\n", 19 | " Checkpoints.append([Point, tm.perf_counter()])" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 2, 25 | "id": "700fcc70-15c8-458d-b897-687db1cbf9c2", 26 | "metadata": { 27 | "editable": true, 28 | "slideshow": { 29 | "slide_type": "" 30 | }, 31 | "tags": [] 32 | }, 33 | "outputs": [], 34 | "source": [ 35 | "# Output list of checkpoint labels and times\n", 36 | "def WriteCheckpoints():\n", 37 | " print('\\nCheckpoint Seconds')\n", 38 | " print('---------------------')\n", 39 | " Start = Checkpoints[0][1]\n", 40 | " for i in range(1, len(Checkpoints)):\n", 41 | " Point = Checkpoints[i][0]\n", 42 | " TimeStep = Checkpoints[i][1] - Start\n", 43 | " print(f'{Point:12}{TimeStep:9,.1f}')" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 3, 49 | "id": "1f6a82cc-7ac3-4446-bd7d-1d1ad86eadc4", 50 | "metadata": { 51 | "editable": true, 52 | "slideshow": { 53 | "slide_type": "" 54 | }, 55 | "tags": [] 56 | }, 57 | "outputs": [], 58 | "source": [ 59 | "# Generic loader from Excel file, given worksheet and named range\n", 60 | "def LoadFromExcel(ExcelFile, Worksheet, Range):\n", 61 | " wb = load_workbook(filename=ExcelFile, read_only = True, data_only = True)\n", 62 | " ws = wb[Worksheet]\n", 63 | " dests = wb.defined_names[Range].destinations\n", 64 | " for title, coord in dests:\n", 65 | " min_col, min_row, max_col, max_row = range_boundaries(coord)\n", 66 | " data = ws.iter_rows(min_row, max_row, min_col, max_col, values_only = True)\n", 67 | " df = pd.DataFrame(data)\n", 68 | " return df" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "id": "6cc58099-1915-4cf4-92aa-7b096fce4a36", 75 | "metadata": { 76 | "editable": true, 77 | "slideshow": { 78 | "slide_type": "" 79 | }, 80 | "tags": [] 81 | }, 82 | "outputs": [], 83 | "source": [ 84 | "# Write model to file, if required. The format will be inferred by Pyomo from the file extension, e.g. .gams or .nl\n", 85 | "def WriteModelToFile(WriteFile, Model):\n", 86 | " if WriteFile:\n", 87 | " Model.write(ModelFile, io_options={'symbolic_solver_labels': False}) # symbolic_solver_labels of True is easier to read, but a longer file" 88 | ] 89 | } 90 | ], 91 | "metadata": { 92 | "kernelspec": { 93 | "display_name": "Python 3 (ipykernel)", 94 | "language": "python", 95 | "name": "python3" 96 | }, 97 | "language_info": { 98 | "codemirror_mode": { 99 | "name": "ipython", 100 | "version": 3 101 | }, 102 | "file_extension": ".py", 103 | "mimetype": "text/x-python", 104 | "name": "python", 105 | "nbconvert_exporter": "python", 106 | "pygments_lexer": "ipython3", 107 | "version": "3.9.13" 108 | } 109 | }, 110 | "nbformat": 4, 111 | "nbformat_minor": 5 112 | } 113 | -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-11-1.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-11-1.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-11-2.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-11-2.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-15-1.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-15-1.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-15-2.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-15-2.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-15-3.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-15-3.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-21-1.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-21-1.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-29-1.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-29-1.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-29-2.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-29-2.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-4-ws.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-4-ws.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-5-ws.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-5-ws.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-6-ws.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-6-ws.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-7-1.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-7-1.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-7-2.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-7-2.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-7-3.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-7-3.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-7-ws.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-7-ws.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-9x15.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-9x15.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/grid-x-1.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/grid/grid-x-1.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/grid/readme.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Articles/Crossword/lexicon/gutenberg.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/lexicon/gutenberg.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/lexicon/large.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Crossword/lexicon/large.xlsx -------------------------------------------------------------------------------- /Articles/Crossword/lexicon/readme.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Articles/Crossword/readme.md: -------------------------------------------------------------------------------- 1 | ## Crossword MILP 2 | Completing crossword puzzles is a popular pastime. There are many puzzle variations, ranging from simple to fiendishly difficult. 3 | 4 | The process of creating a crossword puzzle – known as "compiling" – is difficult. Compiling a crossword can be thought of as a type of search problem, where we need to find a set of words that fits the rules for filling a specific crossword grid. Unsurprisingly, many crossword compilers use software to help them find a suitable set of words. 5 | 6 | We build and test two Mixed Integer Linear Program (MILP) models to compile crosswords. 7 | 8 | In this series of articles, we discuss: 9 | 10 | - Representing a word puzzle in mathematical terms, enabling us to formulate the crossword compilation problem as a MILP model. 11 | - Techniques for fine-tuning the model-building process in Pyomo, to make a model smaller and faster, including omitting constraint terms, skipping constraints, and fixing variable values. 12 | 13 | Blog article: https://www.solvermax.com/blog/crossword-milp-model-1 14 | -------------------------------------------------------------------------------- /Articles/Facility-location-optimization-in-Excel/facilitylocation.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Facility-location-optimization-in-Excel/facilitylocation.xlsx -------------------------------------------------------------------------------- /Articles/Facility-location-optimization-in-Excel/readme.md: -------------------------------------------------------------------------------- 1 | ## Facility location optimization in Excel 2 | The "facility location problem" is a common, and often difficult, decision that organizations need to make. 3 | 4 | Facilities may include assets such as factories, warehouses, shops, mobile telephone sites, etc. Such assets are typically long-lived, so the decision about where to build them has a long-term impact on the organization. 5 | 6 | A wrong, or sub-optimal, decision is likely to be very expensive. To help make the decision, we can model the facility location problem using an optimization model in Excel. 7 | 8 | Blog article: [Facility location optimization in Excel](https://www.solvermax.com/blog/facility-location) 9 | -------------------------------------------------------------------------------- /Articles/Fantasy-sports-Pick-the-best-team/fantasysports.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Fantasy-sports-Pick-the-best-team/fantasysports.xlsx -------------------------------------------------------------------------------- /Articles/Fantasy-sports-Pick-the-best-team/readme.md: -------------------------------------------------------------------------------- 1 | ## Fantasy sports: Pick the best team 2 | "Fantasy sports" is a popular pastime. Like in many real sports, the performance of a fantasy sports team may be determined by relatively small margins. The selection of a slightly better team can lead to substantially better performance. 3 | 4 | To help achieve better team performance, we explore a team selection optimization model built in Excel and solved with OpenSolver. 5 | 6 | Blog article: [Fantasy sports: Pick the best team](https://www.solvermax.com/blog/fantasy-sports) 7 | -------------------------------------------------------------------------------- /Articles/Green-manufacturing-via-cutting-patterns/cuttingpatterns.xlsm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Green-manufacturing-via-cutting-patterns/cuttingpatterns.xlsm -------------------------------------------------------------------------------- /Articles/Green-manufacturing-via-cutting-patterns/readme.md: -------------------------------------------------------------------------------- 1 | ## Green manufacturing via cutting patterns 2 | In this article we replicate a wood-cutting model from a published academic paper, "An application of cutting-stock problem in green manufacturing: A case study of wooden pallet industry". A key motivation of the case study is the minimization of waste, as a means of reducing the manufacturer's environmental impact. 3 | 4 | The paper's model is built in Excel and solved using OpenSolver, so we do the same. More importantly, the paper illustrates a useful pattern selection technique for formulating and solving large one-dimensional cutting problems. 5 | 6 | We focus on replicating the paper's model – though, surprisingly, we obtain a better result. 7 | 8 | Blog article: [Green manufacturing via cutting patterns](https://www.solvermax.com/blog/green-manufacturing-via-cutting-patterns) 9 | -------------------------------------------------------------------------------- /Articles/Green-manufacturing-via-cutting-patterns/woodcutting.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Green-manufacturing-via-cutting-patterns/woodcutting.xlsx -------------------------------------------------------------------------------- /Articles/Job-sequencing-to-minimize-completion-time/jobsequence.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Job-sequencing-to-minimize-completion-time/jobsequence.xlsx -------------------------------------------------------------------------------- /Articles/Job-sequencing-to-minimize-completion-time/readme.md: -------------------------------------------------------------------------------- 1 | ## Job sequencing to minimize completion time 2 | "Job sequencing" is a common management problem, especially in manufacturing and production businesses. 3 | 4 | That is, given a set of jobs, what is the job sequence that takes the minimum total time? 5 | 6 | The benefits from improved sequencing can be substantial, while a poor sequencing solution can be expensive. To illustrate how an optimization model can help us make the decision, in this article we solve a simple job sequencing problem using Solver. 7 | 8 | Blog article: [Job sequencing to minimize completion time](https://www.solvermax.com/blog/job-sequence) 9 | -------------------------------------------------------------------------------- /Articles/Julia-JuMP-vs-Python-Pyomo/productiondata.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Boutique pottery shop - Julia/JuMP", 3 | "Hours": 250, 4 | "kg": 500, 5 | "SalesLimit": 0, 6 | "Coefficients": { 7 | "Discs": {"People": 12.50, "Materials": 18.00, "Sales": -2.00, "Margin": 80.00}, 8 | "Orbs": {"People": 10.00, "Materials": 30.00, "Sales": 1.00, "Margin": 200.00} 9 | }, 10 | "VarInitial": 0, 11 | "VarLBounds": 0, 12 | "VarUBounds": 100, 13 | "Engine": "HiGHS", 14 | "TimeLimit": 60 15 | } -------------------------------------------------------------------------------- /Articles/Julia-JuMP-vs-Python-Pyomo/readme.md: -------------------------------------------------------------------------------- 1 | ## Julia/JuMP vs Python/Pyomo 2 | In this article we develop an optimization model in the Julia programming language, using the JuMP mathematical optimization package. 3 | 4 | Our objectives are to: 5 | - Write a linear programming model using Julia/JuMP. 6 | - Compare the model with an equivalent model written using Python/Pyomo. 7 | 8 | Although Julia/JuMP is not yet widely used outside universities, we expect that it will become an important tool for optimization modelling over the next few years. 9 | 10 | Blog article: https://www.solvermax.com/blog/julia-jump-vs-python-pyomo 11 | -------------------------------------------------------------------------------- /Articles/Logic-constraints/logicconstraints.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Logic-constraints/logicconstraints.xlsx -------------------------------------------------------------------------------- /Articles/Logic-constraints/readme.md: -------------------------------------------------------------------------------- 1 | ## Logic constraints 2 | When formulating a model, we often have a situation described in terms of logic conditions where something implies something else. For example: 3 | - If this happens, then we must do that. 4 | - If this and that happen, then we can't do some other thing. 5 | - We can do some or all of these things, but only if that other thing happens. 6 | 7 | However, mathematical programming models cannot represent logic conditions directly. Therefore, we need to translate the conditions, known as "implications", into a set of constraints that represent the logic. 8 | 9 | In Part 1 we describe a technique for converting a logical implication into an equivalent set of constraints. Along the way, we'll dabble in applying some formal logic notation, define rules for manipulating formal logical implications, have a brief look at Conjunctive Normal Form (CNF), and learn how to convert CNF into constraints. 10 | 11 | Part 2 describes an alternative representation of this technique, specifically designed to improve the proficiency of students learning to formulate models. 12 | 13 | Blog articles: 14 | 15 | [Logic conditions as constraints - Part 1](https://www.solvermax.com/blog/logic-conditions-as-constraints-part-1) 16 | 17 | [Logic conditions as constraints - Part 2](https://www.solvermax.com/blog/logic-conditions-as-constraints-part-2) 18 | -------------------------------------------------------------------------------- /Articles/Muffin-problem/readme.md: -------------------------------------------------------------------------------- 1 | ## The muffin problem 2 | In this article, we solve 'The Muffin Problem' which is a recreational maths puzzle that is simple to state but hard to solve in general. 3 | 4 | The goal of the muffin problem is to divide a number of muffins equally between a number of students, maximizing the size of the smallest piece that any student receives. Some cases are trivial, while some others are easy. But once we have more than a few muffins and/or students, this becomes a difficult problem to solve. 5 | 6 | We use a Mixed Integer Linear Program to solve the problem. Along the way we play with some parallel computing to substantially reduce the run time, and throw in some symmetry-breaking constraints and objective function bounds to help the solver. 7 | 8 | Blog article: [The muffin problem: A slice of recreational maths](https://www.solvermax.com/blog/the-muffin-problem-a-slice-of-recreational-maths) 9 | -------------------------------------------------------------------------------- /Articles/On-the-shoulders-of-giants/deskallocation.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/On-the-shoulders-of-giants/deskallocation.xlsx -------------------------------------------------------------------------------- /Articles/On-the-shoulders-of-giants/readme.md: -------------------------------------------------------------------------------- 1 | ## On the shoulders of giants 2 | Designing an optimization model can be difficult. In seeking guidance, we often look for inspiration from the published academic literature. Since the 1950s, there has been an enormous amount of research into how to design and solve optimization models for a wide range of situations, with models for just about anything we could want. 3 | 4 | There is much that we can learn from the literature, if only we could understand it. Many academic papers include a statement of the model's design, known as the formulation, using mathematical notation that can look impenetrable. The notation is seldom explained, so translating a formulation into a working model can be a perplexing and frustrating process. 5 | 6 | But with a little knowledge about what the notation means, we can gain a lot of insight into how to design or improve our models. The purpose of this article is to explain the meaning of common mathematical notation and illustrate how we can apply the knowledge contained in academic papers to help us build working models in Excel. 7 | 8 | Blog article: [On the shoulders of giants](https://www.solvermax.com/blog/on-the-shoulders-of-giants) 9 | -------------------------------------------------------------------------------- /Articles/One-dimensional-packing-Wire-cutting/readme.md: -------------------------------------------------------------------------------- 1 | ## One-dimensional packing: Wire cutting 2 | "Wire cutting" is a common type of production problem. That is, given available stock of wire (or other one-dimensional stock material) and a list of pieces required, what is the best way to cut the stock to fulfil the requirements while minimizing waste? 3 | 4 | The benefits from improved cutting can be substantial, while poor cutting choices can be expensive. To illustrate how an optimization model can help us make the decision, in this article we design, build, and solve a wire cutting problem using OpenSolver. 5 | 6 | Blog article: [One-dimensional packing: Wire cutting](https://www.solvermax.com/blog/wire-cutting) 7 | -------------------------------------------------------------------------------- /Articles/One-dimensional-packing-Wire-cutting/wirecutting.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/One-dimensional-packing-Wire-cutting/wirecutting.xlsx -------------------------------------------------------------------------------- /Articles/Optimization-in-Excel-vs-Python/flight_schedule.csv: -------------------------------------------------------------------------------- 1 | ,Activity (Feasible Sequence of Flights),,,,,,, 2 | Requirement (Flight),1,2,3,4,5,6,7,8 3 | New York to Buffalo,1,0,0,1,0,0,1,0 4 | New York to Cincinnati,0,1,0,0,1,0,0,0 5 | New York to Chicago,0,0,1,0,0,1,0,1 6 | Buffalo to Chicago,2,0,0,2,0,0,0,0 7 | Chicago to Cincinnati,0,0,2,3,0,2,0,0 8 | Cincinnati to Pittsburgh,0,2,0,4,0,3,0,0 9 | Cincinnati to Buffalo,0,0,3,0,2,0,0,0 10 | Buffalo to New York,0,0,4,0,3,0,2,0 11 | Pittsburgh to New York,0,3,0,5,0,4,0,0 12 | Chicago to New York,3,0,0,0,0,0,0,2 13 | Cost ($1000) for each sequence,5,4,4,9,7,8,3,3 14 | Hours,396,352,1022,847,687,531,236,179 15 | -------------------------------------------------------------------------------- /Articles/Optimization-in-Excel-vs-Python/readme.md: -------------------------------------------------------------------------------- 1 | ## Optimization in Excel vs Python 2 | There are many tools for implementing and solving optimization models. At Solver Max we specialize in using Solver/OpenSolver in Excel. Another popular tool is the Python programming language, using a library such as CVXPY or PuLP. 3 | 4 | Each tool has advantages and disadvantages, including: 5 | 6 | - Excel is, by far, the most widely-used analysis tool. It provides a visual environment for building models, with many built-in tools for analyzing and visualizing data. Excel is relatively easy to get started with, so many people are familiar with using Excel for modelling, analysis, and presentation of results. Excel also has great depth – much more than most people realize. While Excel's grid structure makes data easy to see, its rigidity can complicate the handling of data that varies in size. Consequently, Excel models may not be as flexible as those created using a programming language. 7 | - Python is a popular programming language. Libraries like CVXPY and PuLP handle much of the complexity of the optimization process (like the Solver and OpenSolver add-ins do in Excel). But learning how to program in Python can involve a steep learning curve, which is quite a barrier to entry. Consequently, far fewer people are familiar with programming in Python. Even if the optimization model developer is comfortable with Python, many model users may prefer a more familiar environment for interacting with a model. 8 | 9 | To compare Excel and Python, we replicate a Python optimization model described in the blog [How to schedule flights in Python](https://towardsdatascience.com/how-to-schedule-flights-in-python-3357b200db9e). 10 | 11 | Blog article: [Optimization in Excel vs Python](https://www.solvermax.com/blog/optimization-in-excel-vs-python) 12 | -------------------------------------------------------------------------------- /Articles/Optimization-in-Excel-vs-Python/scheduleflights.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Optimization-in-Excel-vs-Python/scheduleflights.xlsx -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/data-model-1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "fb79d6af-fe90-4ec7-b077-a3eb57db0eeb", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Load data from Excel file\n", 17 | "def GetData(DataFile, DataWorksheet):\n", 18 | " Width = LoadFromExcel(DataFile, DataWorksheet, 'Width')\n", 19 | " Length = LoadFromExcel(DataFile, DataWorksheet, 'Length')\n", 20 | " Weight = LoadFromExcel(DataFile, DataWorksheet, 'Weight')\n", 21 | " Width.columns = ['Item'] # Input data is for each item\n", 22 | " Length.columns = ['Item']\n", 23 | " Weight.columns = ['Item']\n", 24 | " return Width, Length, Weight" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "id": "c4db48f3-4884-446c-935a-644acba77432", 31 | "metadata": { 32 | "editable": true, 33 | "slideshow": { 34 | "slide_type": "" 35 | }, 36 | "tags": [] 37 | }, 38 | "outputs": [], 39 | "source": [ 40 | "# Define model data, assigning all data to the Model\n", 41 | "def DefineModelData(Model, OrderSize, Width, Length, Weight):\n", 42 | " Model.Item = pyo.Set(initialize = range(0, len(Width)))\n", 43 | " Model.Size = pyo.Set(initialize = range(0, OrderSize))\n", 44 | " Model.Width = pyo.Param(Model.Item, within = pyo.NonNegativeIntegers, mutable = True)\n", 45 | " Model.Length = pyo.Param(Model.Item, within = pyo.NonNegativeIntegers, mutable = True)\n", 46 | " Model.Weight = pyo.Param(Model.Item, within = pyo.NonNegativeReals, mutable = True)\n", 47 | "\n", 48 | " Model.Baseline = 0 # Total weighted area of all items\n", 49 | " for i in Model.Item:\n", 50 | " Model.Width[i] = Width['Item'][i]\n", 51 | " Model.Length[i] = Length['Item'][i]\n", 52 | " Model.Weight[i] = Weight['Item'][i]\n", 53 | " Model.Baseline += Model.Width[i] * Model.Length[i] * Model.Weight[i]" 54 | ] 55 | } 56 | ], 57 | "metadata": { 58 | "kernelspec": { 59 | "display_name": "Python 3 (ipykernel)", 60 | "language": "python", 61 | "name": "python3" 62 | }, 63 | "language_info": { 64 | "codemirror_mode": { 65 | "name": "ipython", 66 | "version": 3 67 | }, 68 | "file_extension": ".py", 69 | "mimetype": "text/x-python", 70 | "name": "python", 71 | "nbconvert_exporter": "python", 72 | "pygments_lexer": "ipython3", 73 | "version": "3.9.13" 74 | } 75 | }, 76 | "nbformat": 4, 77 | "nbformat_minor": 5 78 | } 79 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/data-model-3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "fb79d6af-fe90-4ec7-b077-a3eb57db0eeb", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Load data from Excel file\n", 17 | "def GetData(DataFile, DataWorksheet):\n", 18 | " Width = LoadFromExcel(DataFile, DataWorksheet, 'Width')\n", 19 | " Length = LoadFromExcel(DataFile, DataWorksheet, 'Length')\n", 20 | " Weight = LoadFromExcel(DataFile, DataWorksheet, 'Weight')\n", 21 | " Width.columns = ['Item']\n", 22 | " Length.columns = ['Item']\n", 23 | " Weight.columns = ['Item']\n", 24 | " return Width, Length, Weight" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "id": "c4db48f3-4884-446c-935a-644acba77432", 31 | "metadata": { 32 | "editable": true, 33 | "slideshow": { 34 | "slide_type": "" 35 | }, 36 | "tags": [] 37 | }, 38 | "outputs": [], 39 | "source": [ 40 | "# Define model data, assigning all data to the Model\n", 41 | "def DefineModelData(Model, Width, Length, Weight):\n", 42 | " Model.Item = pyo.Set(initialize = range(0, len(Width)))\n", 43 | " Model.Candidate = pyo.Set(initialize = range(0, len(Width) * len(Width)))\n", 44 | " Model.Width = pyo.Param(Model.Item, within = pyo.NonNegativeIntegers, mutable = True)\n", 45 | " Model.Length = pyo.Param(Model.Item, within = pyo.NonNegativeIntegers, mutable = True)\n", 46 | " Model.Weight = pyo.Param(Model.Item, within = pyo.NonNegativeReals, mutable = True)\n", 47 | " \n", 48 | " Model.Baseline = 0 # Total weighted area of all items\n", 49 | " for i in Model.Item:\n", 50 | " Model.Width[i] = Width['Item'][i]\n", 51 | " Model.Length[i] = Length['Item'][i]\n", 52 | " Model.Weight[i] = Weight['Item'][i]\n", 53 | " Model.Baseline += Model.Width[i] * Model.Length[i] * Model.Weight[i]\n", 54 | " \n", 55 | " # Define candidate product sizes using width and length of items, taking item width and lengths independently to enumerate all combinations\n", 56 | " Model.CandidateWidth = pyo.Param(Model.Candidate, within = pyo.NonNegativeIntegers, mutable = True)\n", 57 | " Model.CandidateLength = pyo.Param(Model.Candidate, within = pyo.NonNegativeIntegers, mutable = True)\n", 58 | " Model.CandidateArea = pyo.Param(Model.Candidate, within = pyo.NonNegativeIntegers, mutable = True)\n", 59 | " for i in Model.Item:\n", 60 | " for j in Model.Item:\n", 61 | " Model.CandidateWidth[i * len(Width) + j] = Width['Item'][i]\n", 62 | " Model.CandidateLength[i * len(Width) + j] = Length['Item'][j]\n", 63 | " Model.CandidateArea[i * len(Width) + j] = Width['Item'][i] * Length['Item'][j]" 64 | ] 65 | } 66 | ], 67 | "metadata": { 68 | "kernelspec": { 69 | "display_name": "Python 3 (ipykernel)", 70 | "language": "python", 71 | "name": "python3" 72 | }, 73 | "language_info": { 74 | "codemirror_mode": { 75 | "name": "ipython", 76 | "version": 3 77 | }, 78 | "file_extension": ".py", 79 | "mimetype": "text/x-python", 80 | "name": "python", 81 | "nbconvert_exporter": "python", 82 | "pygments_lexer": "ipython3", 83 | "version": "3.9.13" 84 | } 85 | }, 86 | "nbformat": 4, 87 | "nbformat_minor": 5 88 | } 89 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/formulation-model-1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "1ab9c709-a751-4942-9f89-622fe70a09bb", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Define model\n", 17 | "def DefineModel(Model):\n", 18 | " Model.SelectWidth = pyo.Var(Model.Item, Model.Size, domain = pyo.Binary, initialize = 0) # For each product size, select one of the item widths\n", 19 | " Model.SelectLength = pyo.Var(Model.Item, Model.Size, domain = pyo.Binary, initialize = 0) # For each product size, select one of the item lengths\n", 20 | " Model.Allocation = pyo.Var(Model.Item, Model.Size, within = pyo.Binary, initialize = 0) # Allocate each item to one of the products\n", 21 | " \n", 22 | " def rule_LBWidth(Model, i): # Width of allocated product must be at least width of each item it is allocated to\n", 23 | " return sum(Model.Allocation[i, s] * sum(Model.Width[i] * Model.SelectWidth[i, s] for i in Model.Item) for s in Model.Size) >= Model.Width[i]\n", 24 | " Model.MinWidth = pyo.Constraint(Model.Item, rule = rule_LBWidth)\n", 25 | "\n", 26 | " def rule_LBLength(Model, i): # Length of allocated product must be at least width of each item it is allocated to\n", 27 | " return sum(Model.Allocation[i, s] * sum(Model.Length[i] * Model.SelectLength[i, s] for i in Model.Item) for s in Model.Size) >= Model.Length[i]\n", 28 | " Model.MinLength = pyo.Constraint(Model.Item, rule = rule_LBLength)\n", 29 | " \n", 30 | " def rule_once(Model, i): # Each item is allocated to exactly one product\n", 31 | " return sum(Model.Allocation[i, s] for s in Model.Size) == 1\n", 32 | " Model.AllocateOnce = pyo.Constraint(Model.Item, rule = rule_once)\n", 33 | "\n", 34 | " def rule_OneW(Model, s): # Each product has exactly one width\n", 35 | " return sum(Model.SelectWidth[i, s] for i in Model.Item) == 1\n", 36 | " Model.SelectW = pyo.Constraint(Model.Size, rule = rule_OneW)\n", 37 | "\n", 38 | " def rule_OneL(Model, s): # Each product has exactly one length\n", 39 | " return sum(Model.SelectLength[i, s] for i in Model.Item) == 1\n", 40 | " Model.SelectL = pyo.Constraint(Model.Size, rule = rule_OneL)\n", 41 | " \n", 42 | " def rule_Obj(Model): # Minimize waste = Area of allocated product minus area of item, in total for all items\n", 43 | " return sum(sum(Model.Allocation[i, s] * (sum(Model.Width[i] * Model.SelectWidth[i, s] for i in Model.Item) \\\n", 44 | " * sum(Model.Length[i] * Model.SelectLength[i, s] for i in Model.Item)) * Model.Weight[i] for s in Model.Size) for i in Model.Item) \\\n", 45 | " - sum(Model.Width[i] * Model.Length[i] * Model.Weight[i] for i in Model.Item)\n", 46 | " Model.Obj = pyo.Objective(rule = rule_Obj, sense = pyo.minimize)" 47 | ] 48 | } 49 | ], 50 | "metadata": { 51 | "kernelspec": { 52 | "display_name": "Python 3 (ipykernel)", 53 | "language": "python", 54 | "name": "python3" 55 | }, 56 | "language_info": { 57 | "codemirror_mode": { 58 | "name": "ipython", 59 | "version": 3 60 | }, 61 | "file_extension": ".py", 62 | "mimetype": "text/x-python", 63 | "name": "python", 64 | "nbconvert_exporter": "python", 65 | "pygments_lexer": "ipython3", 66 | "version": "3.9.13" 67 | } 68 | }, 69 | "nbformat": 4, 70 | "nbformat_minor": 5 71 | } 72 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/formulation-model-2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "1ab9c709-a751-4942-9f89-622fe70a09bb", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Define model\n", 17 | "def DefineModel(Model):\n", 18 | " Model.Select = pyo.Var(Model.Candidate, domain = pyo.Binary)\n", 19 | " Model.Allocation = pyo.Var(Model.Item, Model.Candidate, within = pyo.Binary, initialize = 0)\n", 20 | "\n", 21 | " def rule_LBWidth(Model, i): # Width of allocated product must be at least width of each item it is allocated to\n", 22 | " return sum(Model.Allocation[i, c] * Model.CandidateWidth[c] for c in Model.Candidate) >= Model.Width[i]\n", 23 | " Model.MinWidth = pyo.Constraint(Model.Item, rule = rule_LBWidth)\n", 24 | "\n", 25 | " def rule_LBLength(Model, i): # Length of allocated product must be at least length of each item it is allocated to\n", 26 | " return sum(Model.Allocation[i, c] * Model.CandidateLength[c] for c in Model.Candidate) >= Model.Length[i]\n", 27 | " Model.MinLength = pyo.Constraint(Model.Item, rule = rule_LBLength)\n", 28 | " \n", 29 | " def rule_count(Model): # Select the specified number of products that we want to order\n", 30 | " return sum(Model.Select[c] for c in Model.Candidate) == Model.Orders\n", 31 | " Model.NumOrders = pyo.Constraint(rule = rule_count)\n", 32 | "\n", 33 | " def rule_only(Model, i, c): # Allocate an item to a candidate only if that candidate is selected\n", 34 | " return Model.Allocation[i, c] <= Model.Select[c]\n", 35 | " Model.SelectedOnly = pyo.Constraint(Model.Item, Model.Candidate, rule = rule_only)\n", 36 | " \n", 37 | " def rule_once(Model, i): # Each item is allocated to exactly one product\n", 38 | " return sum(Model.Allocation[i, c] for c in Model.Candidate) == 1\n", 39 | " Model.AllocateOnce = pyo.Constraint(Model.Item, rule = rule_once)\n", 40 | "\n", 41 | " def rule_Obj(Model): # Minimize waste = Area of allocated product minus area of item, in total for all items\n", 42 | " return sum(sum(Model.Allocation[i, c] * Model.CandidateArea[c] * Model.Weight[i] for c in Model.Candidate) for i in Model.Item) \\\n", 43 | " - sum(Model.Width[i] * Model.Length[i] * Model.Weight[i] for i in Model.Item)\n", 44 | " Model.Obj = pyo.Objective(rule = rule_Obj, sense = pyo.minimize)" 45 | ] 46 | } 47 | ], 48 | "metadata": { 49 | "kernelspec": { 50 | "display_name": "Python 3 (ipykernel)", 51 | "language": "python", 52 | "name": "python3" 53 | }, 54 | "language_info": { 55 | "codemirror_mode": { 56 | "name": "ipython", 57 | "version": 3 58 | }, 59 | "file_extension": ".py", 60 | "mimetype": "text/x-python", 61 | "name": "python", 62 | "nbconvert_exporter": "python", 63 | "pygments_lexer": "ipython3", 64 | "version": "3.9.13" 65 | } 66 | }, 67 | "nbformat": 4, 68 | "nbformat_minor": 5 69 | } 70 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/imports.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "7ae23084-887a-4777-a63c-aecfbb663992", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Import dependencies\n", 17 | "import pyomo.environ as pyo\n", 18 | "import pandas as pd\n", 19 | "import numpy as np\n", 20 | "import time as tm\n", 21 | "import os.path\n", 22 | "from openpyxl import load_workbook\n", 23 | "from openpyxl.utils.cell import range_boundaries\n", 24 | "import random as rnd" 25 | ] 26 | } 27 | ], 28 | "metadata": { 29 | "kernelspec": { 30 | "display_name": "Python 3 (ipykernel)", 31 | "language": "python", 32 | "name": "python3" 33 | }, 34 | "language_info": { 35 | "codemirror_mode": { 36 | "name": "ipython", 37 | "version": 3 38 | }, 39 | "file_extension": ".py", 40 | "mimetype": "text/x-python", 41 | "name": "python", 42 | "nbconvert_exporter": "python", 43 | "pygments_lexer": "ipython3", 44 | "version": "3.9.13" 45 | } 46 | }, 47 | "nbformat": 4, 48 | "nbformat_minor": 5 49 | } 50 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/main-model-1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "8a801ff4-b68a-4a6a-bb5b-ed1b7dd89f26", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def Case(OrderSize, Width, Length, Weight):\n", 11 | " Model = pyo.ConcreteModel(name = ModelName + ', Order size ' + str(OrderSize))\n", 12 | " print(Model.name.strip('\\''))\n", 13 | " Model.Engine = SolverName\n", 14 | " Model.TimeLimit = TimeLimit\n", 15 | " Solver, Model = SetUpSolver(Model)\n", 16 | " DefineModelData(Model, OrderSize, Width, Length, Weight)\n", 17 | " DefineModel(Model)\n", 18 | " WriteModelToFile(WriteFile, Model)\n", 19 | " Results = CallSolver(Solver, Model)\n", 20 | " WriteOutput(Model, OrderSize, Results)" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "id": "4cfff072-8d5c-4e76-9072-3887ec111953", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "def Main():\n", 31 | " Timer('Start');\n", 32 | " Width, Length, Weight = GetData(DataFile, DataWorksheet)\n", 33 | " Timer('Setup');\n", 34 | " for OrderSize in range(ProductsMin, ProductsMax + 1): # Run multiple product cases, if required\n", 35 | " Case(OrderSize, Width, Length, Weight)\n", 36 | " Timer('Completed ' + str(OrderSize))\n", 37 | " Timer('Finish')\n", 38 | " WriteCheckpoints()" 39 | ] 40 | } 41 | ], 42 | "metadata": { 43 | "kernelspec": { 44 | "display_name": "Python 3 (ipykernel)", 45 | "language": "python", 46 | "name": "python3" 47 | }, 48 | "language_info": { 49 | "codemirror_mode": { 50 | "name": "ipython", 51 | "version": 3 52 | }, 53 | "file_extension": ".py", 54 | "mimetype": "text/x-python", 55 | "name": "python", 56 | "nbconvert_exporter": "python", 57 | "pygments_lexer": "ipython3", 58 | "version": "3.9.13" 59 | } 60 | }, 61 | "nbformat": 4, 62 | "nbformat_minor": 5 63 | } 64 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/main-model-2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "fc84e37b-de78-4d6d-ad0c-4cc42070136a", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def Case(OrderSize, Width, Length, Weight):\n", 11 | " Model = pyo.ConcreteModel(name = ModelName + ', Order size ' + str(OrderSize))\n", 12 | " print(Model.name.strip('\\''))\n", 13 | " Model.Engine = SolverName\n", 14 | " Model.TimeLimit = TimeLimit\n", 15 | " Solver, Model = SetUpSolver(Model)\n", 16 | " DefineModelData(Model, Width, Length, Weight)\n", 17 | " Model.Orders = OrderSize\n", 18 | " DefineModel(Model)\n", 19 | " WriteModelToFile(WriteFile, Model)\n", 20 | " Results = CallSolver(Solver, Model)\n", 21 | " WriteOutput(Model, OrderSize, Results)" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "id": "cc1e25ad-b407-4c46-8d55-a4ed5c447123", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "def Main():\n", 32 | " Timer('Start');\n", 33 | " Width, Length, Weight = GetData(DataFile, DataWorksheet)\n", 34 | " Timer('Setup');\n", 35 | " for OrderSize in range(ProductsMin, ProductsMax + 1): # Run multiple product cases, if required\n", 36 | " Case(OrderSize, Width, Length, Weight)\n", 37 | " Timer('Solved');\n", 38 | " Timer('Finish');\n", 39 | " WriteCheckpoints()" 40 | ] 41 | } 42 | ], 43 | "metadata": { 44 | "kernelspec": { 45 | "display_name": "Python 3 (ipykernel)", 46 | "language": "python", 47 | "name": "python3" 48 | }, 49 | "language_info": { 50 | "codemirror_mode": { 51 | "name": "ipython", 52 | "version": 3 53 | }, 54 | "file_extension": ".py", 55 | "mimetype": "text/x-python", 56 | "name": "python", 57 | "nbconvert_exporter": "python", 58 | "pygments_lexer": "ipython3", 59 | "version": "3.9.13" 60 | } 61 | }, 62 | "nbformat": 4, 63 | "nbformat_minor": 5 64 | } 65 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/main-model-4.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "59d916b3-bd63-4c3d-b917-c7800fb90cb3", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def Case(OrderSize, Width, Length, Weight):\n", 11 | " Model = pyo.ConcreteModel(name = ModelName + ', Order size ' + str(OrderSize))\n", 12 | " Model.Engine = SolverName\n", 13 | " Model.TimeLimit = TimeLimit\n", 14 | " Solver, Model = SetUpSolver(Model)\n", 15 | " DefineModelData(Model, Width, Length, Weight)\n", 16 | " Model.Orders = OrderSize\n", 17 | " DefineModel(Model)\n", 18 | " WriteModelToFile(WriteFile, Model)\n", 19 | " Results = CallSolver(Solver, Model)\n", 20 | " Obj = pyo.value(Model.Obj())\n", 21 | " Products = ''\n", 22 | " for c in Model.Candidate:\n", 23 | " if np.isclose(pyo.value(Model.Select[c]), 1): # Binary variable = 1, give-or-take small precision error\n", 24 | " Products += str(pyo.value(Model.CandidateWidth[c])).rjust(4) + ' ' + str(pyo.value(Model.CandidateLength[c])).rjust(4) + ' '\n", 25 | " return Obj, Products, pyo.value(Model.Baseline)" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 2, 31 | "id": "774e7f04-2366-4dd9-a7fc-464b3ce02dca", 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "def OuterLoop(Size, BestFound): # Solve each product size multiple times, to increase chance of getting a good solution\n", 36 | " Width, Length, Weight = GetData(DataFile, DataWorksheet)\n", 37 | " for OrderSize in range(Size, Size + 1):\n", 38 | " Result, Products, Baseline = Case(OrderSize, Width, Length, Weight)\n", 39 | " Marker = ' '\n", 40 | " if Result < BestFound:\n", 41 | " BestFound = Result\n", 42 | " Marker = '*' # Mark each solution that is the best so far, for this number of products\n", 43 | " Waste = Result / Baseline\n", 44 | " WriteOutput(OrderSize, Result, Waste, Marker, Products)\n", 45 | " return BestFound" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 3, 51 | "id": "a3c64cba-25a3-414e-bf8b-f4ca567c5362", 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "def Main():\n", 56 | " Timer('Start');\n", 57 | " print(ModelName)\n", 58 | " print('Data file:', DataFile, '\\n')\n", 59 | " print('Products Objective Waste Sizes')\n", 60 | " for s in range(ProductsMin, ProductsMax + 1): # Run multiple product cases, if required\n", 61 | " BestFound = np.inf\n", 62 | " for i in range(0, Iterations):\n", 63 | " BestFound = OuterLoop(s, BestFound)\n", 64 | " print() # Blank line between each set of product iterations\n", 65 | " Timer('Completed ' + str(s));\n", 66 | " Timer('Finish');\n", 67 | " WriteCheckpoints()" 68 | ] 69 | } 70 | ], 71 | "metadata": { 72 | "kernelspec": { 73 | "display_name": "Python 3 (ipykernel)", 74 | "language": "python", 75 | "name": "python3" 76 | }, 77 | "language_info": { 78 | "codemirror_mode": { 79 | "name": "ipython", 80 | "version": 3 81 | }, 82 | "file_extension": ".py", 83 | "mimetype": "text/x-python", 84 | "name": "python", 85 | "nbconvert_exporter": "python", 86 | "pygments_lexer": "ipython3", 87 | "version": "3.9.13" 88 | } 89 | }, 90 | "nbformat": 4, 91 | "nbformat_minor": 5 92 | } 93 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/output-model-1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "e7c51632-972c-4475-9775-cf3e6cd62d62", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def WriteOutput(Model, OrderSize, Results):\n", 11 | " Obj = pyo.value(Model.Obj())\n", 12 | " Products = '['\n", 13 | " for s in Model.Size: # Collate list of selected product sizes\n", 14 | " Width = str(sum(pyo.value(Model.Width[i]) * round(pyo.value(Model.SelectWidth[i, s]), 0) for i in Model.Item))\n", 15 | " Length = str(sum(pyo.value(Model.Length[i]) * round(pyo.value(Model.SelectLength[i, s]), 0) for i in Model.Item))\n", 16 | " Products += Width.rjust(6) + ' ' + Length.rjust(6) + ' ' \n", 17 | " Products += ']'\n", 18 | " print()\n", 19 | " print(f'Order size: {OrderSize:<,.0f}')\n", 20 | " print(f'Objective: {Obj:<,.0f} ({Obj / pyo.value(Model.Baseline):.1%} of baseline)')\n", 21 | " print(f'Products: {Products}\\n')" 22 | ] 23 | } 24 | ], 25 | "metadata": { 26 | "kernelspec": { 27 | "display_name": "Python 3 (ipykernel)", 28 | "language": "python", 29 | "name": "python3" 30 | }, 31 | "language_info": { 32 | "codemirror_mode": { 33 | "name": "ipython", 34 | "version": 3 35 | }, 36 | "file_extension": ".py", 37 | "mimetype": "text/x-python", 38 | "name": "python", 39 | "nbconvert_exporter": "python", 40 | "pygments_lexer": "ipython3", 41 | "version": "3.9.13" 42 | } 43 | }, 44 | "nbformat": 4, 45 | "nbformat_minor": 5 46 | } 47 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/output-model-2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "e7c51632-972c-4475-9775-cf3e6cd62d62", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def WriteOutput(Model, OrderSize, Results):\n", 11 | " Obj = pyo.value(Model.Obj())\n", 12 | " Products = '['\n", 13 | " for c in Model.Candidate: # Collate list of selected product sizes\n", 14 | " if np.isclose(pyo.value(Model.Select[c]), 1): # Binary variable = 1, give-or-take small precision error\n", 15 | " Products += str(pyo.value(Model.CandidateWidth[c])).rjust(6) + ' ' + str(pyo.value(Model.CandidateLength[c])).rjust(6) + ' '\n", 16 | " Products += ']'\n", 17 | " print()\n", 18 | " print(f'Order size: {OrderSize:<,.0f}')\n", 19 | " print(f'Objective: {Obj:<,.0f} ({Obj / pyo.value(Model.Baseline):.1%} of baseline)')\n", 20 | " print(f'Products: {Products}\\n')\n", 21 | "\n", 22 | " pd.options.display.float_format = '{:,.0f}'.format\n", 23 | " ItemsAllocated = pd.DataFrame()\n", 24 | " \n", 25 | " SelectedSizes = [] # Dataframe of solution\n", 26 | " for c in Model.Select:\n", 27 | " if np.isclose(pyo.value(Model.Select[c]), 1):\n", 28 | " SelectedSizes.append(str(pyo.value(Model.CandidateWidth[c])) + 'x' + str(pyo.value(Model.CandidateLength[c])))\n", 29 | " for i in Model.Item:\n", 30 | " ItemsAllocated.at[i, c] = pyo.value(Model.Allocation[i, c])\n", 31 | " ItemsAllocated.columns = SelectedSizes\n", 32 | "\n", 33 | " ItemSizes = [] # Add item sizes to solution dataframe\n", 34 | " for i in Model.Item:\n", 35 | " ItemSizes.append(str(pyo.value(Model.Width[i])) + 'x' + str(pyo.value(Model.Length[i])))\n", 36 | " ItemsAllocated['Item'] = ItemSizes\n", 37 | " pd.set_option('display.max_rows', None)\n", 38 | " display(ItemsAllocated)" 39 | ] 40 | } 41 | ], 42 | "metadata": { 43 | "kernelspec": { 44 | "display_name": "Python 3 (ipykernel)", 45 | "language": "python", 46 | "name": "python3" 47 | }, 48 | "language_info": { 49 | "codemirror_mode": { 50 | "name": "ipython", 51 | "version": 3 52 | }, 53 | "file_extension": ".py", 54 | "mimetype": "text/x-python", 55 | "name": "python", 56 | "nbconvert_exporter": "python", 57 | "pygments_lexer": "ipython3", 58 | "version": "3.9.13" 59 | } 60 | }, 61 | "nbformat": 4, 62 | "nbformat_minor": 5 63 | } 64 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/output-model-4.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "e7c51632-972c-4475-9775-cf3e6cd62d62", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def WriteOutput(OrderSize, Result, Waste, Marker, Products):\n", 11 | " print(f'{OrderSize:4,.0f} {Result:<10,.0f} {Waste:>7.2%} {Marker} {Products}')" 12 | ] 13 | } 14 | ], 15 | "metadata": { 16 | "kernelspec": { 17 | "display_name": "Python 3 (ipykernel)", 18 | "language": "python", 19 | "name": "python3" 20 | }, 21 | "language_info": { 22 | "codemirror_mode": { 23 | "name": "ipython", 24 | "version": 3 25 | }, 26 | "file_extension": ".py", 27 | "mimetype": "text/x-python", 28 | "name": "python", 29 | "nbconvert_exporter": "python", 30 | "pygments_lexer": "ipython3", 31 | "version": "3.9.13" 32 | } 33 | }, 34 | "nbformat": 4, 35 | "nbformat_minor": 5 36 | } 37 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/output-model-5c.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "e7c51632-972c-4475-9775-cf3e6cd62d62", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def WriteOutput(Model, OrderSize, Results):\n", 11 | " Obj = pyo.value(Model.Obj())\n", 12 | " Products = '['\n", 13 | " for c in Model.Candidate: # Collate list of selected product sizes\n", 14 | " if np.isclose(pyo.value(Model.Select[c]), 1): # Binary variable = 1, give-or-take small precision error\n", 15 | " Products += str(pyo.value(Model.CandidateWidth[c])).rjust(6) + ' ' + str(pyo.value(Model.CandidateLength[c])).rjust(6) + ' '\n", 16 | " Products += ']'\n", 17 | " print()\n", 18 | " print(f'Order size: {OrderSize:<,.0f}')\n", 19 | " print(f'Objective: {Obj:<,.0f} ({Obj / pyo.value(Model.Baseline):.2%} of baseline)')\n", 20 | " print(f'Products: {Products}\\n')\n", 21 | "\n", 22 | " pd.options.display.float_format = '{:,.0f}'.format\n", 23 | " ItemsAllocated = pd.DataFrame()\n", 24 | " \n", 25 | " SelectedSizes = [] # Dataframe of solution\n", 26 | " for c in Model.Select:\n", 27 | " if np.isclose(pyo.value(Model.Select[c]), 1):\n", 28 | " SelectedSizes.append(str(pyo.value(Model.CandidateWidth[c])) + 'x' + str(pyo.value(Model.CandidateLength[c])))\n", 29 | " for i in Model.Item:\n", 30 | " ItemsAllocated.at[i, c] = pyo.value(Model.Allocation[i, c])\n", 31 | " ItemsAllocated.columns = SelectedSizes\n", 32 | "\n", 33 | " ItemSizes = [] # Add item sizes to solution dataframe\n", 34 | " for i in Model.Item:\n", 35 | " ItemSizes.append(str(pyo.value(Model.Width[i])) + 'x' + str(pyo.value(Model.Length[i])))\n", 36 | " ItemsAllocated['Item'] = ItemSizes\n", 37 | " pd.set_option('display.max_rows', None)\n", 38 | " display(ItemsAllocated)\n", 39 | "\n", 40 | " for i in Model.Item:\n", 41 | " print(pyo.value(Model.portrait[i].binary_indicator_var), pyo.value(Model.landscape[i].binary_indicator_var))" 42 | ] 43 | } 44 | ], 45 | "metadata": { 46 | "kernelspec": { 47 | "display_name": "Python 3 (ipykernel)", 48 | "language": "python", 49 | "name": "python3" 50 | }, 51 | "language_info": { 52 | "codemirror_mode": { 53 | "name": "ipython", 54 | "version": 3 55 | }, 56 | "file_extension": ".py", 57 | "mimetype": "text/x-python", 58 | "name": "python", 59 | "nbconvert_exporter": "python", 60 | "pygments_lexer": "ipython3", 61 | "version": "3.9.13" 62 | } 63 | }, 64 | "nbformat": 4, 65 | "nbformat_minor": 5 66 | } 67 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/solver.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "526984e9-cf4e-4916-90ab-bcebdc58af50", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Create the Solver object for either NEOS or a local solver\n", 17 | "def SetUpSolver(Model):\n", 18 | " Model.Options = None\n", 19 | " if Neos:\n", 20 | " Solver = pyo.SolverManagerFactory('neos') # Solver on NEOS\n", 21 | " if pyo.value(Model.Engine) == 'cplex': # Linear\n", 22 | " Model.Options = {'timelimit': Model.TimeLimit}\n", 23 | " elif pyo.value(Model.Engine) == 'octeract': # Linear or non-linear\n", 24 | " Model.Options = {'MAX_SOLVER_TIME': Model.TimeLimit, 'MILP_SOLVER': 'HIGHS'}\n", 25 | " elif pyo.value(Model.Engine) == 'couenne': # Non-linear\n", 26 | " print('No options for Couenne')\n", 27 | " else:\n", 28 | " print('Unknown NEOS solver when setting options')\n", 29 | " else:\n", 30 | " Solver = pyo.SolverFactory(pyo.value(Model.Engine)) # Local solver installed\n", 31 | " if pyo.value(Model.Engine) == 'couenne': # Non-linear\n", 32 | " print('No options for Couenne') # Couenne doesn't accept command line options, use couenne.opt instead\n", 33 | " elif pyo.value(Model.Engine) == 'appsi_highs': # Linear\n", 34 | " Solver.options['time_limit'] = pyo.value(Model.TimeLimit)\n", 35 | " else:\n", 36 | " print('Unknown local solver when setting options')\n", 37 | " \n", 38 | " return Solver, Model" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 2, 44 | "id": "f6d847fb-073f-446b-9bd8-085117c49067", 45 | "metadata": { 46 | "editable": true, 47 | "slideshow": { 48 | "slide_type": "" 49 | }, 50 | "tags": [] 51 | }, 52 | "outputs": [], 53 | "source": [ 54 | "# Call either NEOS or a local solver\n", 55 | "def CallSolver(Solver, Model):\n", 56 | " if Neos:\n", 57 | " if Model.Options == None:\n", 58 | " Results = Solver.solve(Model, load_solutions = LoadSolution, tee = Verbose, solver = Model.Engine)\n", 59 | " else:\n", 60 | " Results = Solver.solve(Model, load_solutions = LoadSolution, tee = Verbose, solver = Model.Engine, options = Model.Options)\n", 61 | " else:\n", 62 | " Results = Solver.solve(Model, load_solutions = LoadSolution, tee = Verbose)\n", 63 | " \n", 64 | " return Results, Model" 65 | ] 66 | } 67 | ], 68 | "metadata": { 69 | "kernelspec": { 70 | "display_name": "Python 3 (ipykernel)", 71 | "language": "python", 72 | "name": "python3" 73 | }, 74 | "language_info": { 75 | "codemirror_mode": { 76 | "name": "ipython", 77 | "version": 3 78 | }, 79 | "file_extension": ".py", 80 | "mimetype": "text/x-python", 81 | "name": "python", 82 | "nbconvert_exporter": "python", 83 | "pygments_lexer": "ipython3", 84 | "version": "3.9.13" 85 | } 86 | }, 87 | "nbformat": 4, 88 | "nbformat_minor": 5 89 | } 90 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/components/utilities.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "ab1d11b1-6b18-4141-a7d2-b94bbbc54051", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Record time checkpoints\n", 17 | "# Requires global variable: Checkpoints = []\n", 18 | "def Timer(Point): # String label for current checkpoint\n", 19 | " Checkpoints.append([Point, tm.perf_counter()])" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 2, 25 | "id": "700fcc70-15c8-458d-b897-687db1cbf9c2", 26 | "metadata": { 27 | "editable": true, 28 | "slideshow": { 29 | "slide_type": "" 30 | }, 31 | "tags": [] 32 | }, 33 | "outputs": [], 34 | "source": [ 35 | "# Output list of checkpoint labels and times\n", 36 | "def WriteCheckpoints():\n", 37 | " print('\\nCheckpoint Seconds')\n", 38 | " print('---------------------')\n", 39 | " Start = Checkpoints[0][1]\n", 40 | " for i in range(1, len(Checkpoints)):\n", 41 | " Point = Checkpoints[i][0]\n", 42 | " TimeStep = Checkpoints[i][1] - Start\n", 43 | " print(f'{Point:12}{TimeStep:9,.1f}')" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 3, 49 | "id": "1f6a82cc-7ac3-4446-bd7d-1d1ad86eadc4", 50 | "metadata": { 51 | "editable": true, 52 | "slideshow": { 53 | "slide_type": "" 54 | }, 55 | "tags": [] 56 | }, 57 | "outputs": [], 58 | "source": [ 59 | "# Generic loader from Excel file, given worksheet and named range\n", 60 | "def LoadFromExcel(ExcelFile, Worksheet, Range):\n", 61 | " wb = load_workbook(filename=ExcelFile, read_only = True)\n", 62 | " ws = wb[Worksheet]\n", 63 | " dests = wb.defined_names[Range].destinations\n", 64 | " for title, coord in dests:\n", 65 | " min_col, min_row, max_col, max_row = range_boundaries(coord)\n", 66 | " data = ws.iter_rows(min_row, max_row, min_col, max_col, values_only = True)\n", 67 | " df = pd.DataFrame(data)\n", 68 | " return df" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "id": "6cc58099-1915-4cf4-92aa-7b096fce4a36", 75 | "metadata": { 76 | "editable": true, 77 | "slideshow": { 78 | "slide_type": "" 79 | }, 80 | "tags": [] 81 | }, 82 | "outputs": [], 83 | "source": [ 84 | "# Write model to file, if required. The format will be inferred by Pyomo from the file extension, e.g. .gams or .nl\n", 85 | "def WriteModelToFile(WriteFile, Model):\n", 86 | " if WriteFile:\n", 87 | " Model.write(ModelFile, io_options={'symbolic_solver_labels': False}) # symbolic_solver_labels of True is easier to read, but a longer file" 88 | ] 89 | } 90 | ], 91 | "metadata": { 92 | "kernelspec": { 93 | "display_name": "Python 3 (ipykernel)", 94 | "language": "python", 95 | "name": "python3" 96 | }, 97 | "language_info": { 98 | "codemirror_mode": { 99 | "name": "ipython", 100 | "version": 3 101 | }, 102 | "file_extension": ".py", 103 | "mimetype": "text/x-python", 104 | "name": "python", 105 | "nbconvert_exporter": "python", 106 | "pygments_lexer": "ipython3", 107 | "version": "3.9.13" 108 | } 109 | }, 110 | "nbformat": 4, 111 | "nbformat_minor": 5 112 | } 113 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/data/data-100-actual-sorted.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Paper-coverage/data/data-100-actual-sorted.xlsx -------------------------------------------------------------------------------- /Articles/Paper-coverage/data/data-100-actual-unsorted.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Paper-coverage/data/data-100-actual-unsorted.xlsx -------------------------------------------------------------------------------- /Articles/Paper-coverage/data/data-1000-actual-extended-sorted.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Paper-coverage/data/data-1000-actual-extended-sorted.xlsx -------------------------------------------------------------------------------- /Articles/Paper-coverage/data/data-20-sorted.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Paper-coverage/data/data-20-sorted.xlsx -------------------------------------------------------------------------------- /Articles/Paper-coverage/data/data-20-unsorted.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Paper-coverage/data/data-20-unsorted.xlsx -------------------------------------------------------------------------------- /Articles/Paper-coverage/data/data-200-actual-extended-sorted.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Paper-coverage/data/data-200-actual-extended-sorted.xlsx -------------------------------------------------------------------------------- /Articles/Paper-coverage/data/data-5-example-sorted.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Paper-coverage/data/data-5-example-sorted.xlsx -------------------------------------------------------------------------------- /Articles/Paper-coverage/paper-coverage-model-4-column-generation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "498043e7-560b-46f5-be2a-656b720984e2", 6 | "metadata": {}, 7 | "source": [ 8 | "# Paper coverage, Model 4\n", 9 | "\n", 10 | "Variation of Model 2, with a baseline set of sizes plus random candidates.\n", 11 | "\n", 12 | "This model is suitable for larger sets of items, where Model 3 would have too many variables to solve to optimality.\n", 13 | "\n", 14 | "Given enough time, this model will find near optimal solutions - probably within 1%." 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "id": "7c789501-5273-446c-a69c-3f0d48e2eed0", 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "%run ./components/imports.ipynb\n", 25 | "%run ./components/utilities.ipynb\n", 26 | "%run ./components/solver.ipynb\n", 27 | "%run ./components/data-model-4.ipynb\n", 28 | "%run ./components/formulation-model-2.ipynb # Same formulation as Model 2\n", 29 | "%run ./components/output-model-4.ipynb\n", 30 | "%run ./components/main-model-4.ipynb" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "id": "6e2292d2-da8e-466a-8d61-55412c78c818", 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "# Globals\n", 41 | "\n", 42 | "# Data assumptions\n", 43 | "ProductsMin = 2 # >= 1\n", 44 | "ProductsMax = 10 # <= number of items\n", 45 | "DataFile = os.path.join(os.getcwd() + '\\data', 'data-200-actual-extended-sorted.xlsx')\n", 46 | "DataWorksheet = 'Data'\n", 47 | "\n", 48 | "# Run options\n", 49 | "Verbose = False\n", 50 | "LoadSolution = True\n", 51 | "TimeLimit = 3600 # seconds\n", 52 | "Iterations = 10\n", 53 | "ExtraCandidates = 800\n", 54 | "\n", 55 | "# Solver options\n", 56 | "Neos = False\n", 57 | "SolverName = 'appsi_highs'\n", 58 | "os.environ['NEOS_EMAIL'] = 'your-email@company.com'\n", 59 | "\n", 60 | "# Model file\n", 61 | "WriteFile = False\n", 62 | "ModelFile = 'model-4.gams'\n", 63 | "\n", 64 | "# Fixed\n", 65 | "ModelName = 'Paper coverage - Model 4'\n", 66 | "Checkpoints = [] # List of time checkpoints" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "id": "c680e77d-c394-4d5b-9507-66c418a6a54a", 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "Main()" 77 | ] 78 | } 79 | ], 80 | "metadata": { 81 | "kernelspec": { 82 | "display_name": "Python 3 (ipykernel)", 83 | "language": "python", 84 | "name": "python3" 85 | }, 86 | "language_info": { 87 | "codemirror_mode": { 88 | "name": "ipython", 89 | "version": 3 90 | }, 91 | "file_extension": ".py", 92 | "mimetype": "text/x-python", 93 | "name": "python", 94 | "nbconvert_exporter": "python", 95 | "pygments_lexer": "ipython3", 96 | "version": "3.9.13" 97 | } 98 | }, 99 | "nbformat": 4, 100 | "nbformat_minor": 5 101 | } 102 | -------------------------------------------------------------------------------- /Articles/Paper-coverage/readme.md: -------------------------------------------------------------------------------- 1 | ## Optimal but not practical 2 | We address a common modelling issue: The solution is optimal, but not practical. 3 | 4 | In this series of articles, we use a variety of techniques to create several models. Along the way, we address the trade-off between our quantitative and qualitative objectives, enabling the decision maker to make an informed decision to implement an efficient and practical solution. 5 | 6 | The models are built in Python using the Pyomo optimization library. We use local solvers and solvers on NEOS Server to solve the models 7 | 8 | Blog article: https://www.solvermax.com/blog/optimal-but-not-practical-first-attempt 9 | -------------------------------------------------------------------------------- /Articles/Pivot-irrigation/readme.md: -------------------------------------------------------------------------------- 1 | ## Pivot irrigators in a 100 acre field 2 | In this article, we explore a model for optimizing the layout of centre-pivot irrigation machines in a field. Our goal is to see if replacement of the existing worn-out machines would make purchase of a field a viable investment. 3 | 4 | Arranging pivot irrigators in a field is a type of circle packing problem, with a non-linear objective and non-linear constraints. Worse still, the model is non-convex. These characteristics make our optimization problem both difficult and interesting. 5 | 6 | To solve our model, we try several free and commerical solvers. All fail to find good solutions, except in trivial cases. So we try another solver, MINLP-BB via NEOS Server, that finds locally optimal solutions with no guarantee of global optimality. But by using a multi-start technique, we improve our chances of finding a good, perhaps optimal, solution. Is that solution good enough to justify making an investment? 7 | 8 | Note that the model uses the NEOS Server service. You will need to change the EMAIL constant to be your email address. 9 | 10 | Blog article: [Pivot irrigators in a 100 acre field](https://www.solvermax.com/blog/pivot-irrigators-in-a-100-acre-field) 11 | -------------------------------------------------------------------------------- /Articles/Potatoes/data/data_scenario_1.py: -------------------------------------------------------------------------------- 1 | # Network data for potato LMP model 2 | 3 | def case_name(): # Return name of this case 4 | return 'Expand transport capacity' 5 | 6 | def get_node_data(): # Data for each node 7 | return { 8 | # node_id: (max_supply, demand, supply_cost, [connected_node_ids]) 9 | 1: (100000, 0, 0.80, [2, 3]), 10 | 2: (60000, 40000, 1.05, [1, 4, 5]), 11 | 3: (25000, 30000, 0.90, [1, 4]), 12 | 4: (0, 20000, 1.10, [2, 3, 5, 6]), 13 | 5: (0, 25000, 1.12, [2, 6]), 14 | 6: (0, 50000, 1.15, [4, 5]) 15 | } 16 | 17 | def get_connection_data(): # Data for each edge 18 | return { 19 | # (from, to): (capacity, distance, cost, fixed loss, variable loss) 20 | (1, 2): (80000, 150, 0.0010, 0.011*0, 0.00030), 21 | (1, 3): (40000, 200, 0.0015, 0.010*0, 0.00035), 22 | (2, 4): (100000, 95, 0.0024, 0.020*0, 0.00032), 23 | (2, 5): (40000, 100, 0.0012, 0.014*0, 0.00015), 24 | (3, 4): (90000, 120, 0.0012, 0.010*0, 0.00020), 25 | (4, 5): (20000, 50, 0.0025, 0.010*0, 0.00022), 26 | (4, 6): (100000, 75, 0.0008, 0.012*0, 0.00018), 27 | (5, 6): (60000, 60, 0.0011, 0.010*0, 0.00031) 28 | } 29 | 30 | def get_node_positions(): # Position of each node 31 | return { 32 | 1: (0.2, 0.8), 33 | 2: (0.5, 0.8), 34 | 3: (0.2, 0.5), 35 | 4: (0.5, 0.5), 36 | 5: (0.8, 0.5), 37 | 6: (0.8, 0.2) 38 | } 39 | 40 | def get_plot_data(): 41 | node_label_offsets = { # Position labels for each node 42 | 1: (-0.13, +0.00), 43 | 2: (+0.05, +0.00), 44 | 3: (-0.13, +0.00), 45 | 4: (-0.11, -0.06), 46 | 5: (+0.04, +0.00), 47 | 6: (+0.04, +0.00) 48 | } 49 | 50 | edge_label_offsets = { # Position labels for each edge. State both directions, in case want to be non-symmetric 51 | (1, 2): (-0.05, -0.05), 52 | (2, 1): (-0.05, -0.05), 53 | (1, 3): (-0.10, +0.00), 54 | (3, 1): (-0.10, +0.00), 55 | (2, 4): (-0.10, +0.00), 56 | (4, 2): (-0.10, +0.00), 57 | (3, 4): (-0.05, +0.05), 58 | (4, 3): (-0.05, +0.05), 59 | (2, 5): (+0.01, +0.04), 60 | (5, 2): (+0.01, +0.04), 61 | (4, 5): (-0.05, +0.05), 62 | (5, 4): (-0.05, +0.05), 63 | (4, 6): (+0.01, +0.04), 64 | (6, 4): (+0.01, +0.04), 65 | (5, 6): (+0.02, +0.00), 66 | (6, 5): (+0.02, +0.00), 67 | } 68 | return node_label_offsets, edge_label_offsets -------------------------------------------------------------------------------- /Articles/Potatoes/data/data_scenario_2.py: -------------------------------------------------------------------------------- 1 | # Network data for potato LMP model 2 | 3 | def case_name(): # Return name of this case 4 | return 'Local supply at every node' 5 | 6 | def get_node_data(): # Data for each node 7 | return { 8 | # node_id: (max_supply, demand, supply_cost, [connected_node_ids]) 9 | 1: (0, 0, 0.80, [2, 3]), 10 | 2: (40000, 40000, 1.05, [1, 4, 5]), 11 | 3: (30000, 30000, 0.90, [1, 4]), 12 | 4: (20000, 20000, 1.10, [2, 3, 5, 6]), 13 | 5: (25000, 25000, 1.12, [2, 6]), 14 | 6: (50000, 50000, 1.15, [4, 5]) 15 | } 16 | 17 | def get_connection_data(): # Data for each edge 18 | return { 19 | # (from, to): (capacity, distance, cost, fixed loss, variable loss) 20 | (1, 2): (80000, 150, 0.0010, 0.011*0, 0.00030), 21 | (1, 3): (40000, 200, 0.0015, 0.010*0, 0.00035), 22 | (2, 4): (50000, 95, 0.0024, 0.020*0, 0.00032), 23 | (2, 5): (20000, 100, 0.0012, 0.014*0, 0.00015), 24 | (3, 4): (90000, 120, 0.0012, 0.010*0, 0.00020), 25 | (4, 5): (20000, 50, 0.0025, 0.010*0, 0.00022), 26 | (4, 6): (50000, 75, 0.0008, 0.012*0, 0.00018), 27 | (5, 6): (60000, 60, 0.0011, 0.010*0, 0.00031) 28 | } 29 | 30 | def get_node_positions(): # Position of each node 31 | return { 32 | 1: (0.2, 0.8), 33 | 2: (0.5, 0.8), 34 | 3: (0.2, 0.5), 35 | 4: (0.5, 0.5), 36 | 5: (0.8, 0.5), 37 | 6: (0.8, 0.2) 38 | } 39 | 40 | def get_plot_data(): 41 | node_label_offsets = { # Position labels for each node 42 | 1: (-0.13, +0.00), 43 | 2: (+0.05, +0.00), 44 | 3: (-0.13, +0.00), 45 | 4: (-0.11, -0.06), 46 | 5: (+0.04, +0.00), 47 | 6: (+0.04, +0.00) 48 | } 49 | 50 | edge_label_offsets = { # Position labels for each edge. State both directions, in case want to be non-symmetric 51 | (1, 2): (-0.05, -0.05), 52 | (2, 1): (-0.05, -0.05), 53 | (1, 3): (-0.10, +0.00), 54 | (3, 1): (-0.10, +0.00), 55 | (2, 4): (-0.10, +0.00), 56 | (4, 2): (-0.10, +0.00), 57 | (3, 4): (-0.05, +0.05), 58 | (4, 3): (-0.05, +0.05), 59 | (2, 5): (+0.01, +0.04), 60 | (5, 2): (+0.01, +0.04), 61 | (4, 5): (-0.05, +0.05), 62 | (5, 4): (-0.05, +0.05), 63 | (4, 6): (+0.01, +0.04), 64 | (6, 4): (+0.01, +0.04), 65 | (5, 6): (+0.02, +0.00), 66 | (6, 5): (+0.02, +0.00), 67 | } 68 | return node_label_offsets, edge_label_offsets -------------------------------------------------------------------------------- /Articles/Potatoes/data/data_scenario_3.py: -------------------------------------------------------------------------------- 1 | # Network data for potato LMP model 2 | 3 | def case_name(): # Return name of this case 4 | return 'Increase supply at Node 6' 5 | 6 | def get_node_data(): # Data for each node 7 | return { 8 | # node_id: (max_supply, demand, supply_cost, [connected_node_ids]) 9 | 1: (100000, 0, 0.80, [2, 3]), 10 | 2: (60000, 40000, 1.05, [1, 4, 5]), 11 | 3: (25000, 30000, 0.90, [1, 4]), 12 | 4: (0, 20000, 1.10, [2, 3, 5, 6]), 13 | 5: (0, 25000, 1.12, [2, 6]), 14 | 6: (60000, 50000, 1.15, [4, 5]) 15 | } 16 | 17 | def get_connection_data(): # Data for each edge 18 | return { 19 | # (from, to): (capacity, distance, cost, fixed loss, variable loss) 20 | (1, 2): (80000, 150, 0.0010, 0.011*0, 0.00030), 21 | (1, 3): (40000, 200, 0.0015, 0.010*0, 0.00035), 22 | (2, 4): (50000, 95, 0.0024, 0.020*0, 0.00032), 23 | (2, 5): (20000, 100, 0.0012, 0.014*0, 0.00015), 24 | (3, 4): (90000, 120, 0.0012, 0.010*0, 0.00020), 25 | (4, 5): (20000, 50, 0.0025, 0.010*0, 0.00022), 26 | (4, 6): (50000, 75, 0.0008, 0.012*0, 0.00018), 27 | (5, 6): (60000, 60, 0.0011, 0.010*0, 0.00031) 28 | } 29 | 30 | def get_node_positions(): # Position of each node 31 | return { 32 | 1: (0.2, 0.8), 33 | 2: (0.5, 0.8), 34 | 3: (0.2, 0.5), 35 | 4: (0.5, 0.5), 36 | 5: (0.8, 0.5), 37 | 6: (0.8, 0.2) 38 | } 39 | 40 | def get_plot_data(): 41 | node_label_offsets = { # Position labels for each node 42 | 1: (-0.13, +0.00), 43 | 2: (+0.05, +0.00), 44 | 3: (-0.13, +0.00), 45 | 4: (-0.11, -0.06), 46 | 5: (+0.04, +0.00), 47 | 6: (+0.04, +0.00) 48 | } 49 | 50 | edge_label_offsets = { # Position labels for each edge. State both directions, in case want to be non-symmetric 51 | (1, 2): (-0.05, -0.05), 52 | (2, 1): (-0.05, -0.05), 53 | (1, 3): (-0.10, +0.00), 54 | (3, 1): (-0.10, +0.00), 55 | (2, 4): (-0.10, +0.00), 56 | (4, 2): (-0.10, +0.00), 57 | (3, 4): (-0.05, +0.05), 58 | (4, 3): (-0.05, +0.05), 59 | (2, 5): (+0.01, +0.04), 60 | (5, 2): (+0.01, +0.04), 61 | (4, 5): (-0.05, +0.05), 62 | (5, 4): (-0.05, +0.05), 63 | (4, 6): (+0.01, +0.04), 64 | (6, 4): (+0.01, +0.04), 65 | (5, 6): (+0.02, +0.00), 66 | (6, 5): (+0.02, +0.00), 67 | } 68 | return node_label_offsets, edge_label_offsets -------------------------------------------------------------------------------- /Articles/Potatoes/data/data_scenario_4.py: -------------------------------------------------------------------------------- 1 | # Network data for potato LMP model 2 | 3 | def case_name(): # Return name of this case 4 | return 'Minimum cost' 5 | 6 | def get_node_data(): # Data for each node 7 | return { 8 | # node_id: (max_supply, demand, supply_cost, [connected_node_ids]) 9 | 1: (100000, 0, 0.80, [2, 3]), 10 | 2: (60000, 40000, 1.05, [1, 4, 5]), 11 | 3: (110000, 30000, 0.90, [1, 4]), 12 | 4: (90000, 20000, 1.10, [2, 3, 5, 6]), 13 | 5: (90000, 25000, 1.12, [2, 6]), 14 | 6: (90000, 50000, 1.15, [4, 5]) 15 | } 16 | 17 | def get_connection_data(): # Data for each edge 18 | return { 19 | # (from, to): (capacity, distance, cost, fixed loss, variable loss) 20 | (1, 2): (80000, 150, 0.0010, 0.011*0, 0.00030), 21 | (1, 3): (40000, 200, 0.0015, 0.010*0, 0.00035), 22 | (2, 4): (50000, 95, 0.0024, 0.020*0, 0.00032), 23 | (2, 5): (20000, 100, 0.0012, 0.014*0, 0.00015), 24 | (3, 4): (90000, 120, 0.0012, 0.010*0, 0.00020), 25 | (4, 5): (20000, 50, 0.0025, 0.010*0, 0.00022), 26 | (4, 6): (60000, 75, 0.0008, 0.012*0, 0.00018), 27 | (5, 6): (60000, 60, 0.0011, 0.010*0, 0.00031) 28 | } 29 | 30 | def get_node_positions(): # Position of each node 31 | return { 32 | 1: (0.2, 0.8), 33 | 2: (0.5, 0.8), 34 | 3: (0.2, 0.5), 35 | 4: (0.5, 0.5), 36 | 5: (0.8, 0.5), 37 | 6: (0.8, 0.2) 38 | } 39 | 40 | def get_plot_data(): 41 | node_label_offsets = { # Position labels for each node 42 | 1: (-0.13, +0.00), 43 | 2: (+0.05, +0.00), 44 | 3: (-0.13, +0.00), 45 | 4: (-0.11, -0.06), 46 | 5: (+0.04, +0.00), 47 | 6: (+0.04, +0.00) 48 | } 49 | 50 | edge_label_offsets = { # Position labels for each edge. State both directions, in case want to be non-symmetric 51 | (1, 2): (-0.05, -0.05), 52 | (2, 1): (-0.05, -0.05), 53 | (1, 3): (-0.10, +0.00), 54 | (3, 1): (-0.10, +0.00), 55 | (2, 4): (-0.10, +0.00), 56 | (4, 2): (-0.10, +0.00), 57 | (3, 4): (-0.05, +0.05), 58 | (4, 3): (-0.05, +0.05), 59 | (2, 5): (+0.01, +0.04), 60 | (5, 2): (+0.01, +0.04), 61 | (4, 5): (-0.05, +0.05), 62 | (5, 4): (-0.05, +0.05), 63 | (4, 6): (+0.01, +0.04), 64 | (6, 4): (+0.01, +0.04), 65 | (5, 6): (+0.02, +0.00), 66 | (6, 5): (+0.02, +0.00), 67 | } 68 | return node_label_offsets, edge_label_offsets -------------------------------------------------------------------------------- /Articles/Potatoes/data/data_scenario_current.py: -------------------------------------------------------------------------------- 1 | # Network data for potato LMP model 2 | 3 | def case_name(): # Return name of this case 4 | return 'Current' 5 | 6 | def get_node_data(): # Data for each node 7 | return { 8 | # node_id: (max_supply, demand, supply_cost, [connected_node_ids]) 9 | 1: (100000, 0, 0.80, [2, 3]), 10 | 2: (60000, 40000, 1.05, [1, 4, 5]), 11 | 3: (25000, 30000, 0.90, [1, 4]), 12 | 4: (0, 20000, 1.10, [2, 3, 5, 6]), 13 | 5: (0, 25000, 1.12, [2, 6]), 14 | 6: (0, 50000, 1.15, [4, 5]) 15 | } 16 | 17 | def get_connection_data(): # Data for each edge 18 | return { 19 | # (from, to): (capacity, distance, cost, fixed loss, variable loss) 20 | (1, 2): (80000, 150, 0.0010, 0.011*0, 0.00030), 21 | (1, 3): (40000, 200, 0.0015, 0.010*0, 0.00035), 22 | (2, 4): (50000, 95, 0.0024, 0.020*0, 0.00032), 23 | (2, 5): (20000, 100, 0.0012, 0.014*0, 0.00015), 24 | (3, 4): (90000, 120, 0.0012, 0.010*0, 0.00020), 25 | (4, 5): (20000, 50, 0.0025, 0.010*0, 0.00022), 26 | (4, 6): (50000, 75, 0.0008, 0.012*0, 0.00018), 27 | (5, 6): (60000, 60, 0.0011, 0.010*0, 0.00031) 28 | } 29 | 30 | def get_node_positions(): # Position of each node 31 | return { 32 | 1: (0.2, 0.8), 33 | 2: (0.5, 0.8), 34 | 3: (0.2, 0.5), 35 | 4: (0.5, 0.5), 36 | 5: (0.8, 0.5), 37 | 6: (0.8, 0.2) 38 | } 39 | 40 | def get_plot_data(): 41 | node_label_offsets = { # Position labels for each node 42 | 1: (-0.13, +0.00), 43 | 2: (+0.05, +0.00), 44 | 3: (-0.13, +0.00), 45 | 4: (-0.11, -0.06), 46 | 5: (+0.04, +0.00), 47 | 6: (+0.04, +0.00) 48 | } 49 | 50 | edge_label_offsets = { # Position labels for each edge. State both directions, in case want to be non-symmetric 51 | (1, 2): (-0.05, -0.05), 52 | (2, 1): (-0.05, -0.05), 53 | (1, 3): (-0.10, +0.00), 54 | (3, 1): (-0.10, +0.00), 55 | (2, 4): (-0.10, +0.00), 56 | (4, 2): (-0.10, +0.00), 57 | (3, 4): (-0.05, +0.05), 58 | (4, 3): (-0.05, +0.05), 59 | (2, 5): (+0.01, +0.04), 60 | (5, 2): (+0.01, +0.04), 61 | (4, 5): (-0.05, +0.05), 62 | (5, 4): (-0.05, +0.05), 63 | (4, 6): (+0.01, +0.04), 64 | (6, 4): (+0.01, +0.04), 65 | (5, 6): (+0.02, +0.00), 66 | (6, 5): (+0.02, +0.00), 67 | } 68 | return node_label_offsets, edge_label_offsets 69 | -------------------------------------------------------------------------------- /Articles/Potatoes/readme.md: -------------------------------------------------------------------------------- 1 | ## Locational marginal pricing of potatoes 2 | In this article, we apply Locational Marginal Pricing (LMP) to the supply of potatoes to fast-food restaurants. 3 | 4 | Our goal is to provide the restaurants' suppliers and contractors with price signals to incentivize efficient bahaviour. This goal is essentially the same as a more common application of LMP: pricing in electricity markets. 5 | The method is much the same too. That is, the locational marginal prices are calculated by the linear program solver as the "dual values" on the node balance constraints. 6 | 7 | We describe the optimization model and calculation of the LMPs. We then explore several scenarios for how the suppliers and contractors may respond to the LMP signals. 8 | 9 | Blog article: [Locational marginal pricing of potatoes](https://www.solvermax.com/blog/locational-marginal-pricing-of-potatoes) 10 | -------------------------------------------------------------------------------- /Articles/Price-breaks-in-a-linear-programming-model/price-breaks.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Price-breaks-in-a-linear-programming-model/price-breaks.xlsx -------------------------------------------------------------------------------- /Articles/Price-breaks-in-a-linear-programming-model/readme.md: -------------------------------------------------------------------------------- 1 | ## Price breaks in a linear programming model 2 | Price breaks, or volume discounts, are common when buying products in bulk. That is, the marginal cost of additional products falls as volume increases. In a linear program, price breaks are tricky to model because the break points are non-linear discontinuities. 3 | 4 | In a spreadsheet, a natural way to model price breaks is to use functions like IF, VLOOKUP, CHOOSE, MIN, and/or MAX. However, those functions are discontinuous, so we can't use the Simplex linear method in Solver. We also can't use the OpenSolver solvers at all when our model includes those functions. We could use Solver's GRG non-linear or Evolutionary methods, but they are not always reliable. 5 | 6 | Fortunately, there is a way to express price breaks linearly, by using some binary variables, as described in our article [MIP formulations and linearizations](https://www.solvermax.com/blog/mip-formulations-and-linearizations/). 7 | 8 | In this article, we describe an example of how to represent price breaks in a linear programming model. 9 | 10 | The model is built in Excel and solved using either Solver or OpenSolver. 11 | 12 | Blog article: [Price breaks in a linear programming model](https://www.solvermax.com/blog/price-breaks-in-a-linear-programming-model) 13 | -------------------------------------------------------------------------------- /Articles/Production-mix-via-graphical-LP/productionmix.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Production-mix-via-graphical-LP/productionmix.xlsx -------------------------------------------------------------------------------- /Articles/Production-mix-via-graphical-LP/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix via graphical LP 2 | "Production mix" planning is a common business problem. That is, given available resources, what mix of products should we make to maximize profit? 3 | 4 | With potentially many trade-offs between production choices and available resources, deciding on the best volume for each product can be a complex decision. The gains from making a better decision can be substantial, leading to improved profit and more efficient use of resources. 5 | 6 | Blog article: [Production mix via graphical LP](https://www.solvermax.com/blog/production-mix) 7 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-01-Pyomo-concrete/production-model-1.py: -------------------------------------------------------------------------------- 1 | # Production mix - Model 1 2 | 3 | # Import dependencies 4 | 5 | import pyomo.environ as pyo 6 | 7 | # Declarations 8 | 9 | Model = pyo.ConcreteModel(name = 'Boutique pottery shop - Model 1') 10 | 11 | # Define model 12 | 13 | Model.Discs = pyo.Var(domain = pyo.NonNegativeReals) 14 | Model.Orbs = pyo.Var(domain = pyo.NonNegativeReals) 15 | 16 | Model.PeopleHours = pyo.Constraint(expr = 12.50 * Model.Discs + 10.00 * Model.Orbs <= 250) 17 | Model.MaterialUsage = pyo.Constraint(expr = 18.00 * Model.Discs + 30.00 * Model.Orbs <= 500) 18 | Model.SalesRelationship = pyo.Constraint(expr = -2.00 * Model.Discs + 1.00 * Model.Orbs <= 0) 19 | 20 | Model.TotalMargin = pyo.Objective(expr = 80.00 * Model.Discs + 200.00 * Model.Orbs, sense = pyo.maximize) 21 | 22 | # Solve model 23 | 24 | Solver = pyo.SolverFactory('cbc') 25 | Results = Solver.solve(Model) 26 | 27 | # Write output 28 | 29 | print(Model.name, '\n') 30 | print('Status: ', Results.solver.status, '\n') 31 | print(f'Total margin = ${Model.TotalMargin():,.2f}') 32 | print(f'Production of discs = {Model.Discs():6.2f}') 33 | print(f'Production of orbs = {Model.Orbs():6.2f}') 34 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-01-Pyomo-concrete/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix - Model 1, Pyomo 2 | In this article we build Model 1 of the Python Production mix series, using the Pyomo library. 3 | 4 | The Production mix model relates to our hypothetical boutique pottery business, which is described in more detail in the article Python optimization Rosetta Stone. 5 | 6 | Our objective for this article is to build and explain the workings of a simple Pyomo example. 7 | 8 | Although Model 1 is like many examples of a Pyomo model that you may find on the web and in textbooks, it certainly does not represent best (or even good) practice. In subsequent articles, we'll incrementally improve the model, leading to a structure that is more suitable for operational models. 9 | 10 | Even so, Model 1 is an easy-to-understand place to start our exploration of building optimization models in Python. It is worth understanding this model before moving on to more sophisticated versions. 11 | 12 | Blog article: [Production mix - Model 1, Pyomo](https://www.solvermax.com/blog/production-mix-model-1-pyomo) 13 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-02-Pyomo-separate-data/production-model-2.py: -------------------------------------------------------------------------------- 1 | # Production mix - Model 2 2 | 3 | # Import dependencies 4 | 5 | import pyomo.environ as pyo 6 | import pandas as pd 7 | 8 | # Declarations 9 | 10 | Model = pyo.ConcreteModel(name = 'Boutique pottery shop - Model 2') 11 | 12 | Hours = 250 13 | kg = 500 14 | SalesLimit = 0 15 | 16 | Coefficients = { 17 | 'Discs' : {'People': 12.50, 'Materials': 18.00, 'Sales': -2.00, 'Margin': 80.00}, 18 | 'Orbs' : {'People': 10.00, 'Materials': 30.00, 'Sales': 1.00, 'Margin': 200.00}, 19 | } 20 | 21 | Products = Coefficients.keys() 22 | 23 | VarInitial = 0 24 | VarBounds = (0, 100) 25 | 26 | # Declarations 27 | 28 | Model = pyo.ConcreteModel(name = 'Boutique pottery shop - Model 2') 29 | 30 | Hours = 250 31 | kg = 500 32 | SalesLimit = 0 33 | 34 | Coefficients = { 35 | 'Discs' : {'People': 12.50, 'Materials': 18.00, 'Sales': -2.00, 'Margin': 80.00}, 36 | 'Orbs' : {'People': 10.00, 'Materials': 30.00, 'Sales': 1.00, 'Margin': 200.00}, 37 | } 38 | 39 | Products = Coefficients.keys() 40 | 41 | VarInitial = 0 42 | VarBounds = (0, 100) 43 | 44 | # Solve model 45 | 46 | Solver = pyo.SolverFactory('cbc') 47 | Results = Solver.solve(Model) 48 | 49 | # Write output 50 | 51 | print(Model.name, '\n') 52 | print('Status: ', Results.solver.status, '\n') 53 | print(f'Total margin = ${Model.TotalMargin():,.2f}\n') 54 | ProductResults = pd.DataFrame() 55 | for p in Products: 56 | ProductResults.loc[p, 'Production'] = round(pyo.value(Model.Production[p]), 2) 57 | display(ProductResults) 58 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-02-Pyomo-separate-data/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix - Model 2, Pyomo 2 | In this article we continue the Python Production mix series, using the Pyomo library. 3 | 4 | Specifically, we build Model 2, which improves Model 1 by: 5 | - Extracting the hard coded coefficients and putting them into Python data structures. 6 | 7 | Separately the data from the model is an important step in developing a practical design that can be used for operational models. 8 | 9 | Blog article: [Production mix - Model 2, Pyomo](https://www.solvermax.com/blog/production-mix-model-2-pyomo) 10 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-03-Pyomo-external-data/production-model-3.py: -------------------------------------------------------------------------------- 1 | # Production mix - Model 3 2 | 3 | # Import dependencies 4 | 5 | import pyomo.environ as pyo 6 | import pandas as pd 7 | 8 | # Import dependencies 9 | 10 | import pyomo.environ as pyo 11 | import pandas as pd 12 | 13 | # Declarations 14 | 15 | Model = pyo.ConcreteModel(name = Name) 16 | 17 | # Define model 18 | 19 | Model.Production = pyo.Var(Products, domain = pyo.NonNegativeReals, initialize = VarInitial, bounds = VarBounds) 20 | 21 | Model.PeopleHours = pyo.Constraint(expr = sum(Coefficients[p]['People'] * Model.Production[p] for p in Products) <= Hours) 22 | Model.MaterialUsage = pyo.Constraint(expr = sum(Coefficients[p]['Materials'] * Model.Production[p] for p in Products) <= kg) 23 | Model.SalesRelationship = pyo.Constraint(expr = sum(Coefficients[p]['Sales'] * Model.Production[p] for p in Products) <= SalesLimit) 24 | 25 | Model.TotalMargin = pyo.Objective(expr = sum(Coefficients[p]['Margin'] * Model.Production[p] for p in Products), sense = pyo.maximize) 26 | 27 | # Solve model 28 | 29 | Solver = pyo.SolverFactory(Engine) 30 | 31 | if Engine == 'cbc': 32 | Solver.options['seconds'] = TimeLimit 33 | elif Engine == 'glpk': 34 | Solver.options['tmlim'] = TimeLimit 35 | 36 | Results = Solver.solve(Model, load_solutions = False, tee = False) 37 | 38 | # Process results 39 | 40 | WriteSolution = False 41 | Optimal = False 42 | LimitStop = False 43 | Condition = Results.solver.termination_condition 44 | 45 | if Condition == pyo.TerminationCondition.optimal: 46 | Optimal = True 47 | if Condition == pyo.TerminationCondition.maxTimeLimit or Condition == pyo.TerminationCondition.maxIterations: 48 | LimitStop = True 49 | 50 | if Optimal or LimitStop: 51 | try: 52 | WriteSolution = True 53 | Model.solutions.load_from(Results) # Defer loading results until now, in case there is no solution to load 54 | SolverData = Results.Problem._list 55 | SolutionLB = SolverData[0].lower_bound 56 | SolutionUB = SolverData[0].upper_bound 57 | except: 58 | WriteSolution = False 59 | 60 | # Write output 61 | 62 | print(Model.name, '\n') 63 | print('Status:', Results.solver.termination_condition) 64 | print('Solver:', Engine, '\n') 65 | 66 | if LimitStop: # Indicate how close we are to a solution 67 | print('Objective bounds') 68 | print('----------------') 69 | if SolutionLB is None: 70 | print('Lower: None') 71 | else: 72 | print(f'Lower: {SolutionLB:9,.2f}') 73 | if SolutionUB is None: 74 | print('Upper: None\n') 75 | else: 76 | print(f'Upper: {SolutionUB:9,.2f}\n') 77 | if WriteSolution: 78 | print(f'Total margin = ${Model.TotalMargin():,.2f}\n') 79 | ProductResults = pd.DataFrame() 80 | for p in Products: 81 | ProductResults.loc[p, 'Production'] = round(pyo.value(Model.Production[p]), 2) 82 | display(ProductResults) 83 | else: 84 | print('No solution loaded\n') 85 | print('Model:') 86 | Model.pprint() 87 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-03-Pyomo-external-data/productiondata3.py: -------------------------------------------------------------------------------- 1 | Name = 'Boutique pottery shop - Model 3' 2 | Hours = 250 3 | kg = 500 4 | SalesLimit = 0 5 | 6 | Coefficients = { 7 | 'Discs' : {'People': 12.50, 'Materials': 18.00, 'Sales': -2.00, 'Margin': 80.00}, 8 | 'Orbs' : {'People': 10.00, 'Materials': 30.00, 'Sales': 1.00, 'Margin': 200.00} 9 | } 10 | Products = Coefficients.keys() 11 | 12 | VarInitial = 0 13 | VarBounds = (0, 100) 14 | 15 | Engine = 'cbc' 16 | TimeLimit = 60 # seconds 17 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-03-Pyomo-external-data/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix - Model 3, Pyomo 2 | In this article we continue the Python Production mix series, using the Pyomo library. 3 | 4 | Specifically, we build Model 3, which changes Model 2 by: 5 | - Extracting the data from the model, and putting it in an external file. 6 | - Implementing better handling of the solve process. 7 | - Expanding the output to include additional information. 8 | 9 | Each of these improvements are important steps in developing a practical design that can be used for operational models. 10 | 11 | Blog article: [Production mix - Model 3, Pyomo](https://www.solvermax.com/blog/production-mix-model-3-pyomo) 12 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-04-Pyomo-json-file/productiondata4.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Boutique pottery shop - Model 4", 3 | "Hours": 250, 4 | "kg": 500, 5 | "SalesLimit": 0, 6 | "Coefficients": { 7 | "Discs": {"People": 12.50, "Materials": 18.00, "Sales": -2.00, "Margin": 80.00}, 8 | "Orbs": {"People": 10.00, "Materials": 30.00, "Sales": 1.00, "Margin": 200.00} 9 | }, 10 | "VarInitial": 0, 11 | "VarLBounds": 0, 12 | "VarUBounds": 100, 13 | "Engine": "cbc", 14 | "TimeLimit": 60 15 | } 16 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-04-Pyomo-json-file/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix - Model 4, Pyomo 2 | In this article we continue the Python Production mix series, using the Pyomo library. 3 | 4 | Specifically, we build Model 4, which changes Model 3 to: 5 | - Import the data from an external json file. 6 | - Read the data into the Model object, rather than into separate objects. 7 | 8 | These changes reflect features that we may need to include in an operational model. 9 | 10 | Blog article: [Production mix - Model 4, Pyomo](https://www.solvermax.com/blog/production-mix-model-4-pyomo) 11 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-05-Pyomo-using-def/productiondata5.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Boutique pottery shop - Model 5", 3 | "Hours": 250, 4 | "kg": 500, 5 | "SalesLimit": 0, 6 | "Coefficients": { 7 | "Discs": {"People": 12.50, "Materials": 18.00, "Sales": -2.00, "Margin": 80.00}, 8 | "Orbs": {"People": 10.00, "Materials": 30.00, "Sales": 1.00, "Margin": 200.00} 9 | }, 10 | "VarInitial": 0, 11 | "VarLBounds": 0, 12 | "VarUBounds": 100, 13 | "Engine": "cbc", 14 | "TimeLimit": 60 15 | } 16 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-05-Pyomo-using-def/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix - Model 5, Pyomo 2 | In this article we continue the Python Production mix series, using the Pyomo library. 3 | 4 | Specifically, we build Model 5, which changes Model 4 to: 5 | - Define the constraints and objective function using def function blocks. 6 | - Output the slack values and dual prices (also known as shadow prices) for each constraint. 7 | 8 | These changes give us more control over how the model is defined and provide more information about the solution. 9 | 10 | Blog article: [Production mix - Model 5, Pyomo](https://www.solvermax.com/blog/production-mix-model-5-pyomo) 11 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-06-Pyomo-abstract/productiondata6.dat: -------------------------------------------------------------------------------- 1 | param: Name := 'Boutique pottery shop - Model 6'; 2 | param: Hours := 250; 3 | param: kg := 500; 4 | param: SalesLimit := 0; 5 | param: VarInitial := 0; 6 | param: VarLBounds := 0; 7 | param: VarUBounds := 100; 8 | param: Engine := "cbc"; 9 | param: TimeLimit := 60; 10 | 11 | param: Products: People Materials Sales Margin := 12 | Discs 12.50 18.00 -2 80.00 13 | Orbs 10.00 30.00 1 200.00; 14 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-06-Pyomo-abstract/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix - Model 6, Pyomo 2 | In this article we continue the Python Production mix series, using the Pyomo library. 3 | 4 | Specifically, we build Model 6, which changes Model 5 to: 5 | - Declare the model as a Pyomo pyo.AbstractModel, rather than as a pyo.ConcreteModel. 6 | - Read the data from a dat file rather than a json file. 7 | 8 | These changes show that, contrary to how abstract and concrete models are portrayed in most blogs, there is actually little difference between abstract and concrete Pyomo models. 9 | 10 | Blog article: [Production mix - Model 6, Pyomo](https://www.solvermax.com/blog/production-mix-model-6-pyomo) 11 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-07-PuLP/production-model-7.py: -------------------------------------------------------------------------------- 1 | # Production mix - Model 7 2 | 3 | # Import dependencies 4 | 5 | import pulp as pu 6 | import pandas as pd 7 | import os.path 8 | import json 9 | 10 | # Get data 11 | 12 | DataFilename = os.path.join('.', 'productiondata7.json') 13 | with open(DataFilename, 'r') as f: 14 | Data = json.load(f) 15 | 16 | # Declarations 17 | 18 | Model = pu.LpProblem(Data['Name'], pu.LpMaximize) 19 | 20 | Model.Hours = Data['Hours'] 21 | Model.kg = Data['kg'] 22 | Model.SalesLimit = Data['SalesLimit'] 23 | Model.VarInitial = Data['VarInitial'] 24 | Model.VarLBounds = Data['VarLBounds'] 25 | Model.VarUBounds = Data['VarUBounds'] 26 | Model.Engine = Data['Engine'] 27 | Model.TimeLimit = Data['TimeLimit'] 28 | 29 | Coefficients = Data['Coefficients'] 30 | Model.Products = list(Coefficients.keys()) 31 | 32 | Model.People = {} 33 | Model.Materials = {} 34 | Model.Sales = {} 35 | Model.Margin = {} 36 | 37 | for p in Model.Products: 38 | Model.People[p] = Coefficients[p]['People'] 39 | Model.Materials[p] = Coefficients[p]['Materials'] 40 | Model.Sales[p] = Coefficients[p]['Sales'] 41 | Model.Margin[p] = Coefficients[p]['Margin'] 42 | 43 | # Define model 44 | 45 | Model.Production = pu.LpVariable.dicts("Products", Model.Products, lowBound=Model.VarLBounds, upBound=Model.VarUBounds, cat=pu.LpContinuous) 46 | for p in Model.Products: 47 | Model.Production[p].setInitialValue(Model.VarInitial) 48 | 49 | def constraint_hours(): 50 | return pu.lpSum([Model.People[p] * Model.Production[p] for p in Model.Products]) <= Model.Hours, 'PeopleHours' 51 | Model += constraint_hours() 52 | 53 | def constraint_usage(): 54 | return pu.lpSum([Model.Materials[p] * Model.Production[p] for p in Model.Products]) <= Model.kg, 'MaterialUsage' 55 | Model += constraint_usage() 56 | 57 | def constraint_sales(): 58 | return pu.lpSum([Model.Sales[p] * Model.Production[p] for p in Model.Products]) <= Model.SalesLimit, 'SalesRelationship' 59 | Model += constraint_sales() 60 | 61 | def objective_margin(): 62 | return pu.lpSum([Model.Margin[p] * Model.Production[p] for p in Model.Products]) 63 | Model.setObjective(objective_margin()) 64 | 65 | # Solve model 66 | 67 | if Model.Engine == 'cbc': 68 | Solver = pu.PULP_CBC_CMD(timeLimit = Model.TimeLimit) 69 | elif Model.Engine == 'glpk': 70 | Solver = pu.GLPK_CMD(timeLimit = Model.TimeLimit) 71 | 72 | Status = Model.solve(Solver) 73 | 74 | # Process results 75 | 76 | WriteSolution = False 77 | Optimal = False 78 | Condition = pu.LpStatus[Model.status] 79 | 80 | if Condition == 'Optimal': 81 | Optimal = True 82 | WriteSolution = True 83 | 84 | # Write output 85 | 86 | print(Model.name, '\n') 87 | print('Status:', pu.LpStatus[Model.status]) 88 | print('Solver:', Model.Engine, '\n') 89 | 90 | if WriteSolution: 91 | print(f"Total margin = ${Model.objective.value():,.2f}\n") 92 | pd.options.display.float_format = "{:,.4f}".format 93 | ProductResults = pd.DataFrame() 94 | for p in Model.Products: 95 | ProductResults.loc[p, 'Production'] = pu.value(Model.Production[p]) 96 | display(ProductResults) 97 | 98 | ConstraintStatus = pd.DataFrame(columns=['Slack', 'Dual']) 99 | for name, c in list(Model.constraints.items()): 100 | ConstraintStatus.loc[name] = [c.slack, c.pi] 101 | display(ConstraintStatus) 102 | else: 103 | print('No solution loaded\n') 104 | print('Model:') 105 | print(Model) -------------------------------------------------------------------------------- /Articles/Production-mix/Model-07-PuLP/productiondata7.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Boutique_pottery_shop_Model_7", 3 | "Hours": 250, 4 | "kg": 500, 5 | "SalesLimit": 0, 6 | "Coefficients": { 7 | "Discs": {"People": 12.50, "Materials": 18.00, "Sales": -2.00, "Margin": 80.00}, 8 | "Orbs": {"People": 10.00, "Materials": 30.00, "Sales": 1.00, "Margin": 200.00} 9 | }, 10 | "VarInitial": 0, 11 | "VarLBounds": 0, 12 | "VarUBounds": 100, 13 | "Engine": "cbc", 14 | "TimeLimit": 10 15 | } -------------------------------------------------------------------------------- /Articles/Production-mix/Model-07-PuLP/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix - Model 7, PuLP 2 | 3 | In this article we continue the Python Production mix series, using the PuLP library. 4 | 5 | Specifically, we build Model 7, which: 6 | 7 | - Builds the model using the PuLP library. 8 | - Reads the data from a json file. 9 | 10 | There is a close similarity between this PuLP model and our Pyomo models. Although the syntax of the two libraries is somewhat different, the general structure of the model definitions and solution process is familiar. This isn't surprising, as both PuLP and Pyomo are COIN-OR projects. 11 | 12 | Blog article: [Production mix - Model 7, Pyomo](https://www.solvermax.com/blog/production-mix-model-7-pulp) 13 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-08-OR-Tools/production-model-8.py: -------------------------------------------------------------------------------- 1 | # Production mix - Model 8 2 | 3 | # Import dependencies 4 | 5 | from ortools.linear_solver import pywraplp 6 | import pandas as pd 7 | import os.path 8 | import json 9 | 10 | # Get data 11 | 12 | DataFilename = os.path.join('.', 'productiondata8.json') 13 | with open(DataFilename, 'r') as f: 14 | Data = json.load(f) 15 | 16 | # Declarations 17 | 18 | Model = pywraplp.Solver.CreateSolver(Data['Engine']) 19 | 20 | Model.Name = Data['Name'] 21 | Model.Hours = Data['Hours'] 22 | Model.kg = Data['kg'] 23 | Model.VarInitial = Data['VarInitial'] # Not used 24 | Model.VarLBounds = Data['VarLBounds'] 25 | Model.VarUBounds = Data['VarUBounds'] 26 | Model.Engine = Data['Engine'] 27 | Model.TimeLimit = Data['TimeLimit'] 28 | 29 | Model.SalesLimit = Data['SalesLimit'] 30 | Coefficients = Data['Coefficients'] 31 | Model.Products = list(Coefficients.keys()) 32 | Model.Production = {} 33 | 34 | Model.People = {} 35 | Model.Materials = {} 36 | Model.Sales = {} 37 | Model.Margin = {} 38 | 39 | for p in Model.Products: 40 | Model.People[p] = Coefficients[p]['People'] 41 | Model.Materials[p] = Coefficients[p]['Materials'] 42 | Model.Sales[p] = Coefficients[p]['Sales'] 43 | Model.Margin[p] = Coefficients[p]['Margin'] 44 | 45 | # Define model 46 | 47 | for p in Model.Products: 48 | Model.Production[p] = Model.NumVar(Model.VarLBounds, Model.VarUBounds, p) 49 | 50 | Model.PeopleHours = Model.Add(sum(Model.People[p] * Model.Production[p] for p in Model.Products) <= Model.Hours, 'PeopleHours') 51 | Model.MaterialUsage = Model.Add(sum(Model.Materials[p] * Model.Production[p] for p in Model.Products) <= Model.kg, 'MaterialUsage') 52 | Model.SalesRelationship = Model.Add(sum(Model.Sales[p] * Model.Production[p] for p in Model.Products) <= Model.SalesLimit, 'SalesRelationship') 53 | 54 | Model.TotalMargin = sum(Model.Margin[p] * Model.Production[p] for p in Model.Products) 55 | Model.Maximize(Model.TotalMargin) 56 | 57 | # Solve model 58 | 59 | Model.set_time_limit(Model.TimeLimit) 60 | Status = Model.Solve() 61 | 62 | # Process results 63 | 64 | WriteSolution = False 65 | Optimal = False 66 | 67 | if Status == pywraplp.Solver.OPTIMAL: 68 | Optimal = True 69 | WriteSolution = True 70 | StatusText = 'Optimal' 71 | elif (Status == pywraplp.Solver.INFEASIBLE): 72 | StatusText = 'Infeasible' 73 | elif (Status == pywraplp.Solver.UNBOUNDED): 74 | StatusText = 'Unbounded' 75 | elif (Status == pywraplp.Solver.ABNORMAL): 76 | StatusText = 'Abnormal' 77 | elif (Status == pywraplp.Solver.NOT_SOLVED): 78 | StatusText = 'Not solved' 79 | 80 | # Write output 81 | 82 | print(Model.Name, '\n') 83 | print('Status:', StatusText) 84 | print('Solver:', Model.Engine, '\n') 85 | 86 | if WriteSolution: 87 | print(f"Total margin = ${Model.Objective().Value():,.2f}\n") 88 | pd.options.display.float_format = "{:,.4f}".format 89 | ProductResults = pd.DataFrame() 90 | for p in Model.Products: 91 | ProductResults.loc[p, 'Production'] = Model.Production[p].solution_value() 92 | display(ProductResults) 93 | 94 | ConstraintStatus = pd.DataFrame(columns=['Slack', 'Dual']) 95 | activities = Model.ComputeConstraintActivities() 96 | for i, constraint in enumerate(Model.constraints()): 97 | ConstraintStatus.loc[constraint.name()] = [constraint.ub() - activities[constraint.index()], constraint.dual_value()] 98 | display(ConstraintStatus) 99 | else: 100 | print('No solution loaded\n') 101 | print('Model:') 102 | print(Model.ExportModelAsLpFormat(False).replace('\\', ' ')) -------------------------------------------------------------------------------- /Articles/Production-mix/Model-08-OR-Tools/productiondata8.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Boutique pottery shop - Model 8", 3 | "Hours": 250, 4 | "kg": 500, 5 | "SalesLimit": 0, 6 | "Coefficients": { 7 | "Discs": {"People": 12.50, "Materials": 18.00, "Sales": -2.00, "Margin": 80.00}, 8 | "Orbs": {"People": 10.00, "Materials": 30.00, "Sales": 1.00, "Margin": 200.00} 9 | }, 10 | "VarInitial": 0, 11 | "VarLBounds": 0, 12 | "VarUBounds": 100, 13 | "Engine": "GLOP", 14 | "TimeLimit": 10 15 | } -------------------------------------------------------------------------------- /Articles/Production-mix/Model-08-OR-Tools/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix - Model 8, OR-Tools 2 | 3 | In this article we continue the Python Production mix series, using the OR-Tools library. 4 | 5 | Specifically, we build Model 8, which: 6 | 7 | - Builds the model using the OR-Tools library. 8 | - Reads the data from a json file. 9 | 10 | There is a close similarity between this OR-Tools model and our Pyomo models. Although the syntax of the two libraries is somewhat different, the general structure of the model definitions and solution process is familiar. 11 | 12 | Blog article: https://www.solvermax.com/blog/production-mix-model-8-or-tools 13 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-09-Gekko/productiondata9.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Boutique pottery shop - Model 9", 3 | "Hours": 250, 4 | "kg": 500, 5 | "SalesLimit": 0, 6 | "Coefficients": { 7 | "Discs": {"People": 12.50, "Materials": 18.00, "Sales": -2.00, "Margin": 80.00}, 8 | "Orbs": {"People": 10.00, "Materials": 30.00, "Sales": 1.00, "Margin": 200.00} 9 | }, 10 | "VarInitial": 30, 11 | "VarLBounds": 0, 12 | "VarUBounds": 100, 13 | "Engine": "apopt", 14 | "TimeLimit": 60 15 | } -------------------------------------------------------------------------------- /Articles/Production-mix/Model-09-Gekko/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix - Model 9, Gekko 2 | 3 | In this article we continue the Python Production mix series, using the Gekko library. 4 | 5 | Specifically, we build Model 9, which: 6 | 7 | - Builds the model using the Gekko library. 8 | - Reads the data from a json file. 9 | 10 | There is a close similarity between this Gekko model and our Pyomo models. Although the syntax of the two libraries is somewhat different, the general structure of the model definitions and solution process is familiar. 11 | 12 | Blog article: https://www.solvermax.com/blog/production-mix-model-9-gekko 13 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-10-CVXPY/production-model-10.py: -------------------------------------------------------------------------------- 1 | # Production mix - Model 10 2 | 3 | # Import dependencies 4 | 5 | import cvxpy as cp 6 | import pandas as pd 7 | import numpy as np 8 | import os.path 9 | import json 10 | 11 | # Get data 12 | 13 | DataFilename = os.path.join('.', 'productiondata10.json') 14 | with open(DataFilename, 'r') as f: 15 | Data = json.load(f) 16 | 17 | # Declarations 18 | 19 | Name = Data['Name'] 20 | Hours = Data['Hours'] 21 | kg = Data['kg'] 22 | SalesLimit = Data['SalesLimit'] 23 | VarInitial = Data['VarInitial'] # Not used 24 | VarLBounds = Data['VarLBounds'] 25 | VarUBounds = Data['VarUBounds'] 26 | Engine = Data['Engine'] 27 | TimeLimit = Data['TimeLimit'] 28 | 29 | Coefficients = Data['Coefficients'] 30 | Products = list(Coefficients.keys()) 31 | NumProducts = len(Products) 32 | 33 | Margin = np.zeros(NumProducts) 34 | People = np.zeros(NumProducts) 35 | Materials = np.zeros(NumProducts) 36 | Sales = np.zeros(NumProducts) 37 | for p in Products: 38 | i = int(p) 39 | Margin[i] = Coefficients[p]['Margin'] 40 | People[i] = Coefficients[p]['People'] 41 | Materials[i] = Coefficients[p]['Materials'] 42 | Sales[i] = Coefficients[p]['Sales'] 43 | 44 | # Define model 45 | 46 | Production = cp.Variable(NumProducts) # Variables 47 | 48 | objective = cp.Maximize(cp.sum(Margin @ Production)) # Objectve function 49 | constraints = [] # Constraints 50 | constraints += [cp.sum(People @ Production) <= Hours] 51 | constraints += [cp.sum(Materials @ Production) <= kg] 52 | constraints += [cp.sum(Sales @ Production) <= SalesLimit] 53 | 54 | constraints += [Production >= VarLBounds] # Bounds on variables 55 | constraints += [Production <= VarUBounds] 56 | 57 | # Solve model 58 | 59 | Model = cp.Problem(objective, constraints) 60 | 61 | if Engine == 'cbc': 62 | EngineObj = cp.CBC 63 | elif Engine == 'glop': 64 | EngineObj = cp.GLOP 65 | elif Engine == 'glpk': 66 | EngineObj = cp.GLPK 67 | elif Engine == 'cvxopt': 68 | EngineObj = cp.CVXOPT 69 | 70 | Result = Model.solve(solver=EngineObj, verbose=True, max_seconds=TimeLimit) 71 | 72 | # Process results 73 | 74 | WriteSolution = False 75 | Optimal = False 76 | Condition = Model.status 77 | 78 | if Condition == 'optimal': 79 | Optimal = True 80 | WriteSolution = True 81 | 82 | # Write output 83 | 84 | print(Name, '\n') 85 | print('Status:', Model.status) 86 | print('Solver:', Engine, '\n') 87 | 88 | if WriteSolution: 89 | print(f"Total margin = ${objective.value:,.2f}\n") 90 | pd.options.display.float_format = "{:,.4f}".format 91 | ProductResults = pd.DataFrame() 92 | for p in Products: 93 | ProductResults.loc[p, 'Production'] = Production[int(p)].value 94 | display(ProductResults) 95 | 96 | ConstraintStatus = pd.DataFrame(columns=['Slack', 'Dual']) 97 | for c in range(3): 98 | ConstraintStatus.loc[c] = [constraints[c].expr.value, constraints[c].dual_value] 99 | display(ConstraintStatus) 100 | else: 101 | print('No solution loaded\n') 102 | print('Model:') 103 | print(Model) -------------------------------------------------------------------------------- /Articles/Production-mix/Model-10-CVXPY/productiondata10.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Boutique pottery shop - Model 10", 3 | "Hours": 250, 4 | "kg": 500, 5 | "SalesLimit": 0, 6 | "Coefficients": { 7 | "0": {"People": 12.50, "Materials": 18.00, "Sales": -2.00, "Margin": 80.00}, 8 | "1": {"People": 10.00, "Materials": 30.00, "Sales": 1.00, "Margin": 200.00} 9 | }, 10 | "VarInitial": 0, 11 | "VarLBounds": 0, 12 | "VarUBounds": 100, 13 | "Engine": "cvxopt", 14 | "TimeLimit": 60 15 | } -------------------------------------------------------------------------------- /Articles/Production-mix/Model-10-CVXPY/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix - Model 10, CVXPY 2 | 3 | In this article we continue the Python Production mix series, using the CVXPY library. 4 | 5 | Specifically, we build Model 10, which: 6 | 7 | - Builds the model using the CVXPY library. 8 | - Reads the data from a json file. 9 | 10 | There is a close similarity between this CVXPY model and our Pyomo models. Although the syntax of the two libraries is somewhat different, the general structure of the model definitions and solution process is familiar. 11 | 12 | Blog article: https://www.solvermax.com/blog/production-mix-model-10-cvxpy 13 | -------------------------------------------------------------------------------- /Articles/Production-mix/Model-11-SciPy/production-model-11.py: -------------------------------------------------------------------------------- 1 | # Production mix - Model 11 2 | 3 | # Import dependencies 4 | 5 | from scipy.optimize import linprog 6 | import pandas as pd 7 | import numpy as np 8 | import os.path 9 | import json 10 | 11 | # Get data 12 | 13 | DataFilename = os.path.join('.', 'productiondata11.json') 14 | with open(DataFilename, 'r') as f: 15 | Data = json.load(f) 16 | 17 | # Declarations 18 | 19 | Name = Data['Name'] 20 | Hours = Data['Hours'] 21 | kg = Data['kg'] 22 | SalesLimit = Data['SalesLimit'] 23 | VarInitial = Data['VarInitial'] # Not used 24 | VarLBounds = Data['VarLBounds'] 25 | VarUBounds = Data['VarUBounds'] 26 | Engine = Data['Engine'] 27 | TimeLimit = Data['TimeLimit'] 28 | 29 | Coefficients = Data['Coefficients'] 30 | Products = list(Coefficients.keys()) 31 | NumProducts = len(Products) 32 | 33 | # Define model 34 | 35 | Margin = np.zeros(NumProducts) 36 | People = np.zeros(NumProducts) 37 | Materials = np.zeros(NumProducts) 38 | Sales = np.zeros(NumProducts) 39 | for p in Products: 40 | i = int(p)-1 41 | Margin[i] = -Coefficients[p]['Margin'] # Need to negate, as SciPy always minimizes, but we want to maximize 42 | People[i] = Coefficients[p]['People'] 43 | Materials[i] = Coefficients[p]['Materials'] 44 | Sales[i] = Coefficients[p]['Sales'] 45 | 46 | ObJCoeff = Margin 47 | Constraints = [People, Materials, Sales] 48 | rhs = [Hours, kg, SalesLimit] 49 | 50 | # Solve model 51 | 52 | Model = linprog(c = ObJCoeff, A_ub = Constraints, b_ub = rhs, bounds = [(VarLBounds, VarUBounds)], method = Engine, options = {'time_limit': TimeLimit}) 53 | 54 | # Process results 55 | 56 | WriteSolution = False 57 | Optimal = False 58 | Condition = Model.success 59 | 60 | if Condition: 61 | Optimal = True 62 | WriteSolution = True 63 | 64 | # Write output 65 | 66 | print(Name, '\n') 67 | print('Status: ', Model.success, '\n') 68 | 69 | if WriteSolution: 70 | print(f"Total margin = ${-Model.fun:,.2f}\n") # Need to negate, as we're maximizing 71 | pd.options.display.float_format = "{:,.4f}".format 72 | ProductResults = pd.DataFrame() 73 | for p in Products: 74 | ProductResults.loc[p, 'Production'] = Model.x[int(p)-1] 75 | display(ProductResults) 76 | ConstraintStatus = pd.DataFrame(columns=['Slack', 'Dual']) 77 | for c in range(len(Constraints)): 78 | ConstraintStatus.loc[c] = [Model.slack[c], Model['ineqlin']['marginals'][c]] 79 | display(ConstraintStatus) 80 | else: 81 | print('No solution loaded\n') 82 | print('Model:') 83 | print(Model) -------------------------------------------------------------------------------- /Articles/Production-mix/Model-11-SciPy/productiondata11.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Boutique pottery shop - Model 11", 3 | "Hours": 250, 4 | "kg": 500, 5 | "SalesLimit": 0, 6 | "Coefficients": { 7 | "1": {"People": 12.50, "Materials": 18.00, "Sales": -2.00, "Margin": 80.00}, 8 | "2": {"People": 10.00, "Materials": 30.00, "Sales": 1.00, "Margin": 200.00} 9 | }, 10 | "VarInitial": 0, 11 | "VarLBounds": 0, 12 | "VarUBounds": 100, 13 | "Engine": "highs", 14 | "TimeLimit": 60 15 | } -------------------------------------------------------------------------------- /Articles/Production-mix/Model-11-SciPy/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix - Model 11, SciPy 2 | 3 | In this article we continue the Python Production mix series, using the SciPy library. 4 | 5 | Specifically, we build Model 11, which: 6 | 7 | - Builds the model using the SciPy library. 8 | - Reads the data from a json file. 9 | 10 | There is a close similarity between this SciPy model and our Pyomo models. Although the syntax of the two libraries is somewhat different, the general structure of the model definitions and solution process is familiar. 11 | 12 | Blog article: https://www.solvermax.com/blog/production-mix-model-11-scipy 13 | -------------------------------------------------------------------------------- /Articles/Production-mix/readme.md: -------------------------------------------------------------------------------- 1 | ## Production mix model 2 | In this series of articles we build the "Production mix" optimization model using various Python Pyomo libraries. 3 | 4 | In total, we build eleven versions of the model – all of which return the same solution, but using different libraries, solvers, and Python programming techniques. 5 | 6 | Our first six models are built using Pyomo, starting with a simple "concrete" model, progressing through a variety of increasingly sophisticated concrete models, ending with an "abstract" model. The range of Pyomo models illustrates some of the many techniques that can be used to build optimization models in Python, even when using the same library. 7 | 8 | We then build the model in each of five other libraries: PuLP, OR Tools, Gekko, CVXPY, and SciPy. 9 | 10 | In addition to using different libraries to build the models, some libraries have multiple solvers available. For each model, we'll choose an appropriate solver, depending on which solvers are available to the library we're using. 11 | 12 | Our conclusions are summarized in the blog article: https://www.solvermax.com/blog/production-mix-conclusions 13 | -------------------------------------------------------------------------------- /Articles/Project-crashing-Time-cost-trade-off/projectcrashing.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Project-crashing-Time-cost-trade-off/projectcrashing.xlsx -------------------------------------------------------------------------------- /Articles/Project-crashing-Time-cost-trade-off/readme.md: -------------------------------------------------------------------------------- 1 | ## Project crashing: Time-cost trade-off 2 | Project crashing is the process of compressing a project plan by using additional resources to reduce the duration of some tasks. 3 | 4 | Using additional resources incurs additional cost, so it is important that the resources are deployed to the tasks that produce the greatest benefit. A project manager must decide an appropriate trade-off between time and cost for each task, leading to the best revised project plan. Given the dependencies between project tasks, making these trade-offs can be complex and difficult, even for a small project. 5 | 6 | This article describes an example of project crashing using an optimization model to help the project manager decide what to do. 7 | 8 | Blog article: [Project crashing: Time-cost trade-off](https://www.solvermax.com/blog/project-crashing) 9 | -------------------------------------------------------------------------------- /Articles/Python-embedded-in-Excel-First-impressions/Production-Mix-in-Python.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Python-embedded-in-Excel-First-impressions/Production-Mix-in-Python.xlsx -------------------------------------------------------------------------------- /Articles/Python-embedded-in-Excel-First-impressions/readme.md: -------------------------------------------------------------------------------- 1 | ## Python embedded in Excel:First impressions 2 | Microsoft recently announced a new Excel feature, Python embedded within Excel. This is great news, as it combines our two favourite tools: Excel and Python. 3 | 4 | Since our focus is on optimization modelling, we immediately wondered if it is possible to use a Python library to solve optimization models directly within Excel. Spoiler alert, it is! 5 | 6 | In this workbook we build and solve a linear program using the Python SciPy library directly in Excel. 7 | 8 | Blog article: https://www.solvermax.com/blog/python-embedded-in-excel-first-impressions 9 | -------------------------------------------------------------------------------- /Articles/RacksShelves/components/data-model-3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "e25bec47-ee83-4d54-bf9f-15c095e4aacd", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Load data from Excel file\n", 17 | "def GetData(DataFile, DataWorksheet, CasesFile, CasesWorksheet):\n", 18 | " Pallets = LoadFromExcel(DataFile, DataWorksheet, 'Heights') # List of all pallet sizes (decimetres) that we need to store os shelves\n", 19 | " Pallets.columns = ['Sizes'] # Name the pallets column\n", 20 | " MaxShelves = LoadFromExcel(DataFile, DataWorksheet, 'MaxShelves') # Maximum number of shelves in a rack\n", 21 | " WeightRacks = LoadFromExcel(DataFile, DataWorksheet, 'WeightRacks') # Objective function weight on the variable for number of racks\n", 22 | " WeightShelves = LoadFromExcel(DataFile, DataWorksheet, 'WeightShelves') # Objective function weight on the variable for number of shelves\n", 23 | " ShelfHeights = LoadFromExcel(CasesFile, CasesWorksheet, 'Cases') # Height of each shelf in each case\n", 24 | " ShelfHeights.columns = [str(n).zfill(1) for n in range(1, ShelfHeights.shape[1] + 1)] # Label columns from '1' to 'n'\n", 25 | " PalletsPerShelf = LoadFromExcel(DataFile, DataWorksheet, 'PalletsPerShelf') # Number of pallets on each shelf\n", 26 | " return Pallets, MaxShelves, ShelfHeights, WeightRacks, WeightShelves, PalletsPerShelf" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "id": "2a5ea3a6-34b0-43a7-9c35-d8e534f004f4", 33 | "metadata": { 34 | "editable": true, 35 | "slideshow": { 36 | "slide_type": "" 37 | }, 38 | "tags": [] 39 | }, 40 | "outputs": [], 41 | "source": [ 42 | "# Define model data, assigning all data to the Model\n", 43 | "def DefineModelData(Model, Case, Pallets, MaxShelves, ShelfHeights, PalletsPerShelf, WeightRacks, WeightShelves):\n", 44 | " Model.MaxShelves = MaxShelves[0].item()\n", 45 | " Model.WeightRacks = WeightRacks[0].item()\n", 46 | " Model.WeightShelves = WeightShelves[0].item()\n", 47 | " Model.PalletsPerShelf = PalletsPerShelf[0].item()\n", 48 | " Model.P = pyo.Set(initialize = range(0, len(Pallets)))\n", 49 | " Model.S = pyo.Set(initialize = range(0, Model.MaxShelves))\n", 50 | " Model.Pallets = pyo.Param(Model.P, within = pyo.NonNegativeIntegers, mutable = True)\n", 51 | " Model.ShelfHeights = pyo.Param(Model.S, within = pyo.NonNegativeIntegers, mutable = True)\n", 52 | " Model.NumShelves = 0\n", 53 | "\n", 54 | " for p in Model.P:\n", 55 | " Model.Pallets[p] = Pallets['Sizes'][p]\n", 56 | " for s in Model.S:\n", 57 | " Model.ShelfHeights[s] = ShelfHeights[str(s+1)][Case]\n", 58 | " if ShelfHeights[str(s+1)][Case] > 0:\n", 59 | " Model.NumShelves += 1" 60 | ] 61 | } 62 | ], 63 | "metadata": { 64 | "kernelspec": { 65 | "display_name": "Python 3 (ipykernel)", 66 | "language": "python", 67 | "name": "python3" 68 | }, 69 | "language_info": { 70 | "codemirror_mode": { 71 | "name": "ipython", 72 | "version": 3 73 | }, 74 | "file_extension": ".py", 75 | "mimetype": "text/x-python", 76 | "name": "python", 77 | "nbconvert_exporter": "python", 78 | "pygments_lexer": "ipython3", 79 | "version": "3.9.13" 80 | } 81 | }, 82 | "nbformat": 4, 83 | "nbformat_minor": 5 84 | } 85 | -------------------------------------------------------------------------------- /Articles/RacksShelves/components/formulation-model-2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "a4f7ad52-2fb1-4263-91a5-bb24e7cba005", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Define extra components of Model 2\n", 17 | "def DefineModel2(Model):\n", 18 | " Model.PalletShelf = pyo.Var(Model.P, Model.S, within = pyo.NonNegativeIntegers, bounds = (0, Model.MaxShelfSize), initialize = Model.MaxShelfSize)\n", 19 | "\n", 20 | " Model.PalletFits.deactivate() # This constraint is replaced by the following constraints\n", 21 | "\n", 22 | " # Bounds on ShelfHeights:\n", 23 | " # - Lower bound is zero, to allow for unused shelf positions. We retain the \"0 *\" part for completeness.\n", 24 | " # - Upper bound is maximum shelf size.\n", 25 | " \n", 26 | " def rule_fitlinear(Model, p): # Each pallet must be allocated to a shelf that is at least the height of the pallet\n", 27 | " return sum(Model.PalletShelf[p, s] for s in Model.S) >= Model.Pallets[p]\n", 28 | " Model.PalletFitsLinear = pyo.Constraint(Model.P, rule = rule_fitlinear)\n", 29 | "\n", 30 | " def rule_fitLB1(Model, p, s): # Linearization of fit constraint, part 1\n", 31 | " return 0 * Model.Allocation[p, s] <= Model.PalletShelf[p, s]\n", 32 | " Model.PalletFitsLB1 = pyo.Constraint(Model.P, Model.S, rule = rule_fitLB1)\n", 33 | "\n", 34 | " def rule_fitUB1(Model, p, s): # Linearization of fit constraint, part 2\n", 35 | " return Model.MaxShelfSize * Model.Allocation[p, s] >= Model.PalletShelf[p, s]\n", 36 | " Model.PalletFitsUB1 = pyo.Constraint(Model.P, Model.S, rule = rule_fitUB1)\n", 37 | "\n", 38 | " def rule_fitUB2(Model, p, s): # Linearization of fit constraint, part 3\n", 39 | " return Model.ShelfHeights[s] - Model.MaxShelfSize * (1 - Model.Allocation[p, s]) <= Model.PalletShelf[p, s]\n", 40 | " Model.PalletFitsUB2 = pyo.Constraint(Model.P, Model.S, rule = rule_fitUB2)\n", 41 | " \n", 42 | " def rule_fitLB2(Model, p, s): # Linearization of fit constraint, part 4\n", 43 | " return Model.ShelfHeights[s] - 0 * (1 - Model.Allocation[p, s]) >= Model.PalletShelf[p, s]\n", 44 | " Model.PalletFitsLB2 = pyo.Constraint(Model.P, Model.S, rule = rule_fitLB2)" 45 | ] 46 | } 47 | ], 48 | "metadata": { 49 | "kernelspec": { 50 | "display_name": "Python 3 (ipykernel)", 51 | "language": "python", 52 | "name": "python3" 53 | }, 54 | "language_info": { 55 | "codemirror_mode": { 56 | "name": "ipython", 57 | "version": 3 58 | }, 59 | "file_extension": ".py", 60 | "mimetype": "text/x-python", 61 | "name": "python", 62 | "nbconvert_exporter": "python", 63 | "pygments_lexer": "ipython3", 64 | "version": "3.9.13" 65 | } 66 | }, 67 | "nbformat": 4, 68 | "nbformat_minor": 5 69 | } 70 | -------------------------------------------------------------------------------- /Articles/RacksShelves/components/formulation-model-3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "026ed2ff-22c3-444c-8e33-e8f5eb18b342", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Define model\n", 17 | "def DefineModel3(Model):\n", 18 | " Model.Racks = pyo.Var(domain = pyo.NonNegativeIntegers, initialize = 0) # Number of racks\n", 19 | " Model.Allocation = pyo.Var(Model.P, Model.S, domain = pyo.Binary, initialize = 0) # Allocation of pallets to shelves and racks\n", 20 | "\n", 21 | " def rule_fit(Model, P): # Each pallet must be allocated to a shelf that is at least the height of the pallet\n", 22 | " return sum(Model.ShelfHeights[s] * Model.Allocation[P, s] for s in Model.S) >= Model.Pallets[P]\n", 23 | " Model.PalletFits = pyo.Constraint(Model.P, rule = rule_fit)\n", 24 | "\n", 25 | " def rule_use(Model, P): # Each pallet must be allocated to exactly one shelf\n", 26 | " return sum(Model.Allocation[P, s] for s in Model.S) == 1\n", 27 | " Model.MustUse = pyo.Constraint(Model.P, rule = rule_use)\n", 28 | "\n", 29 | " def rule_within(Model, S): # Times each shelf size is allocated to a pallet must be no larger than the number of racks\n", 30 | " return sum(Model.Allocation[p, S] for p in Model.P) <= Model.Racks * Model.PalletsPerShelf # Some shelves may be empty (Allocation = 0)\n", 31 | " Model.WithinRack = pyo.Constraint(Model.S, rule = rule_within)\n", 32 | " \n", 33 | " def rule_Obj(Model): # Minimize the number of racks we need to allocate all pallets to a shelf\n", 34 | " if WeightedObj: # Weighted to also minimize number of shelves in a rack, if required\n", 35 | " return Model.WeightRacks * Model.Racks + Model.WeightShelves * Model.NumShelves\n", 36 | " else:\n", 37 | " return Model.Racks\n", 38 | " Model.Obj = pyo.Objective(rule = rule_Obj, sense = pyo.minimize)" 39 | ] 40 | } 41 | ], 42 | "metadata": { 43 | "kernelspec": { 44 | "display_name": "Python 3 (ipykernel)", 45 | "language": "python", 46 | "name": "python3" 47 | }, 48 | "language_info": { 49 | "codemirror_mode": { 50 | "name": "ipython", 51 | "version": 3 52 | }, 53 | "file_extension": ".py", 54 | "mimetype": "text/x-python", 55 | "name": "python", 56 | "nbconvert_exporter": "python", 57 | "pygments_lexer": "ipython3", 58 | "version": "3.11.8" 59 | } 60 | }, 61 | "nbformat": 4, 62 | "nbformat_minor": 5 63 | } 64 | -------------------------------------------------------------------------------- /Articles/RacksShelves/components/imports.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "7ae23084-887a-4777-a63c-aecfbb663992", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Import dependencies\n", 17 | "import pyomo.environ as pyo\n", 18 | "import pandas as pd\n", 19 | "import numpy as np\n", 20 | "import time as tm\n", 21 | "import os.path\n", 22 | "from openpyxl import load_workbook\n", 23 | "from openpyxl.utils.cell import range_boundaries" 24 | ] 25 | } 26 | ], 27 | "metadata": { 28 | "kernelspec": { 29 | "display_name": "Python 3 (ipykernel)", 30 | "language": "python", 31 | "name": "python3" 32 | }, 33 | "language_info": { 34 | "codemirror_mode": { 35 | "name": "ipython", 36 | "version": 3 37 | }, 38 | "file_extension": ".py", 39 | "mimetype": "text/x-python", 40 | "name": "python", 41 | "nbconvert_exporter": "python", 42 | "pygments_lexer": "ipython3", 43 | "version": "3.9.13" 44 | } 45 | }, 46 | "nbformat": 4, 47 | "nbformat_minor": 5 48 | } 49 | -------------------------------------------------------------------------------- /Articles/RacksShelves/components/main-1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "b90954ba-eba0-4a80-9dc9-5fc90e9b7d37", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def main():\n", 11 | " Timer('Start');\n", 12 | " Model = pyo.ConcreteModel(name = ModelName)\n", 13 | " Model.Engine = SolverName\n", 14 | " Model.TimeLimit = TimeLimit\n", 15 | " Solver, Model = SetUpSolver(Model) \n", 16 | " Pallets, MaxShelves, MinShelfSize, MaxShelfSize, RackHeight, Gap, PalletsPerShelf, WeightRacks, WeightShelves = GetData(DataFile, DataWorksheet)\n", 17 | " DefineModelData(Model, Pallets, MaxShelves, MinShelfSize, MaxShelfSize, RackHeight, Gap, PalletsPerShelf, WeightRacks, WeightShelves)\n", 18 | " DefineModel1(Model)\n", 19 | " WriteModelToFile(WriteFile, Model)\n", 20 | " Timer('Setup');\n", 21 | " Results, Model = CallSolver(Solver, Model)\n", 22 | " Timer('Solved');\n", 23 | " WriteSolution, LimitStop, SolutionLB, SolutionUB = ProcessResults(Results, Model)\n", 24 | " Output(Results, Model, WriteSolution, LimitStop, SolutionLB, SolutionUB)\n", 25 | " Timer('Finish');\n", 26 | " WriteCheckpoints()" 27 | ] 28 | } 29 | ], 30 | "metadata": { 31 | "kernelspec": { 32 | "display_name": "Python 3 (ipykernel)", 33 | "language": "python", 34 | "name": "python3" 35 | }, 36 | "language_info": { 37 | "codemirror_mode": { 38 | "name": "ipython", 39 | "version": 3 40 | }, 41 | "file_extension": ".py", 42 | "mimetype": "text/x-python", 43 | "name": "python", 44 | "nbconvert_exporter": "python", 45 | "pygments_lexer": "ipython3", 46 | "version": "3.9.13" 47 | } 48 | }, 49 | "nbformat": 4, 50 | "nbformat_minor": 5 51 | } 52 | -------------------------------------------------------------------------------- /Articles/RacksShelves/components/main-2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "b2966f28-312c-4708-a308-cb8abf448523", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def main():\n", 11 | " Timer('Start');\n", 12 | " Model = pyo.ConcreteModel(name = ModelName)\n", 13 | " Model.Engine = SolverName\n", 14 | " Model.TimeLimit = TimeLimit\n", 15 | " Solver, Model = SetUpSolver(Model) \n", 16 | " Pallets, MaxShelves, MinShelfSize, MaxShelfSize, RackHeight, Gap, PalletsPerShelf, WeightRacks, WeightShelves = GetData(DataFile, DataWorksheet)\n", 17 | " DefineModelData(Model, Pallets, MaxShelves, MinShelfSize, MaxShelfSize, RackHeight, Gap, PalletsPerShelf, WeightRacks, WeightShelves)\n", 18 | " DefineModel1(Model)\n", 19 | " DefineModel2(Model) # Extra components of Model 2\n", 20 | " WriteModelToFile(WriteFile, Model)\n", 21 | " Timer('Setup');\n", 22 | " Results, Model = CallSolver(Solver, Model)\n", 23 | " Timer('Solved');\n", 24 | " WriteSolution, LimitStop, SolutionLB, SolutionUB = ProcessResults(Results, Model)\n", 25 | " Output(Results, Model, WriteSolution, LimitStop, SolutionLB, SolutionUB)\n", 26 | " Timer('Finish');\n", 27 | " WriteCheckpoints()" 28 | ] 29 | } 30 | ], 31 | "metadata": { 32 | "kernelspec": { 33 | "display_name": "Python 3 (ipykernel)", 34 | "language": "python", 35 | "name": "python3" 36 | }, 37 | "language_info": { 38 | "codemirror_mode": { 39 | "name": "ipython", 40 | "version": 3 41 | }, 42 | "file_extension": ".py", 43 | "mimetype": "text/x-python", 44 | "name": "python", 45 | "nbconvert_exporter": "python", 46 | "pygments_lexer": "ipython3", 47 | "version": "3.9.13" 48 | } 49 | }, 50 | "nbformat": 4, 51 | "nbformat_minor": 5 52 | } 53 | -------------------------------------------------------------------------------- /Articles/RacksShelves/components/main-3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "873e4261-eaf5-427a-add0-b81d82a04a96", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "def ModelCase(Case, Pallets, MaxShelves, ShelfHeights, PalletsPerShelf, WeightRacks, WeightShelves, BestObj, BestAllocation, BestCase):\n", 11 | " Model = pyo.ConcreteModel(name = ModelName + ', Case ' + str(Case + 1))\n", 12 | " Model.Engine = SolverName\n", 13 | " Model.TimeLimit = TimeLimit\n", 14 | " Solver, Model = SetUpSolver(Model) \n", 15 | " DefineModelData(Model, Case, Pallets, MaxShelves, ShelfHeights, PalletsPerShelf, WeightRacks, WeightShelves)\n", 16 | " DefineModel3(Model)\n", 17 | " Results, Model = CallSolver(Solver, Model)\n", 18 | " Output = f'Case {Case + 1:3,.0f}, shelves = '\n", 19 | " for s in Model.S:\n", 20 | " Output += f'{pyo.value(Model.ShelfHeights[s]):3,.0f}, '\n", 21 | " Output += f'obj = {Model.Obj():7,.1f}'\n", 22 | " ObjMarker = ''\n", 23 | " if round(pyo.value(Model.Obj()), 4) < BestObj:\n", 24 | " ObjMarker = ' *'\n", 25 | " BestObj = round(pyo.value(Model.Obj()), 4)\n", 26 | " BestAllocation = OutputAllocation(Model)\n", 27 | " BestCase = Output\n", 28 | " WriteModelToFile(WriteFile, Model)\n", 29 | " Output += ObjMarker\n", 30 | " print(Output)\n", 31 | " return BestObj, BestAllocation, BestCase" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "id": "f5f6d2bf-09b2-4df5-999e-006183b1c747", 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "def main():\n", 42 | " Timer('Start');\n", 43 | " BestObj = np.inf\n", 44 | " BestCase = ''\n", 45 | " BestAllocation = None\n", 46 | " Pallets, MaxShelves, ShelfHeights, WeightRacks, WeightShelves, PalletsPerShelf = GetData(DataFile, DataWorksheet, CasesFile, CasesWorksheet)\n", 47 | " Timer('Setup');\n", 48 | " for Case in range(0, ShelfHeights.shape[0]):\n", 49 | " BestObj, BestAllocation, BestCase = ModelCase(Case, Pallets, MaxShelves, ShelfHeights, PalletsPerShelf, WeightRacks, WeightShelves, BestObj, BestAllocation, BestCase)\n", 50 | " print('\\n')\n", 51 | " print(f'Best objective = {BestObj:7,.1f}')\n", 52 | " print(BestCase)\n", 53 | " print(BestAllocation)\n", 54 | " Timer('Finish');\n", 55 | " WriteCheckpoints()" 56 | ] 57 | } 58 | ], 59 | "metadata": { 60 | "kernelspec": { 61 | "display_name": "Python 3 (ipykernel)", 62 | "language": "python", 63 | "name": "python3" 64 | }, 65 | "language_info": { 66 | "codemirror_mode": { 67 | "name": "ipython", 68 | "version": 3 69 | }, 70 | "file_extension": ".py", 71 | "mimetype": "text/x-python", 72 | "name": "python", 73 | "nbconvert_exporter": "python", 74 | "pygments_lexer": "ipython3", 75 | "version": "3.9.13" 76 | } 77 | }, 78 | "nbformat": 4, 79 | "nbformat_minor": 5 80 | } 81 | -------------------------------------------------------------------------------- /Articles/RacksShelves/components/results-model-3.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "f7f36caa-06d4-4d11-a663-b8284a655404", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Write model output\n", 17 | "def OutputAllocation(Model):\n", 18 | " BestAllocation = '\\nAllocation\\n' # Allocation of pallets to shelves\n", 19 | " Header = ' Shelf\\n'\n", 20 | " Header += 'Pallet '\n", 21 | " for s in Model.S:\n", 22 | " Header += '{:6.0f}'.format(s + 1)\n", 23 | " Header += '\\n--------' + 6 * len(Model.S) * '-'\n", 24 | " BestAllocation += Header\n", 25 | " for p in Model.P:\n", 26 | " Row = '{:6.0f}'.format(p + 1) + ' '\n", 27 | " for s in Model.S:\n", 28 | " if round(pyo.value(Model.Allocation[p, s]), 0) == 1:\n", 29 | " Row += ' 1'\n", 30 | " else:\n", 31 | " Row += ' -'\n", 32 | " BestAllocation += '\\n' + Row\n", 33 | " Footer = '\\n--------' + 6 * len(Model.S) * '-' + '\\n'\n", 34 | " TotalRow = 'Total ' # Total pallets allocated to each shelf\n", 35 | " for s in Model.S:\n", 36 | " NumPallets = 0\n", 37 | " for p in Model.P:\n", 38 | " NumPallets += round(pyo.value(Model.Allocation[p, s]), 0)\n", 39 | " if NumPallets == 0:\n", 40 | " TotalRow += ' - '\n", 41 | " else:\n", 42 | " TotalRow += '{:5.0f}'.format(NumPallets) + ' '\n", 43 | " Footer += TotalRow\n", 44 | " BestAllocation += Footer\n", 45 | " return BestAllocation" 46 | ] 47 | } 48 | ], 49 | "metadata": { 50 | "kernelspec": { 51 | "display_name": "Python 3 (ipykernel)", 52 | "language": "python", 53 | "name": "python3" 54 | }, 55 | "language_info": { 56 | "codemirror_mode": { 57 | "name": "ipython", 58 | "version": 3 59 | }, 60 | "file_extension": ".py", 61 | "mimetype": "text/x-python", 62 | "name": "python", 63 | "nbconvert_exporter": "python", 64 | "pygments_lexer": "ipython3", 65 | "version": "3.9.13" 66 | } 67 | }, 68 | "nbformat": 4, 69 | "nbformat_minor": 5 70 | } 71 | -------------------------------------------------------------------------------- /Articles/RacksShelves/components/utilities.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "ab1d11b1-6b18-4141-a7d2-b94bbbc54051", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Record time checkpoints\n", 17 | "# Requires global variable: Checkpoints = []\n", 18 | "def Timer(Point):\n", 19 | " Checkpoints.append([Point, tm.perf_counter()])\n", 20 | " return Checkpoints" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 2, 26 | "id": "700fcc70-15c8-458d-b897-687db1cbf9c2", 27 | "metadata": { 28 | "editable": true, 29 | "slideshow": { 30 | "slide_type": "" 31 | }, 32 | "tags": [] 33 | }, 34 | "outputs": [], 35 | "source": [ 36 | "# Output list of times at each checkpoint\n", 37 | "def WriteCheckpoints():\n", 38 | " print('\\nCheckpoint Seconds')\n", 39 | " print('---------------------')\n", 40 | " Start = Checkpoints[0][1]\n", 41 | " for i in range(1, len(Checkpoints)):\n", 42 | " Point = Checkpoints[i][0]\n", 43 | " TimeStep = Checkpoints[i][1] - Start\n", 44 | " print(f'{Point:12}{TimeStep:9,.1f}')" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": 3, 50 | "id": "1f6a82cc-7ac3-4446-bd7d-1d1ad86eadc4", 51 | "metadata": { 52 | "editable": true, 53 | "slideshow": { 54 | "slide_type": "" 55 | }, 56 | "tags": [] 57 | }, 58 | "outputs": [], 59 | "source": [ 60 | "# Generic loader from Excel file, given worksheet and named range\n", 61 | "def LoadFromExcel(ExcelFile, Worksheet, Range):\n", 62 | " wb = load_workbook(filename=ExcelFile, read_only=True)\n", 63 | " ws = wb[Worksheet]\n", 64 | " dests = wb.defined_names[Range].destinations\n", 65 | " for title, coord in dests:\n", 66 | " min_col, min_row, max_col, max_row = range_boundaries(coord)\n", 67 | " data = ws.iter_rows(min_row, max_row, min_col, max_col, values_only=True)\n", 68 | " df = pd.DataFrame(data)\n", 69 | " return df" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "id": "6cc58099-1915-4cf4-92aa-7b096fce4a36", 76 | "metadata": { 77 | "editable": true, 78 | "slideshow": { 79 | "slide_type": "" 80 | }, 81 | "tags": [] 82 | }, 83 | "outputs": [], 84 | "source": [ 85 | "# Write model to file, if required\n", 86 | "def WriteModelToFile(WriteFile, Model):\n", 87 | " if WriteFile:\n", 88 | " Model.write(ModelFile, io_options={'symbolic_solver_labels': False})" 89 | ] 90 | } 91 | ], 92 | "metadata": { 93 | "kernelspec": { 94 | "display_name": "Python 3 (ipykernel)", 95 | "language": "python", 96 | "name": "python3" 97 | }, 98 | "language_info": { 99 | "codemirror_mode": { 100 | "name": "ipython", 101 | "version": 3 102 | }, 103 | "file_extension": ".py", 104 | "mimetype": "text/x-python", 105 | "name": "python", 106 | "nbconvert_exporter": "python", 107 | "pygments_lexer": "ipython3", 108 | "version": "3.9.13" 109 | } 110 | }, 111 | "nbformat": 4, 112 | "nbformat_minor": 5 113 | } 114 | -------------------------------------------------------------------------------- /Articles/RacksShelves/couenne.opt: -------------------------------------------------------------------------------- 1 | time_limit 3600 -------------------------------------------------------------------------------- /Articles/RacksShelves/data/pallets-200.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/RacksShelves/data/pallets-200.xlsx -------------------------------------------------------------------------------- /Articles/RacksShelves/data/pallets-2000.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/RacksShelves/data/pallets-2000.xlsx -------------------------------------------------------------------------------- /Articles/RacksShelves/data/pallets-20000.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/RacksShelves/data/pallets-20000.xlsx -------------------------------------------------------------------------------- /Articles/RacksShelves/racks-shelves-model-1-non-linear.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "6dac899b-30ae-43dd-972e-90e04d78bb65", 6 | "metadata": {}, 7 | "source": [ 8 | "# Racks and Shelves, Model 1, Non-linear formulation\n", 9 | "\n", 10 | "## Purpose\n", 11 | "Identify shelf heights that minimize the number of racks required to store given collection of pallets.\n", 12 | "\n", 13 | "## Usage:\n", 14 | "- Specify an Excel file containing a list of pallet heights.\n", 15 | "- Specify the other global assumptions, as defined below." 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "id": "e8c701b3-11d6-4472-8c93-9269bb73062a", 22 | "metadata": { 23 | "editable": true, 24 | "slideshow": { 25 | "slide_type": "" 26 | }, 27 | "tags": [] 28 | }, 29 | "outputs": [], 30 | "source": [ 31 | "%run ./components/imports.ipynb\n", 32 | "%run ./components/utilities.ipynb\n", 33 | "%run ./components/solver.ipynb\n", 34 | "%run ./components/data-model-1.ipynb\n", 35 | "%run ./components/formulation-model-1.ipynb\n", 36 | "%run ./components/main-1.ipynb\n", 37 | "%run ./components/results-model-1.ipynb" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "id": "dd6f0e84-fed5-436e-96f1-7de29350853f", 44 | "metadata": { 45 | "editable": true, 46 | "slideshow": { 47 | "slide_type": "" 48 | }, 49 | "tags": [] 50 | }, 51 | "outputs": [], 52 | "source": [ 53 | "# Globals\n", 54 | "\n", 55 | "# User selections\n", 56 | "DataFile = os.path.join(os.getcwd() + '\\data', 'pallets-200.xlsx')\n", 57 | "DataWorksheet = 'Data'\n", 58 | "WeightedObj = True\n", 59 | "\n", 60 | "# Solver options\n", 61 | "SolverName = 'couenne'\n", 62 | "Verbose = True\n", 63 | "LoadSolution = False\n", 64 | "Neos = False\n", 65 | "os.environ['NEOS_EMAIL'] = 'your.email@sample.com'\n", 66 | "TimeLimit = 1*3600\n", 67 | "\n", 68 | "# Model file\n", 69 | "WriteFile = True\n", 70 | "ModelFile = 'model1.nl'\n", 71 | "\n", 72 | "# Fixed\n", 73 | "ModelName = 'Racks and shelves - Model 1'\n", 74 | "Checkpoints = [] # List of time checkpoints" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "id": "85e0ef37-ff3f-4eeb-8213-1ee2806b8cf5", 81 | "metadata": { 82 | "editable": true, 83 | "slideshow": { 84 | "slide_type": "" 85 | }, 86 | "tags": [] 87 | }, 88 | "outputs": [], 89 | "source": [ 90 | "main()" 91 | ] 92 | } 93 | ], 94 | "metadata": { 95 | "kernelspec": { 96 | "display_name": "Python 3 (ipykernel)", 97 | "language": "python", 98 | "name": "python3" 99 | }, 100 | "language_info": { 101 | "codemirror_mode": { 102 | "name": "ipython", 103 | "version": 3 104 | }, 105 | "file_extension": ".py", 106 | "mimetype": "text/x-python", 107 | "name": "python", 108 | "nbconvert_exporter": "python", 109 | "pygments_lexer": "ipython3", 110 | "version": "3.11.8" 111 | } 112 | }, 113 | "nbformat": 4, 114 | "nbformat_minor": 5 115 | } 116 | -------------------------------------------------------------------------------- /Articles/RacksShelves/readme.md: -------------------------------------------------------------------------------- 1 | ## Warehouse space for free 2 | In this article series, we look at improving the efficiency of a pallet warehouse (also known as a unit-load warehouse), where all items are stored on standard-size pallets. 3 | 4 | Along the way, we: 5 | 6 | - Formulate a non-linear model of the situation. 7 | - Compare several solvers, to see how they perform. 8 | - Linearize our model to, hopefully, make it easier to solve. 9 | - Disaggregate our model to make some variables exogenous, then iterate over an enumeration of the exogenous variables. 10 | - Demonstrate use of Pyomo's last() and next() functions, which enable us to work with elements of ordered sets. 11 | - Turn off a constraint using Pyomo's deactivate() function. 12 | 13 | Importantly, we show that there's a surprising amount of extra storage space available for free, or minimal cost, just by redesigning the warehouse's racks and shelves. 14 | 15 | The model is built in Python using Pyomo. 16 | 17 | Blog article: https://www.solvermax.com/blog/warehouse-space-for-free-non-linear-model 18 | -------------------------------------------------------------------------------- /Articles/Refactor-Python-models-into-modules/Refactored/data.py: -------------------------------------------------------------------------------- 1 | # Data functions 2 | 3 | def defineData(): # Create random data for models 4 | random.seed(4) # Define a seed, so random numbers are the same in each run 5 | P, Q, cap = {}, {}, {} 6 | a = [j for j in range(1, 1 + Nstudent)] # List 1..j of Students 7 | b = [i for i in range(1, 1 + Nprof)] # List 1..i of Professors 8 | for i in range(1, 1 + Nprof): 9 | cap[i] = random.randint(ProfMinCapacity, ProfMaxCapacity) # Professor capacity for advising students 10 | random.shuffle(a) 11 | for j in range(1, 1 + Nstudent): 12 | P[i, j] = a[j - 1] # Preference of Professor i for Student j 13 | for j in range(1, 1 + Nstudent): 14 | random.shuffle(b) 15 | for i in range(1, 1 + Nprof): 16 | Q[i, j] = b[i - 1] # Preference of Student j for Professor i 17 | return a, b, P, Q, cap -------------------------------------------------------------------------------- /Articles/Refactor-Python-models-into-modules/Refactored/models.py: -------------------------------------------------------------------------------- 1 | # Model functions 2 | 3 | def defineModel1(): # Base case model, with additive Professor and Student preference weights 4 | model = ConcreteModel() 5 | model.i = RangeSet(Nprof) 6 | model.j = RangeSet(Nstudent) 7 | 8 | model.Coef = Param(model.i, model.j, initialize = 0, mutable = True) 9 | for (i,j) in P: 10 | model.Coef[i, j] = P[i, j] + Q[i, j] 11 | 12 | model.U = Var(model.i, model.j, initialize = 0, within = Binary) 13 | 14 | def rule_c1(model, i): 15 | return sum(model.U[i, j] for j in model.j) <= cap[i] 16 | model.C1 = Constraint(model.i, rule = rule_c1) 17 | 18 | def rule_c2(model, j): 19 | return sum(model.U[i, j] for i in model.i) == 1 20 | model.C2 = Constraint(model.j, rule = rule_c2) 21 | 22 | def rule_of(model): 23 | return sum(model.Coef[i, j] * model.U[i, j] for j in model.j for i in model.i) 24 | model.obj = Objective(rule = rule_of, sense = maximize) 25 | 26 | return model 27 | 28 | def defineModel2(model): # Change weights to be multiplicative 29 | for (i, j) in P: 30 | model.Coef[i, j] = P[i, j] * Q[i, j] 31 | return model 32 | 33 | def defineModel3(modelInherit): # Add constraints for minimum Professor and Student outcomes. Duplicates model, without changing existing model 34 | model = copy.deepcopy(modelInherit) 35 | model.ProfMinScore = Param(mutable = True, initialize = ProfMinScore) 36 | model.StudentMinScore = Param(mutable = True, initialize = StudentMinScore) 37 | 38 | def rule_minForProf(model, i): 39 | return sum(model.U[i, j] * P[i, j] for j in model.j) >= model.ProfMinScore * sum(model.U[i, j] for j in model.j) 40 | model.C_Prof_Min = Constraint(model.i, rule = rule_minForProf) 41 | 42 | def rule_minForStudent(model, j): 43 | return sum(model.U[i, j] * Q[i, j] for i in model.i) >= model.StudentMinScore 44 | model.C_Student_Min = Constraint(model.j, rule = rule_minForStudent) 45 | 46 | return model 47 | 48 | def defineModel4(modelInherit): # Generate efficient frontier by adding to existing model. Duplicates model, without changing existing model 49 | model = copy.deepcopy(modelInherit) 50 | model.limit = Param(initialize = 0, mutable = True) 51 | model.obj_prof = Var(initialize = 0, within = NonNegativeReals) 52 | model.obj_students = Var(initialize = 0, within = NonNegativeReals) 53 | 54 | def rule_c3(model): 55 | return sum(Q[i, j] * model.U[i, j] for i in model.i for j in model.j) == model.obj_students 56 | model.C3 = Constraint(rule = rule_c3) 57 | 58 | def rule_c4(model): 59 | return sum(P[i, j] * model.U[i, j] for i in model.i for j in model.j) == model.obj_prof 60 | model.C4 = Constraint(rule = rule_c4) 61 | 62 | def rule_c5(model): 63 | return model.obj_students >= model.limit 64 | model.C5 = Constraint(rule = rule_c5) 65 | 66 | return model 67 | 68 | def defineModel5(modelInherit): # Change minimum Professor and Student outcomes. Duplicate model, without changing existing model 69 | model = copy.deepcopy(modelInherit) 70 | model.StudentMinScore = StudentMinScore 71 | model.ProfMinScore = ProfMinScore 72 | 73 | return model 74 | -------------------------------------------------------------------------------- /Articles/Refactor-Python-models-into-modules/readme.md: -------------------------------------------------------------------------------- 1 | ## Refactor Python models into modules 2 | In this article we explore two aspects of working with multiple variations of an optimization model: 3 | - Modularization. We refactor an existing model into modules, consisting of functions located in separate files. 4 | - Adding models. We extend the existing model by adding variations that make efficient use of the modular structure. 5 | 6 | Along the way, we look at shallow vs deep copying in Python. This is a topic that often causes subtle bugs, so it is important to understand when working with variations of a model. 7 | 8 | Blog article: [https://www.solvermax.com/blog/refactor-python-models-into-modules](https://www.solvermax.com/blog/refactor-python-models-into-modules) 9 | -------------------------------------------------------------------------------- /Articles/Role-mining/data/sacmat relations.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Role-mining/data/sacmat relations.zip -------------------------------------------------------------------------------- /Articles/Role-mining/readme.md: -------------------------------------------------------------------------------- 1 | ## Permission granted: A role mining model 2 | In this article, we implement a recently published role mining model. 3 | 4 | The paper's authors claim that their new model formulation is both tractable and practical, allowing real world data sets to be solved in a reasonable amount of time. Like the authors of that paper, we build both constraint programming (CP) and mixed integer linear programming (MILP) models, then compare their relative performance while solving several examples. 5 | 6 | Blog article: [Permission granted: A role mining model](https://www.solvermax.com/blog/permission-granted-a-role-mining-model) 7 | -------------------------------------------------------------------------------- /Articles/Schedule-staff-with-enumerated-shifts/Schedule-staff-with-enumerated-shifts.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Schedule-staff-with-enumerated-shifts/Schedule-staff-with-enumerated-shifts.xlsx -------------------------------------------------------------------------------- /Articles/Schedule-staff-with-enumerated-shifts/readme.md: -------------------------------------------------------------------------------- 1 | ## Schedule staff with enumerated shifts 2 | A common application of optimization modelling is the scheduling of staff to meet demand. Scheduling problems can be difficult to solve, as there are often very specific requirements that need to be met, including staff availability, working hours, break times, etc. 3 | 4 | One approach for formulating scheduling problems is to enumerate all possible shift patterns and then decide how many of each shift pattern to use so that we meet the various constraints at least cost. 5 | 6 | In these blog articles we implement a staff scheduling model using two tools: 7 | - Excel, using the CBC solver via OpenSolver. 8 | - Python, using the CBC solver via Pyomo. 9 | 10 | Blog article, using OpenSolver: https://www.solvermax.com/blog/schedule-staff-with-enumerated-shifts-opensolver 11 | 12 | Blog article, using Pyomo: https://www.solvermax.com/blog/schedule-staff-with-enumerated-shifts-pyomo 13 | -------------------------------------------------------------------------------- /Articles/Sorting/readme.md: -------------------------------------------------------------------------------- 1 | ## Objectives matter: Sorting using a MIP model 2 | In this article, we assess the impact of using an alternative objective function in a model that sorts input parameters. 3 | 4 | The idea is to give the HiGHS solver greater traction while working through the solution space, hopefully helping it to solve the model faster. We've found this technique to be useful for some other models – will it help in this situation? 5 | 6 | Blog article: https://www.solvermax.com/blog/objectives-matter-sorting-using-a-mip-model 7 | -------------------------------------------------------------------------------- /Articles/Symmetry-less-1D-bin-packing/readme.md: -------------------------------------------------------------------------------- 1 | ## Symmetry-less 1D bin packing 2 | Recently we were working on a small one-dimensional bin packing model. The situation was simple, and we expected the model to be easy to solve. But there was just one problem: we couldn't find an optimal solution, even after letting the solver run overnight for 12 hours. 3 | 4 | Initially, we were using the CBC solver. Since that didn't work, we tried CPLEX via NEOS, but we encountered the same problem – CPLEX couldn't find an optimal solution either. 5 | 6 | So, we searched the Operations Research literature for an alternative formulation. We discovered a recently published academic paper that has a new, innovative formulation for one-dimensional bin packing (and potentially other types of packing situations). 7 | 8 | This article describes the new formulation and our experience applying it to our simple, yet difficult to solve, model. 9 | 10 | Blog article: [Symmetry-less 1D bin packing](https://www.solvermax.com/blog/symmetry-less-1d-bin-packing) 11 | -------------------------------------------------------------------------------- /Articles/Symmetry-less-1D-bin-packing/symmetrylessbinpacking.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Symmetry-less-1D-bin-packing/symmetrylessbinpacking.xlsx -------------------------------------------------------------------------------- /Articles/Taking-a-dip-in-the-MIP-solution-pool/cplex-pool-subsetsum.opt: -------------------------------------------------------------------------------- 1 | set timelimit 600 2 | set mip pool intensity 4 3 | set mip pool capacity 20 4 | set mip pool replace 1 5 | set mip pool absgap 0.015 6 | -------------------------------------------------------------------------------- /Articles/Taking-a-dip-in-the-MIP-solution-pool/cplex-write-pool-subsetsum.txt: -------------------------------------------------------------------------------- 1 | populate 2 | write pool.sol all -------------------------------------------------------------------------------- /Articles/Taking-a-dip-in-the-MIP-solution-pool/model-subsetsum.lp: -------------------------------------------------------------------------------- 1 | \ Model solved using the solver: CBC 2 | \ Model for sheet Model 1 3 | \ Model has 4 Excel constraints giving 7 constraint rows and 31 variables. 4 | MINIMIZE 5 | Obj: +0 B46 +0 B47 +0 B48 +0 B49 +0 B50 +0 B51 +0 B52 +0 B53 +0 B54 +0 B55 +0 B56 +0 B57 +0 B58 +0 B59 +0 B60 +0 B61 +0 B62 +0 B63 +0 B64 +0 B65 +0 B66 +0 B67 +0 B68 +0 B69 +0 B70 +0 B71 +0 B72 +0 B73 +0 B74 +0 B75 -1 K46 6 | SUBJECT TO 7 | \ J46 <= fUB 8 | -181.18 B46 -491.53 B47 -50.51 B48 -156.07 B49 -442.04 B50 -333.35 B51 -207.55 B52 -265.69 B53 -165.44 B54 -446.05 B55 -127.83 B56 -260.88 B57 -486.72 B58 -27.38 B59 -195.39 B60 -316.57 B61 -361.21 B62 -310.48 B63 -390.84 B64 -20.8499999999999 B65 -195.09 B66 -370.36 B67 -299.13 B68 -437.2 B69 -382.01 B70 -52.29 B71 -127.17 B72 -200.83 B73 -222 B74 -340.38 B75 +1 K46 <= -1048.67 9 | \ J46 >= vLB 10 | -181.18 B46 -491.53 B47 -50.51 B48 -156.07 B49 -442.04 B50 -333.35 B51 -207.55 B52 -265.69 B53 -165.44 B54 -446.05 B55 -127.83 B56 -260.88 B57 -486.72 B58 -27.38 B59 -195.39 B60 -316.57 B61 -361.21 B62 -310.48 B63 -390.84 B64 -20.8499999999999 B65 -195.09 B66 -370.36 B67 -299.13 B68 -437.2 B69 -382.01 B70 -52.29 B71 -127.17 B72 -200.83 B73 -222 B74 -340.38 B75 -1 K46 >= -1048.67 11 | \ I50:M50 <= fAllowed 12 | \ (A row with all zero coeffs) <= +5 13 | \ (A row with all zero coeffs) <= +4 14 | \ (A row with all zero coeffs) <= +5 15 | \ (A row with all zero coeffs) <= +5 16 | \ (A row with all zero coeffs) <= +4 17 | 18 | BOUNDS 19 | 20 | \'Assume Non Negative' is FALSE, so default lower bounds of zero are removed from all non-binary variables. 21 | K46 FREE 22 | 23 | BINARY 24 | B46 B47 B48 B49 B50 B51 B52 B53 B54 B55 B56 B57 B58 B59 B60 B61 B62 B63 B64 B65 B66 B67 B68 B69 B70 B71 B72 B73 B74 B75 25 | END 26 | -------------------------------------------------------------------------------- /Articles/Taking-a-dip-in-the-MIP-solution-pool/readme.md: -------------------------------------------------------------------------------- 1 | ## Taking a dip in the MIP solution pool 2 | We're frequently asked questions like: "How many other optimal solutions exist?" and "How do I find those solutions?". Often these questions are prompted by our mentioning that most models have alternative optima – that is, optimal solutions with the same objective function value, but different variable values. 3 | 4 | Although a model may have a unique optimal solution, models with integer/binary variables typically have multiple optimal solutions, and continuous linear models may have an infinite number of alternative optimal solutions. The likely existence of multiple alternative optima is why we usually say "an optimal solution", rather than saying "the optimal solution". 5 | 6 | Sometimes people also ask, "How do I find solutions that are almost optimal?". This question typically indicates that the decision maker may accept a sub-optimal solution (or an alternative optimal solution) that is "better" according to some criteria that aren't captured by the model design. Of course, we should look at incorporating the unspecified criteria within the model, but sometimes that is difficult or even impossible. In any case, exploring the solution space around the optimal solution is an important part of the modelling process. 7 | 8 | This article describes methods for finding alternative optima and solutions that are almost optimal. Specifically, we explore the CPLEX "solution pool" feature, which NEOS Server has recently made available through their online portal. 9 | 10 | Blog article: [Taking a dip in the MIP solution pool](https://www.solvermax.com/blog/taking-a-dip-in-the-mip-solution-pool) 11 | -------------------------------------------------------------------------------- /Articles/Taking-a-dip-in-the-MIP-solution-pool/subsetsum.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Taking-a-dip-in-the-MIP-solution-pool/subsetsum.xlsx -------------------------------------------------------------------------------- /Articles/Vaccination-plan-for-Hong-Kong/readme.md: -------------------------------------------------------------------------------- 1 | ## Vaccination plan for Hong Kong 2 | In this article we replicate an academic paper's model formulation of a [COVID-19 vaccination plan for Hong Kong](https://researchinfotext.com/ropen/attachments/articles/pdfs/259006COVID20-5050.pdf). 3 | 4 | The model represents the supply of, and demand for, vaccine doses as a transportation problem, with doses "transported" from month-to-month given a storage cost rate. The objective is to minimize the total storage cost, while matching monthly supply and demand. 5 | 6 | The paper's author solves the model using Excel and Solver. We do the same, though we also use OpenSolver – to see how it behaves differently. To incentivize the model to produce an intuitively better solution, we extend the model to include an escalating cost over time. 7 | 8 | Blog article: [Vaccination plan for Hong Kong](https://www.solvermax.com/blog/vaccination-plan-for-hong-kong) 9 | -------------------------------------------------------------------------------- /Articles/Vaccination-plan-for-Hong-Kong/vaccineplanning.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Vaccination-plan-for-Hong-Kong/vaccineplanning.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/Frequency-full.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Warehouse-optimization/Frequency-full.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/Frequency-small.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Warehouse-optimization/Frequency-small.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/Layout-A.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Warehouse-optimization/Layout-A.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/Layout-B.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Warehouse-optimization/Layout-B.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/Layout-C.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Warehouse-optimization/Layout-C.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/Layout-D.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Warehouse-optimization/Layout-D.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/Layout-E.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Warehouse-optimization/Layout-E.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/Layout-F.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Warehouse-optimization/Layout-F.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/Layout-G.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Warehouse-optimization/Layout-G.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/Layout-H.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Warehouse-optimization/Layout-H.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/Layout-I.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Warehouse-optimization/Layout-I.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/Layout-small.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/Warehouse-optimization/Layout-small.xlsx -------------------------------------------------------------------------------- /Articles/Warehouse-optimization/readme.md: -------------------------------------------------------------------------------- 1 | ## Warehouse optimization 2 | In this article series we explore optimization of a "picking warehouse" – that is, a warehouse where we pick items off the shelves to fulfil a customer order. 3 | As a result, we improve the efficiency of our warehouse by a factor of three. 4 | 5 | Specifically, we model the efficiency of: 6 | - A variety of shelving layout designs. 7 | - Positioning items on shelves according to order probability. 8 | - Combining orders to achieve economies of scale. 9 | 10 | The model is built in Python, using the OR-Tools and networkx libraries.. 11 | 12 | First blog article in the series: https://www.solvermax.com/blog/warehouse-optimization-model-design 13 | -------------------------------------------------------------------------------- /Articles/We-need-more-power-NEOS-Server/equalteams.xlsm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SolverMax/Blog/8987a50b493c1a0d9fe559b35dc632fa1cb8c762/Articles/We-need-more-power-NEOS-Server/equalteams.xlsm -------------------------------------------------------------------------------- /Articles/We-need-more-power-NEOS-Server/readme.md: -------------------------------------------------------------------------------- 1 | ## We need more power: NEOS Server 2 | OpenSolver uses the free, open-source CBC solver. For most linear models, CBC is good enough. But sometimes CBC struggles to solve a model in a reasonable time. That usually happens when the model has a large number of variables or constraints, though some small models can also be difficult to solve. 3 | 4 | When CBC doesn't get the job done, we can try using a more powerful solver. One way to apply more power is to use the NEOS Server, which is an online service that provides access to many different solvers, including commercial solvers, for free. 5 | 6 | This article describes an example of how we can solve a model using the CPLEX solver via the NEOS Server. 7 | 8 | Blog article: [We need more power: NEOS Server](https://www.solvermax.com/blog/we-need-more-power-neos-server) 9 | -------------------------------------------------------------------------------- /Articles/Wrong-hill/components/imports.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "4cecbb7a-e309-4916-b4d0-f1e3f44f4747", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# Dependencies\n", 11 | "\n", 12 | "import pyomo.environ as pyo\n", 13 | "import numpy as np\n", 14 | "import os.path\n", 15 | "import logging\n", 16 | "logging.getLogger('pyomo.core').setLevel(logging.ERROR) # Suppress warnings" 17 | ] 18 | } 19 | ], 20 | "metadata": { 21 | "kernelspec": { 22 | "display_name": "Python 3 (ipykernel)", 23 | "language": "python", 24 | "name": "python3" 25 | }, 26 | "language_info": { 27 | "codemirror_mode": { 28 | "name": "ipython", 29 | "version": 3 30 | }, 31 | "file_extension": ".py", 32 | "mimetype": "text/x-python", 33 | "name": "python", 34 | "nbconvert_exporter": "python", 35 | "pygments_lexer": "ipython3", 36 | "version": "3.9.13" 37 | } 38 | }, 39 | "nbformat": 4, 40 | "nbformat_minor": 5 41 | } 42 | -------------------------------------------------------------------------------- /Articles/Wrong-hill/components/objective-functions.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "d4f1b8cb-83b6-48e8-b4e7-f902d7b0bb4a", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# Define variable and the objective function choices\n", 11 | "\n", 12 | "def DefineFunction(Model, Function):\n", 13 | " # Set bounds on x\n", 14 | " Model.xMin = 0\n", 15 | " Model.xMax = 8\n", 16 | "\n", 17 | " Model.x = pyo.Var(domain = pyo.Reals, bounds = (Model.xMin, Model.xMax), initialize = 0)\n", 18 | "\n", 19 | " # Define objective function choices\n", 20 | " if Function == 1:\n", 21 | " z = 1.5 * Model.x + 1 # Linear\n", 22 | " elif Function == 2:\n", 23 | " z = 1 * pyo.exp(-((Model.x - 2)**2)) # Non-linear bump\n", 24 | " else:\n", 25 | " Height = [-2.000, 2.000, 1.750, -1.000] # Negative height indicates a dip\n", 26 | " Position = [ 1.000, 2.750, 5.000, 7.000]\n", 27 | " z = 0\n", 28 | " for t in range(0, len(Position)):\n", 29 | " z += Height[t] * pyo.exp(-((Model.x - Position[t])**2)) # Non-linear bumps and dips\n", 30 | " \n", 31 | " Model.Obj = pyo.Objective(expr = z, sense = pyo.maximize)\n", 32 | " \n", 33 | " if Model.Obj.sense == 1: # Minimize\n", 34 | " Model.BestObj = np.inf # Initialize best objective function value found\n", 35 | " else: # Maximize\n", 36 | " Model.BestObj = -np.inf\n", 37 | "\n", 38 | " return Model " 39 | ] 40 | } 41 | ], 42 | "metadata": { 43 | "kernelspec": { 44 | "display_name": "Python 3 (ipykernel)", 45 | "language": "python", 46 | "name": "python3" 47 | }, 48 | "language_info": { 49 | "codemirror_mode": { 50 | "name": "ipython", 51 | "version": 3 52 | }, 53 | "file_extension": ".py", 54 | "mimetype": "text/x-python", 55 | "name": "python", 56 | "nbconvert_exporter": "python", 57 | "pygments_lexer": "ipython3", 58 | "version": "3.9.13" 59 | } 60 | }, 61 | "nbformat": 4, 62 | "nbformat_minor": 5 63 | } 64 | -------------------------------------------------------------------------------- /Articles/Wrong-hill/components/readme.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Articles/Wrong-hill/components/solver-4.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "526984e9-cf4e-4916-90ab-bcebdc58af50", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Create the Solver object for a local solver\n", 17 | "def SetUpSolver():\n", 18 | " Solver = pyo.SolverFactory('multistart')\n", 19 | " return Solver" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 2, 25 | "id": "f6d847fb-073f-446b-9bd8-085117c49067", 26 | "metadata": { 27 | "editable": true, 28 | "slideshow": { 29 | "slide_type": "" 30 | }, 31 | "tags": [] 32 | }, 33 | "outputs": [], 34 | "source": [ 35 | "# Call a local solver using multi-start\n", 36 | "def CallSolver(Solver, Model):\n", 37 | " Results = Solver.solve(Model, solver = Model.Engine, suppress_unbounded_warning = True, iterations = Model.Repeats)\n", 38 | " return Results, Model" 39 | ] 40 | } 41 | ], 42 | "metadata": { 43 | "kernelspec": { 44 | "display_name": "Python 3 (ipykernel)", 45 | "language": "python", 46 | "name": "python3" 47 | }, 48 | "language_info": { 49 | "codemirror_mode": { 50 | "name": "ipython", 51 | "version": 3 52 | }, 53 | "file_extension": ".py", 54 | "mimetype": "text/x-python", 55 | "name": "python", 56 | "nbconvert_exporter": "python", 57 | "pygments_lexer": "ipython3", 58 | "version": "3.9.13" 59 | } 60 | }, 61 | "nbformat": 4, 62 | "nbformat_minor": 5 63 | } 64 | -------------------------------------------------------------------------------- /Articles/Wrong-hill/components/solver.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "526984e9-cf4e-4916-90ab-bcebdc58af50", 7 | "metadata": { 8 | "editable": true, 9 | "slideshow": { 10 | "slide_type": "" 11 | }, 12 | "tags": [] 13 | }, 14 | "outputs": [], 15 | "source": [ 16 | "# Create the Solver object for a local solver\n", 17 | "def SetUpSolver(Model):\n", 18 | " Solver = pyo.SolverFactory(pyo.value(Model.Engine)) # Local solver installed\n", 19 | " return Solver, Model" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 2, 25 | "id": "f6d847fb-073f-446b-9bd8-085117c49067", 26 | "metadata": { 27 | "editable": true, 28 | "slideshow": { 29 | "slide_type": "" 30 | }, 31 | "tags": [] 32 | }, 33 | "outputs": [], 34 | "source": [ 35 | "# Call a local solver\n", 36 | "def CallSolver(Solver, Model):\n", 37 | " Results = Solver.solve(Model, load_solutions = True, tee = False)\n", 38 | " return Results, Model" 39 | ] 40 | } 41 | ], 42 | "metadata": { 43 | "kernelspec": { 44 | "display_name": "Python 3 (ipykernel)", 45 | "language": "python", 46 | "name": "python3" 47 | }, 48 | "language_info": { 49 | "codemirror_mode": { 50 | "name": "ipython", 51 | "version": 3 52 | }, 53 | "file_extension": ".py", 54 | "mimetype": "text/x-python", 55 | "name": "python", 56 | "nbconvert_exporter": "python", 57 | "pygments_lexer": "ipython3", 58 | "version": "3.9.13" 59 | } 60 | }, 61 | "nbformat": 4, 62 | "nbformat_minor": 5 63 | } 64 | -------------------------------------------------------------------------------- /Articles/Wrong-hill/readme.md: -------------------------------------------------------------------------------- 1 | ## Running up the wrong hill 2 | We explore some aspects of solver behaviour when solving non-linear optimization models. 3 | 4 | Our goal is to provide insights into what the solvers are doing, why they may find different solutions, and how we can improve our chances of finding at least a good, and hopefully a globally optimal, solution. 5 | 6 | The model is built in Python using Pyomo. 7 | 8 | Blog article: https://www.solvermax.com/blog/running-up-the-wrong-hill 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blog 2 | 3 | Our blog describes a variety of optimization models, written in either Excel or Python. 4 | 5 | Blog topics include: 6 | - Optimization model examples with detailed explanations. 7 | - Projects we've worked on recently (anonymized, of course). 8 | - Links to posts by other optimization bloggers. 9 | - Interesting developments in the optimization field. 10 | - Updates to relevant software. 11 | - Anything else relating to optimization modelling that grabs our attention. 12 | 13 | The code for our optimization models, written in Excel or Python, is available here. 14 | --------------------------------------------------------------------------------