# All nine nodes, re-solved here so neither panel can drift from the
# search above. L and G are the two directions a branch can take.
L, G = :le, :ge
paths = Dict(0 => [], 1 => [(1,L,3)], 2 => [(1,L,3),(2,L,1)],
3 => [(1,L,3),(2,G,2)], 4 => [(1,L,3),(2,G,2),(1,L,2)],
5 => [(1,L,3),(2,G,2),(1,L,2),(2,L,2)],
6 => [(1,L,3),(2,G,2),(1,L,2),(2,G,3)],
7 => [(1,L,3),(2,G,2),(1,G,3)], 8 => [(1,G,4)])
node(cuts) = solve_lp((mm, x) -> for (v, s, r) in cuts
s === :le ? @constraint(mm, x[v] <= r) :
@constraint(mm, x[v] >= r)
end)
res = Dict(k => node(v) for (k, v) in paths)
function thirds(v) # 31.667 -> "31 2/3", for a plain string
w, f = floor(Int, v + 1e-9), v - floor(v + 1e-9)
f < 1e-6 && return string(w)
abs(f - 1/3) < 1e-6 && return "$(w)⅓"
abs(f - 2/3) < 1e-6 && return "$(w)⅔"
return string(round(v, digits = 2))
end
function texfrac(v) # 31.667 -> 31\frac{2}{3}, as the slide
w, f = floor(Int, v + 1e-9), v - floor(v + 1e-9)
f < 1e-6 && return string(w)
abs(f - 1/3) < 1e-6 && return string(w) * raw"\frac{1}{3}"
abs(f - 2/3) < 1e-6 && return string(w) * raw"\frac{2}{3}"
return string(round(v, digits = 2))
end
# The nodes are visited in order, so the lower bound at each is the best
# integer solution seen up to and including it, and zero before the first.
integral(r) = !isnan(r.obj) && all(r.x .== round.(r.x)) # snapped above
best(id) = [res[j].obj for j in 0:id if integral(res[j])]
lb = Dict(id => maximum([0.0; best(id)]) for id in 0:8)
# The upper bound a node SHOWS is the one still in force: a node that gets
# branched on shows its own relaxation, and a leaf carries its parent's
# down, because nothing better has been solved when the leaf is reached.
par = Dict(1=>0, 8=>0, 2=>1, 3=>1, 4=>3, 7=>3, 5=>4, 6=>4)
kids = Set(values(par))
ub = Dict{Int,Float64}()
for id in 0:8
ub[id] = id in kids ? res[id].obj : ub[par[id]]
end
gx = Dict(0=>0.0, 1=>-1.35, 8=>1.35, 2=>-2.25, 3=>-0.45, 4=>-1.30,
7=>0.45, 5=>-2.05, 6=>-0.60)
gy = Dict(0=>4.0, 1=>3.0, 8=>3.0, 2=>2.0, 3=>2.0, 4=>1.0, 7=>1.0,
5=>0.0, 6=>0.0)
blab = Dict(1=>L"x_1 \leq 3", 8=>L"x_1 \geq 4", 2=>L"x_2 \leq 1",
3=>L"x_2 \geq 2", 4=>L"x_1 \leq 2", 7=>L"x_1 \geq 3",
5=>L"x_2 \leq 2", 6=>L"x_2 \geq 3")
side = Dict(0=>:right, 1=>:left, 8=>:right, 2=>:left, 3=>:right,
4=>:left, 7=>:right, 5=>:left, 6=>:right)
incumb = Set([2, 5, 6]) # where a new best integer solution lands
fig = Figure(size = (840, 650))
ax = Axis(fig[1, 1]; titlesize = 20,
title = "The bounds close until the gap is under one")
hidedecorations!(ax); hidespines!(ax)
# Wide enough for node 2's caption, and deep enough for the stacked
# fraction in the gap line, which the axis clips if it is not.
limits!(ax, -3.95, 2.75, -1.40, 4.60)
# Centre to centre, and the white-filled circles are drawn over them
# below: the fill hides the overshoot, so every edge meets its node
# exactly, which trimming a fixed amount in y cannot do on an axis
# whose two units are not the same size.
for (kid, pa) in par
lines!(ax, [Point2f(gx[pa], gy[pa]), Point2f(gx[kid], gy[kid])];
color = lattice, linewidth = 1.4)
text!(ax, (gx[pa] + gx[kid]) / 2 + (gx[kid] < gx[pa] ? -0.10 : 0.10),
(gy[pa] + gy[kid]) / 2 + 0.06; text = blab[kid], fontsize = 20,
align = (gx[kid] < gx[pa] ? :right : :left, :bottom),
color = lattice)
end
for id in 0:8
out = isnan(res[id].obj)
col = out ? RGBf(0.55, 0.55, 0.58) : id in incumb ? feasible : relaxed
scatter!(ax, [Point2f(gx[id], gy[id])]; color = :white,
strokecolor = col, strokewidth = 2.2, markersize = 34)
text!(ax, gx[id], gy[id]; text = string(id), color = col,
align = (:center, :center), fontsize = 19)
dx = side[id] === :left ? -0.30 : 0.30
al = side[id] === :left ? :right : :left
if out # a fathomed node has no bounds to show
text!(ax, gx[id] + dx, gy[id]; text = "fathomed,\ninfeasible",
align = (al, :center), color = col, fontsize = 17)
continue
end
u, l = texfrac(ub[id]), texfrac(lb[id])
dy = id in incumb ? 0.13 : 0.0
text!(ax, gx[id] + dx, gy[id] + dy; text = L"UB = %$u, \; LB = %$l",
align = (al, :center), color = lattice, fontsize = 20)
id in incumb &&
text!(ax, gx[id] + dx, gy[id] - 0.22; text = "incumbent",
align = (al, :center), color = feasible, fontsize = 17)
end
text!(ax, gx[0] + 0.30, gy[0] + 0.30; text = "LP", color = relaxed,
align = (:left, :center), fontsize = 19, font = :bold)
g6, l6 = texfrac(ub[6]), texfrac(lb[6])
text!(ax, -0.85, -0.70; align = (:right, :top), color = lattice,
fontsize = 20, text = L"gap = %$g6 - %$l6 < 1 \; \Rightarrow")
text!(ax, -0.78, -0.70; align = (:left, :top), color = feasible,
fontsize = 19, font = :bold, text = "stop")
fig
off = Dict(0 => (0.10, -0.16), 1 => (0.10, 0.10), 2 => (0.10, -0.14),
3 => (0.10, 0.10), 4 => (-0.12, 0.12), 5 => (-0.12, -0.16),
6 => (0.10, 0.10))
fig = Figure(size = (620, 450))
ax = region_axis(fig[1, 1], "Every node is an LP over a smaller region")
for id in 0:6
q = res[id].x
scatter!(ax, [Point2f(q...)]; color = relaxed, markersize = 11)
text!(ax, q[1] + off[id][1], q[2] + off[id][2];
text = L"%$id: \; %$(texfrac(res[id].obj))",
align = (:left, :center), color = relaxed, fontsize = 17)
end
scatter!(ax, [Point2f(ilp.x...)]; color = feasible, markersize = 14,
marker = :diamond)
fig