Skip to content

Commit 75d320b

Browse files
committed
import spanning tree code from old repository
1 parent 0f5590e commit 75d320b

11 files changed

Lines changed: 944 additions & 0 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Problem statement
2+
3+
We consider a two-stage stochastic variant of the classic [minimum spanning tree problem](https://en.wikipedia.org/wiki/Minimum_spanning_tree).
4+
5+
Rather than immediately constructing a spanning tree and incurring a cost ``c_e`` for each selected edge in the tree, we instead can build only a partial tree (forest) during the first stage and paying first stage costs ``c_e`` for the selected edges. Then, second stage costs ``d_e`` are revealed and replace first stage costs. The task then involves completing the first stage forest into a spanning tree.
6+
7+
The objective is to minimize the total incurred cost in expectation.
8+
9+
## Instance
10+
Let ``G = (V,E)`` be an undirected **graph**, and ``S`` be a finite set of **scenarios**.
11+
12+
For each edge ``e`` in ``E``, we have a **first stage cost** ``c_e\in\mathbb{R}``.
13+
14+
For each edge ``e`` in ``E`` and scenario ``s`` in ``S``, we have a **second stage cost** ``d_{es}\in\mathbb{R}``.
15+
16+
# MIP formulation
17+
Unlike the regular minimum spanning tree problem, this two-stage variant is NP-hard.
18+
However, it can still be formulated as linear program with binary variables, and exponential number of constraints.
19+
```math
20+
\begin{array}{lll}
21+
\min\limits_{y, z}\, & \sum\limits_{e\in E}c_e y_e + \frac{1}{|S|}\sum\limits_{s \in S}d_{es}z_{es} & \\
22+
\mathrm{s.t.}\, & \sum\limits_{e\in E}y_e + z_{es} = |V| - 1, & \forall s \in S\\
23+
& \sum\limits_{e\in E(Y)} y_e + z_{es} \leq |Y| - 1,\quad & \forall \emptyset \subsetneq Y \subsetneq V,\, \forall s\in S\\
24+
& y_e\in \{0, 1\}, & \forall e\in E\\
25+
& z_{es}\in \{0, 1\}, & \forall e\in E, \forall s\in S
26+
\end{array}
27+
```
28+
where ``y_e`` is a binary variable indicating if ``e`` is in the first stage solution, and ``z_{es}`` is a binary variable indicating if ``e`` is in the second stage solution for scenario ``s``.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
$TYPEDSIGNATURES
3+
4+
Compute an anticipative solution for given scenario.
5+
"""
6+
function anticipative_solution(instance::TwoStageSpanningTreeInstance, scenario::Int=1)
7+
(; graph, first_stage_costs, second_stage_costs) = instance
8+
scenario_second_stage_costs = @view second_stage_costs[:, scenario]
9+
weights = min.(first_stage_costs, scenario_second_stage_costs)
10+
(; value, tree) = kruskal(graph, weights)
11+
12+
slice = first_stage_costs .<= scenario_second_stage_costs
13+
y = tree[slice]
14+
z = tree[.!slice]
15+
return (; value, y, z)
16+
end
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
function separate_benders_cut(instance::TwoStageSpanningTreeInstance, y, s; MILP_solver, tol=1e-5)
2+
(; graph, second_stage_costs) = instance
3+
4+
E = ne(graph)
5+
6+
columns = BitVector[]
7+
8+
# Feasibility cut
9+
model = Model(MILP_solver)
10+
11+
@variable(model, dummy, Bin)
12+
13+
@variable(model, νₛ <= 1)
14+
@variable(model, 0 <= μₛ[e in 1:E] <= 1)
15+
16+
@objective(model, Max, νₛ + sum(y[e] * μₛ[e] for e in 1:E))
17+
18+
function feasibility_callback(cb_data)
19+
μ_val = callback_value.(cb_data, μₛ)
20+
ν_val = callback_value(cb_data, νₛ)
21+
22+
weights = -μ_val
23+
val, tree = kruskal(graph, weights)
24+
25+
push!(columns, tree)
26+
27+
if val + tol < ν_val
28+
new_constraint = @build_constraint(
29+
- sum(μₛ[e] for e in 1:E if tree[e]) - νₛ >= 0
30+
)
31+
MOI.submit(
32+
model, MOI.LazyConstraint(cb_data), new_constraint
33+
)
34+
end
35+
end
36+
37+
set_attribute(model, MOI.LazyConstraintCallback(), feasibility_callback)
38+
optimize!(model)
39+
40+
if objective_value(model) > tol
41+
return false, value.(νₛ), value.(μₛ), objective_value(model)
42+
end
43+
44+
# Else, optimality cut
45+
optimality_model = Model(MILP_solver)
46+
47+
@variable(optimality_model, dummy, Bin)
48+
49+
@variable(optimality_model, νₛ)
50+
@variable(optimality_model, μₛ[e in 1:E] >= 0)
51+
52+
@objective(
53+
optimality_model, Max,
54+
νₛ + sum(y[e] * μₛ[e] for e in 1:E) - sum(second_stage_costs[e, s] * y[e] for e in 1:E)
55+
)
56+
57+
for tree in columns
58+
@constraint(
59+
optimality_model,
60+
sum(second_stage_costs[e, s] - μₛ[e] for e in 1:E if tree[e]) >= νₛ
61+
)
62+
end
63+
64+
function my_callback_function(cb_data)
65+
μ_val = callback_value.(cb_data, μₛ)
66+
ν_val = callback_value(cb_data, νₛ)
67+
68+
weights = second_stage_costs[:, s] .- μ_val
69+
70+
val, tree = kruskal(graph, weights)
71+
72+
if val - ν_val + tol < 0
73+
new_constraint = @build_constraint(
74+
sum(second_stage_costs[e, s] - μₛ[e] for e in 1:E if tree[e]) >= νₛ
75+
)
76+
MOI.submit(
77+
optimality_model, MOI.LazyConstraint(cb_data), new_constraint
78+
)
79+
end
80+
end
81+
82+
set_attribute(optimality_model, MOI.LazyConstraintCallback(), my_callback_function)
83+
84+
optimize!(optimality_model)
85+
86+
# If primal feasible, add an optimality cut
87+
@assert termination_status(optimality_model) != DUAL_INFEASIBLE
88+
return true, value.(νₛ), value.(μₛ), objective_value(optimality_model)
89+
end
90+
91+
"""
92+
$TYPEDSIGNATURES
93+
94+
Returns the optimal solution using a Benders decomposition algorithm.
95+
"""
96+
function benders_decomposition(
97+
instance::TwoStageSpanningTreeInstance;
98+
MILP_solver=GLPK.Optimizer,
99+
tol=1e-6,
100+
verbose=true
101+
)
102+
(; graph, first_stage_costs, second_stage_costs) = instance
103+
E = ne(graph)
104+
S = nb_scenarios(instance)
105+
106+
model = Model(MILP_solver)
107+
@variable(model, y[e in 1:E], Bin)
108+
@variable(
109+
model,
110+
θ[s in 1:S] >= sum(min(0, second_stage_costs[e, s]) for e in 1:E)
111+
)
112+
@objective(
113+
model,
114+
Min,
115+
sum(first_stage_costs[e] * y[e] for e in 1:E) + sum(θ[s] for s in 1:S) / S
116+
)
117+
118+
# current_scenario = 0
119+
callback_counter = 0
120+
function benders_callback(cb_data)
121+
if callback_counter % 10 == 0
122+
verbose && @info("Benders iteration: $(callback_counter)")
123+
end
124+
callback_counter += 1
125+
126+
y_val = callback_value.(cb_data, y)
127+
θ_val = callback_value.(cb_data, θ)
128+
129+
for current_scenario in 1:S
130+
optimality_cut, ν_val, μ_val =
131+
separate_benders_cut(instance, y_val, current_scenario; MILP_solver)
132+
133+
# If feasibility cut
134+
if !optimality_cut
135+
new_feasibility_cut = @build_constraint(
136+
ν_val + sum(μ_val[e] * y[e] for e in 1:E) <= 0
137+
)
138+
MOI.submit(
139+
model,
140+
MOI.LazyConstraint(cb_data),
141+
new_feasibility_cut
142+
)
143+
144+
return nothing
145+
end
146+
147+
# Else, optimality cut
148+
if θ_val[current_scenario] + tol < ν_val + sum(μ_val[e] * y_val[e] for e in 1:E) -
149+
sum(second_stage_costs[e, current_scenario] * y_val[e] for e in 1:E)
150+
con = @build_constraint(
151+
θ[current_scenario] >=
152+
ν_val + sum(μ_val[e] * y[e] for e in 1:E) - sum(second_stage_costs[e, current_scenario] * y[e] for e in 1:E)
153+
)
154+
MOI.submit(model, MOI.LazyConstraint(cb_data), con)
155+
return nothing
156+
end
157+
end
158+
end
159+
160+
set_attribute(model, MOI.LazyConstraintCallback(), benders_callback)
161+
optimize!(model)
162+
163+
return solution_from_first_stage_forest(value.(y) .> 0.5, instance)
164+
end
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""
2+
$TYPEDSIGNATURES
3+
4+
Solves the linear relaxation using a column generation algorithm.
5+
"""
6+
function column_generation(instance; MILP_solver=GLPK.Optimizer, tol=1e-6, verbose=true)
7+
(; graph, first_stage_costs, second_stage_costs) = instance
8+
9+
V = nv(graph)
10+
E = ne(graph)
11+
S = nb_scenarios(instance)
12+
13+
model = Model(MILP_solver)
14+
15+
@variable(model, dummy, Bin) # dummy binary variable to activate callbacks
16+
17+
@variable(model, ν[s in 1:S])
18+
@variable(model, μ[e in 1:E, s in 1:S])
19+
20+
@objective(model, Max, sum(ν[s] for s in 1:S))
21+
22+
@constraint(
23+
model, [e in 1:E, s in 1:S],
24+
second_stage_costs[e, s] / S - μ[e, s] >= 0
25+
)
26+
27+
@constraint(
28+
model, [e in 1:E],
29+
first_stage_costs[e] - sum(μ[e, s] for s in 1:S) >= 0
30+
)
31+
32+
trees = BitVector[]
33+
34+
for s in 1:S
35+
_, dummy_tree = kruskal(graph, min.(first_stage_costs, second_stage_costs[:, s]))
36+
@constraint(
37+
model,
38+
ν[s] <= sum(μ[e, s] for e in 1:E if dummy_tree[e])
39+
)
40+
push!(trees, dummy_tree)
41+
end
42+
43+
callback_counter = 0
44+
function my_callback_function(cb_data)
45+
callback_counter += 1
46+
47+
ν_val = callback_value.(cb_data, ν)
48+
μ_val = callback_value.(cb_data, μ)
49+
50+
for s in 1:S
51+
val, T = kruskal(graph, @view μ_val[:, s])
52+
53+
if val + tol < ν_val[s]
54+
push!(trees, T)
55+
new_constraint = @build_constraint(
56+
ν[s] <= sum(μ[e, s] for e in 1:E if T[e])
57+
)
58+
MOI.submit(
59+
model, MOI.LazyConstraint(cb_data), new_constraint
60+
)
61+
end
62+
end
63+
end
64+
65+
set_attribute(model, MOI.LazyConstraintCallback(), my_callback_function)
66+
67+
optimize!(model)
68+
verbose && @info "Optimal solution found after $callback_counter cuts"
69+
return (; value=objective_value(model), ν=value.(ν), μ=value.(μ), columns=trees)
70+
end
71+
72+
function column_heuristic(instance, columns; MILP_solver=GLPK.Optimizer)
73+
(; graph, first_stage_costs, second_stage_costs) = instance
74+
E = ne(graph)
75+
S = nb_scenarios(instance)
76+
T = length(columns)
77+
78+
model = Model(MILP_solver)
79+
80+
@variable(model, y[e in 1:E], Bin)
81+
@variable(model, z[e in 1:E, s in 1:S], Bin)
82+
83+
@variable(model, λ[t in 1:T, s in 1:S], Bin)
84+
85+
@objective(
86+
model, Min,
87+
sum(first_stage_costs[e] * y[e] for e in 1:E) + sum(second_stage_costs[e, s] * z[e, s] for e in 1:E for s in 1:S) / S
88+
)
89+
90+
@constraint(model, [s in 1:S], sum(λ[t, s] for t in 1:T) == 1)
91+
@constraint(
92+
model, [e in 1:E, s in 1:S],
93+
y[e] + z[e, s] == sum(λ[t, s] for t in 1:T if columns[t][e])
94+
)
95+
96+
optimize!(model)
97+
98+
return TwoStageSpanningTreeSolution(value.(y) .> 0.5, value.(z) .> 0.5)
99+
end
100+
101+
"""
102+
$TYPEDSIGNATURES
103+
104+
Column generation heuristic, that solves the linear relaxation and then outputs the solution of the proble restricted to selected columns.
105+
Returns an heuristic solution.
106+
"""
107+
function column_heuristic(instance; MILP_solver=GLPK.Optimizer, verbose=true)
108+
(; columns) = column_generation(instance; verbose=false, MILP_solver)
109+
verbose && @info "Continuous relaxation solved with $(length(columns)) columns."
110+
return column_heuristic(instance, columns)
111+
end

0 commit comments

Comments
 (0)