diff --git a/docs/Project.toml b/docs/Project.toml index 2af2308b226..593830b0ca2 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -15,7 +15,9 @@ MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SCS = "c946c3f1-0d1f-5ce8-9dea-7daa1f7e2d13" +SQLite = "0aa819cd-b072-5ff4-a722-6bc24af294d9" StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd" +Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] @@ -33,4 +35,6 @@ Literate = "2.8" MathOptInterface = "=1.8.2" Plots = "1" SCS = "=1.1.2" +SQLite = "1" StatsPlots = "0.15" +Tables = "1" diff --git a/docs/src/tutorials/linear/multi.jl b/docs/src/tutorials/linear/multi.jl index 4ba27f77c3c..a9691b71fd4 100644 --- a/docs/src/tutorials/linear/multi.jl +++ b/docs/src/tutorials/linear/multi.jl @@ -5,110 +5,182 @@ # # The multi-commodity flow problem -# JuMP implementation of the multi-commodity transportation model AMPL: A -# Modeling Language for Mathematical Programming, 2nd ed by Robert Fourer, David -# Gay, and Brian W. Kernighan 4-1. -# -# Originally contributed by Louis Luangkesorn, February 26, 2015. +# **Originally contributed by:** Louis Luangkesorn + +# This tutorial is a JuMP implementation of the multi-commodity transportation +# model described in +# [_AMPL: A Modeling Language for Mathematical Programming_](https://ampl.com/resources/the-ampl-book/), +# by R. Fourer, D.M. Gay and B.W. Kernighan. + +# The purpose of this tutorial is to demonstrate creating a JuMP model from an +# SQLite database. + +# ## Required packages + +# This tutorial uses the following packages using JuMP +import DataFrames import HiGHS -import Test - -function example_multi(; verbose = true) - orig = ["GARY", "CLEV", "PITT"] - dest = ["FRA", "DET", "LAN", "WIN", "STL", "FRE", "LAF"] - prod = ["bands", "coils", "plate"] - numorig = length(orig) - numdest = length(dest) - numprod = length(prod) - ## supply(prod, orig) amounts available at origins - supply = [ - 400 700 800 - 800 1600 1800 - 200 300 300 - ] - ## demand(prod, dest) amounts required at destinations - demand = [ - 300 300 100 75 650 225 250 - 500 750 400 250 950 850 500 - 100 100 0 50 200 100 250 - ] - ## limit(orig, dest) of total units from any origin to destination - limit = [625.0 for j in 1:numorig, i in 1:numdest] - ## cost(dest, orig, prod) Shipment cost per unit - cost = reshape( - [ - [ - [30, 10, 8, 10, 11, 71, 6] - [22, 7, 10, 7, 21, 82, 13] - [19, 11, 12, 10, 25, 83, 15] - ] - [ - [39, 14, 11, 14, 16, 82, 8] - [27, 9, 12, 9, 26, 95, 17] - [24, 14, 17, 13, 28, 99, 20] - ] - [ - [41, 15, 12, 16, 17, 86, 8] - [29, 9, 13, 9, 28, 99, 18] - [26, 14, 17, 13, 31, 104, 20] - ] - ], - 7, - 3, - 3, - ) - ## DECLARE MODEL - multi = Model(HiGHS.Optimizer) - ## VARIABLES - @variable(multi, trans[1:numorig, 1:numdest, 1:numprod] >= 0) - ## OBJECTIVE - @objective( - multi, - Max, - sum( - cost[j, i, p] * trans[i, j, p] for i in 1:numorig, j in 1:numdest, - p in 1:numprod - ) - ) - ## CONSTRAINTS - ## Supply constraint - @constraint( - multi, - supply_con[i in 1:numorig, p in 1:numprod], - sum(trans[i, j, p] for j in 1:numdest) == supply[p, i] - ) - ## Demand constraint - @constraint( - multi, - demand_con[j in 1:numdest, p in 1:numprod], - sum(trans[i, j, p] for i in 1:numorig) == demand[p, j] - ) - ## Total shipment constraint - @constraint( - multi, - total_con[i in 1:numorig, j in 1:numdest], - sum(trans[i, j, p] for p in 1:numprod) - limit[i, j] <= 0 - ) - optimize!(multi) - Test.@test termination_status(multi) == OPTIMAL - Test.@test primal_status(multi) == FEASIBLE_POINT - Test.@test objective_value(multi) == 225_700.0 - if verbose - println("RESULTS:") - for i in 1:length(orig) - for j in 1:length(dest) - for p in 1:length(prod) - print( - " $(prod[p]) $(orig[i]) $(dest[j]) = $(value(trans[i, j, p]))\t", - ) - end - println() - end - end - end - return +import SQLite +import Tables +import Test #src + +const DBInterface = SQLite.DBInterface + +# ## Formulation + +# The multi-commondity flow problem is a simple extension of +# [The transportation problem](@ref) to multiple types of products. Briefly, we +# start with the formulation of the transportation problem: +# ```math +# \begin{aligned} +# \min && \sum_{i \in O, j \in D} c_{i,j} x_{i,j} \\ +# s.t. && \sum_{j \in D} x_{i, j} \le s_i && \forall i \in O \\ +# && \sum_{i \in O} x_{i, j} = d_j && \forall j \in D \\ +# && x_{i, j} \ge 0 && \forall i \in O, j \in D +# \end{aligned} +# ``` +# but introduce a set of products ``P``, resulting in: +# ```math +# \begin{aligned} +# \min && \sum_{i \in O, j \in D, k \in P} c_{i,j,k} x_{i,j,k} \\ +# s.t. && \sum_{j \in D} x_{i, j, k} \le s_{i,k} && \forall i \in O, k \in P \\ +# && \sum_{i \in O} x_{i, j, k} = d_{j,k} && \forall j \in D, k \in P \\ +# && x_{i, j,k} \ge 0 && \forall i \in O, j \in D, k \in P \\ +# && \sum_{k \in P} x_{i, j, k} \le u_{i,j} && \forall i \in O, j \in D +# \end{aligned} +# ``` +# Note that the last constraint is new; it says that there is a maximum quantity +# of goods (of any type) that can be transported from origin ``i`` to +# destination ``j``. + +# ## Data + +# For the purpose of this tutorial, the JuMP repository contains an example +# database called `multi.sqlite`: + +db = SQLite.DB(joinpath(@__DIR__, "multi.sqlite")) + +# A quick way to see the schema of the database is via `SQLite.tables`: + +SQLite.tables(db) + +# We interact with the database by executing queries, and then piping the +# results to an appropriate table. One example is a `DataFrame`: + +DBInterface.execute(db, "SELECT * FROM locations") |> DataFrames.DataFrame + +# But other table types are supported, such as `Tables.rowtable`: + +DBInterface.execute(db, "SELECT * FROM locations") |> Tables.rowtable + +# A `rowtable` is a `Vector` of `NamedTuple`s. + +# You can construct more complicated SQL queries: + +origins = + DBInterface.execute( + db, + "SELECT location FROM locations WHERE type = \"origin\"", + ) |> Tables.rowtable + +# But for our purpose, we just want the list of strings: + +origins = map(y -> y.location, origins) + +# We can compose these two operations to get a list of destinations: + +destinations = + DBInterface.execute( + db, + "SELECT location FROM locations WHERE type = \"destination\"", + ) |> + Tables.rowtable |> + x -> map(y -> y.location, x) + +# And a list of products from our `products` table: + +products = + DBInterface.execute(db, "SELECT product FROM products") |> + Tables.rowtable |> + x -> map(y -> y.product, x) + +# ## JuMP formulation + +# We start by creating a model and our decision variables: + +model = Model(HiGHS.Optimizer) +set_silent(model) +@variable(model, x[origins, destinations, products] >= 0) + +# One approach when working with databases is to extract all of the data into a +# Julia datastructure. For example, let's pull the cost table into a DataFrame +# and then construct our objective by iterating over the rows of the DataFrame: + +cost = DBInterface.execute(db, "SELECT * FROM cost") |> DataFrames.DataFrame +@objective( + model, + Max, + sum(r.cost * x[r.origin, r.destination, r.product] for r in eachrow(cost)), +); + +# If we don't want to use a DataFrame, we can use a `Tables.rowtable` instead: + +supply = DBInterface.execute(db, "SELECT * FROM supply") |> Tables.rowtable +for r in supply + @constraint(model, sum(x[r.origin, :, r.product]) <= r.supply) +end + +# Another approach is to execute the query, and then to iterate through the rows +# of the query using `Tables.rows`: + +demand = DBInterface.execute(db, "SELECT * FROM demand") +for r in Tables.rows(demand) + @constraint(model, sum(x[:, r.destination, r.product]) == r.demand) end -example_multi() +# !!! warning +# Iterating through the rows of a query result works by incrementing a +# cursor inside the database. As a consequence, you cannot call +# `Tables.rows` twice on the same query result. + +# The SQLite queries can be arbitrarily complex. For example, here's a query +# which builds every possible origin-destination pair: + +od_pairs = DBInterface.execute( + db, + """ + SELECT a.location as 'origin', + b.location as 'destination' + FROM locations a + INNER JOIN locations b + ON a.type = 'origin' AND b.type = 'destination' + """, +) + +# With a constraint that we cannot send more than 625 units between each pair: + +for r in Tables.rows(od_pairs) + @constraint(model, sum(x[r.origin, r.destination, :]) <= 625) +end + +# ## Solution + +# Finally, we can optimize the model: + +optimize!(model) +Test.@test termination_status(model) == OPTIMAL #src +Test.@test primal_status(model) == FEASIBLE_POINT #src +Test.@test objective_value(model) == 225_700.0 #src +solution_summary(model) + +# and print the solution: + +begin + println(" ", join(products, ' ')) + for o in origins, d in destinations + v = lpad.([round(Int, value(x[o, d, p])) for p in products], 5) + println(o, " ", d, " ", join(replace.(v, " 0" => " . "), " ")) + end +end diff --git a/docs/src/tutorials/linear/multi.sqlite b/docs/src/tutorials/linear/multi.sqlite new file mode 100644 index 00000000000..7750c3ef93a Binary files /dev/null and b/docs/src/tutorials/linear/multi.sqlite differ