Skip to content

Commit

Permalink
[docs] update multi.jl tutorial to use an SQLite database (#3098)
Browse files Browse the repository at this point in the history
  • Loading branch information
odow authored Sep 30, 2022
1 parent 771922c commit 2b2c321
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 102 deletions.
4 changes: 4 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -33,4 +35,6 @@ Literate = "2.8"
MathOptInterface = "=1.8.2"
Plots = "1"
SCS = "=1.1.2"
SQLite = "1"
StatsPlots = "0.15"
Tables = "1"
276 changes: 174 additions & 102 deletions docs/src/tutorials/linear/multi.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Binary file added docs/src/tutorials/linear/multi.sqlite
Binary file not shown.

0 comments on commit 2b2c321

Please sign in to comment.