Copying a JuMP model is slower than building it from scratch

As the title. Then what is the point of the Base.copy(model::AbstractModel) function?

julia> Min_ȷ_fixed_part
Min_ȷ_fixed_part (generic function with 1 method)

julia> t = time(); model = Min_ȷ_fixed_part(); time() - t # build a model with native JuMP code
1.9090001583099365

julia> t = time(); model1 = copy(model); time() - t # copy that model
5.925000190734863

julia> t = time(); model_vec = [Min_ȷ_fixed_part() for _ = 1:3]; time() - t
6.236999988555908

julia> t = time(); model_vec_via_copy = [copy(model) for _ = 1:3]; time() - t
17.314000129699707

I want to create a vector of JuMP.Model’s, they all share a fixed part. And the rest have different data. The better way is to do model_vec = [Min_ȷ_fixed_part() for _ = 1:3]?

Can you provide a complete reproducible example?

It’s a bit lengthy but hopefully it is valuable. It’s a runnable Dantzig-Wolfe/Lagrangian decomposition algorithm (can converges to the Lagrangian dual bound). The relevant line is commented with a # 🍅🍅🍅

Block Decomposition pglib_opf_case3012wp_k
import LinearAlgebra.norm as norm
import LinearAlgebra.⋅ as ⋅
import SparseArrays, PGLib; RawDict = PGLib.pglib("pglib_opf_case3012wp_k.m");
import Random; Random.seed!(1);

macro load_data()
    esc(quote
        T, H = 3, 24;
        daynumvec = [91, 183, 91] # length(~) == T && sum(~) == 365
        # Network's technical data
        (B, N) = length.((RawDict["branch"], RawDict["bus"])); # (3572, 3012)
        BrateA = [RawDict["branch"]["$b"]["rate_a"] for b = 1:B]; # we'll extract a subvector as Fpmaxy0
        N2RawN = collect(1:3013); popat!(N2RawN, 3009); # length(N2RawN) == N != RawN
        bft, A = let # load a rectified bft
            bft = [findfirst(x -> x == RawDict["branch"]["$r"][c == 1 ? "f_bus" : "t_bus"], 
                N2RawN) for r in 1:B, c in 1:2]; # extrema(bft) == (1, N) && size(bft) == (B, 2)
            # Don't execute to save time assert_is_connected(bft)
            rectify_dir!(bft)
            A = bft_2_A(bft)
            bft, A
        end;

        D, D2N, Dref = let
            D = length(RawDict["load"]); # 2271
            D2N = [findfirst(x -> x == RawDict["load"]["$d"]["load_bus"], N2RawN) for d in 1:D];
            @assert length(Set(D2N)) == length(D2N) == D
            @assert D2N == sort(D2N)
            Dref = let
                Dref = [RawDict["load"]["$d"]["pd"] for d in 1:D]; Dref[Dref .< 0.0001] .= 0.0001;
                Dref += rand(-1e-5:1.7e-12:1e-5, D) # disturb!
                Dref[Dref .< 0.0001] *= 10;
                Dref
            end;
            D, D2N, Dref
        end;

        G, G2N, Gpmaxy0, InvG = let
            G_gw = length(RawDict["gen"]) - 6 # we drop 6 gen's due to zero capacity
            Gpmaxy0_gw = [RawDict["gen"]["$g"]["pmax"] for g in 1:G_gw] # capacity for each unaggregated gen
            @assert minimum(Gpmaxy0_gw) == 0.0011
            G_generatorwise2N = [findfirst(x -> x == RawDict["gen"]["$g"]["gen_bus"], N2RawN) for g in 1:G_gw] # length = 496
            G2N = sort(collect(Set(G_generatorwise2N))) # length = 341
            G = length(G2N) # G == G_buswise
            Gpmaxy0 = [sum(Gpmaxy0_gw[findall(x -> x == n, G_generatorwise2N)]) for n in G2N]
            Gpmaxy0 += rand(-1e-4:1.7e-11:1e-4, G)
            InvG = rand(0.5:1.7e-12:0.67, G) .* Gpmaxy0 # newly_added_capacity := InvG[g] * invg[g]::Int
            G, G2N, Gpmaxy0, InvG
        end;

        # Network's simplifications
        DGN2N, BareN2N = D2N ∩ G2N, setdiff(1:N, D2N ∪ G2N);
        DonlyN2N, GonlyN2N = setdiff(D2N, DGN2N), setdiff(G2N, DGN2N); # @assert +(length(BareN2N), length(DonlyN2N), length(GonlyN2N), length(DGN2N)) == N
        n8, b8 = zeros(Int, N), ones(Int, B); # 📗 n8 = let
            # [0]: a normal bus
            # [-2]: a bus with load only, and whose degree is 1, we won't add KCL constr here
            # [-3]: a bus with gen only, and whose degree is 1, we won't add KCL constr here
            # [-4]: invalid bus
        # end; b8 = let 
            # [1]: a normal line
            # [-2]: power flow of this line hinges on the corresponding pure load, we won't allocate pf variable here
            # [-3]: power flow of this line hinges on the corresponding pure gen, we won't allocate pf variable here
            # [-4]: invalid branch
        # end;

        # make minor simplification to the network
        begin # This code removes all bare nodes whose degree = 1, by set!(-4)
            bv, nv, mv = Int[], Int[], Int[]
            for n in BareN2N # depth = 0
                adj_n_vec = adjnode(n)
                length(adj_n_vec) == 0 && error("No here")
                if length(adj_n_vec) == 1
                    m = adj_n_vec[1] # the other node
                    b = get_b(n, m)
                    push!(bv, b)
                    push!(nv, n)
                    m ∈ BareN2N && push!(mv, m) # make provision for the next deeper layer
                end
            end; n8[nv] .= -4; b8[bv] .= -4; # mark as invalid
            asubsetofBareN = copy(mv)
            invalid_node_vec = findall(x -> x == -4, n8)
            bv, nv, mv = Int[], Int[], Int[]
            for n in asubsetofBareN # depth = 1
                adj_n_vec = setdiff(adjnode(n), invalid_node_vec)
                length(adj_n_vec) == 0 && error("No here")
                if length(adj_n_vec) == 1
                    m = adj_n_vec[1] # the other node
                    b = get_b(n, m)
                    push!(bv, b)
                    push!(nv, n)
                    m ∈ BareN2N && push!(mv, m)
                end
            end; n8[nv] .= -4; b8[bv] .= -4; # mark as invalid
        end
        begin # This code marks all Gen-only-degree-1 nodes, by set!(-3)
            bv, nv = Int[], Int[]
            for n in GonlyN2N
                adj_n_vec = filter(x -> n8[x] != -4, adjnode(n))
                length(adj_n_vec) == 0 && error("No here")
                if length(adj_n_vec) == 1
                    m = adj_n_vec[1] # the other node
                    b = get_b(n, m)
                    push!(nv, n)
                    push!(bv, b)
                end
            end; b8[bv] .= -3; n8[nv] .= -3;
        end
        begin # This code marks all Demand-only-degree-1 nodes, by set!(-2)
            bv, nv = Int[], Int[]
            for n in DonlyN2N
                adj_n_vec = filter(x -> n8[x] != -4, adjnode(n))
                length(adj_n_vec) == 0 && error("No here")
                if length(adj_n_vec) == 1
                    m = adj_n_vec[1] # the other node
                    b = get_b(n, m)
                    push!(nv, n)
                    push!(bv, b)
                end
            end; b8[bv] .= -2; n8[nv] .= -2;
        end
        F2B = findall(x -> x == 1, b8); F = length(F2B); # only need F decisions representing pf
        Fpmaxy0 = BrateA[F2B]; # only need Capacity of those (having pf decision) branches
        InvF = rand(0.75:1.7e-12:1.25, F) .* Fpmaxy0
        KCLnodes = findall(x -> x == 0, n8); # only need KCLconstrs at these nodes

        # Network's economical data
        Gphcost, Ginvcost, Finvcost = let
            Gphcost = rand(0.00017:1.7e-12:0.00223, G)
            Ginvcost = rand(117.3:1.3e-10:1115.7, G) .* Gphcost
            M = maximum(Ginvcost)
            Finvcost = rand(0.7M:1.3e-11:1.7M, F)
            Gphcost, Ginvcost, Finvcost
        end

        Y, iY = 30, 5;
        iY2Y = collect(1:(Y ÷ iY):Y); # years that we make investment
        Y2iY = repeat(1:iY; inner = Y ÷ iY); # call `iY = Y2iY[y]`
    end)
end;
function rectify_dir!(bft)
    for r in 1:size(bft, 1)
        if bft[r, 1] > bft[r, 2]
            bft[r, 1], bft[r, 2] = bft[r, 2], bft[r, 1]
        elseif bft[r, 1] == bft[r, 2]
            error("self-branch")
        end
    end
end; function assert_is_connected(bft)
    B = size(bft, 1)
    pb = Vector(1:B) # primal bs
    sn = [1] # subnet
    for ite in 1:B # only du a full iteration as a conservative choice
        progress = false
        for (i, b) in enumerate(pb) # take out branch b
            if bft[b, 1] in sn
                on = bft[b, 2] # the other node
                on ∉ sn && push!(sn, on)
            elseif bft[b, 2] in sn
                on = bft[b, 1] # the other node
                on ∉ sn && push!(sn, on)
            else
                continue
            end
            popat!(pb, i); progress = true; break # fathom branch b
        end
        if progress == false
            error("ite = $ite. All the rest branches are not connected to the subnet being investigated. Check the current subnet")
        end
        1:maximum(bft) ⊆ sn && return  # The graph is proved to be connected
    end
    error("here shouldn't be reached")
end; function bft_2_A(bft)
    B, N = size(bft, 1), maximum(bft)
    return SparseArrays.sparse([Vector(1:B); Vector(1:B)], vec(bft), [-ones(Int, B); ones(Int, B)], B, N)
end; function adjnode(n)
    nv = Int[]
    for ci in findall(x -> x == n, bft)
        b, c = ci.I # row(= branch), col
        on = bft[b, 3 - c]
        on ∉ nv && push!(nv, on)
    end
    sort(nv)
end; function get_b(n, m) # use this after `rectify_dir!(bft)` had been done
    n > m && ((n, m) = (m, n))
    for r in 1:size(bft, 1)
        (bft[r, 1] == n && bft[r, 2] == m) && return r
    end
    error("no such branch exist")
end; function bpf_genonly(b, pgvec)
    n, m = bft[b, :] # bpf dir is defined as n -> m
    n2g = node -> findfirst(x -> x == node, G2N)
    is_source = node -> n8[node] == -3
    if is_source(n)
        is_source(m) && error("No here ioyhbgopiueryt54y")
        return pgvec[n2g(n)]
    elseif is_source(m)
        return -pgvec[n2g(m)]
    else
        error("No here -oiysdg0wieuy62udfh")
    end
end; function bpf_Donly(b, Dvec)
    n, m = bft[b, :] # bpf dir is defined as n -> m
    n2d = node -> findfirst(x -> x == node, D2N)
    is_load = node -> n8[node] == -2
    if is_load(m)
        is_load(n) && error("No here fopi7yhw9e8r769")
        return Dvec[n2d(m)]
    elseif is_load(n)
        return -Dvec[n2d(n)]
    else
        error("No here lkipc709tyerywr76672w46resy")
    end
end; function bpf(b, Dvec, pgvec, fvec) # dispatch function of power flow on branch b
    b8[b] == -4 && return false
    b8[b] == -3 && return bpf_genonly(b, pgvec)
    b8[b] == -2 && return bpf_Donly(b, Dvec)
    return fvec[findfirst(x -> x == b, F2B)] # [Fallback] the corresponding JuMP's decision
end;
@load_data();

import JuMP, Gurobi; GRB_ENV = Gurobi.Env(); function JuMP_add_constr_KCL(pD, pg, pf) # Demand data vec, pg and pf decision vec
    m = JuMP.owner_model(pf[1])
    for n ∈ KCLnodes
        p_demand = (n ∈ D2N ? pD[findfirst(x -> x == n, D2N)] : false)
        p_gen    = (n ∈ G2N ? pg[findfirst(x -> x == n, G2N)] : false)
        bv, sgv = SparseArrays.findnz(view(A, :, n))
        d = b -> bpf(b, pD, pg, pf) # dispatch function
        JuMP.@constraint(m, sgv ⋅ d.(bv) + p_gen == p_demand) # at node n
    end
end;
Random.seed!(D);
Dref = rand(0.0006878765231343202:1.7345234e-12:0.013646062201588, D);
Gpmaxy0, Fpmaxy0, InvF, iF, iF2F = let
    model = JuMP.Model(() -> Gurobi.Optimizer(GRB_ENV)); JuMP.set_silent(model)
    JuMP.@variable(model, false ≤ pg[g = 1:G]);
    JuMP.@variable(model, pf[f = 1:F]);
    JuMP.@variable(model, false ≤ pfb[f = 1:F]);
    JuMP.@constraint(model, pf .<=  pfb);
    JuMP.@constraint(model, pf .>= -pfb);
    D⅁ = (1.0 + 0.0977(T-1)) * (1 + 0.01Y)Dref;
    JuMP_add_constr_KCL(D⅁, pg, pf)
    JuMP.@objective(model, Min, sum(pfb));
    JuMP.optimize!(model); JuMP.assert_is_solved_and_feasible(model; allow_local = false) # Devise data by Gurobi
    Gpmaxy0 = 1.4 * JuMP.value.(pg);
    Fpmaxy0 = JuMP.value.(pfb);
    # 💡 devise a deficient system at initial state
    iF = 5; # suppose we select `iF` lines to invest
    ind_vec = sortperm(Fpmaxy0);
    iF2F = ind_vec[end-(iF-1):end] # ⚠️ Note that this is orderless
    drop_ratio = 0.1 # This is all-time feasible!
    drop_ratio = 0.2 # This is already INfeasible at (y = 20, t = 3, h = 22)
    drop_cap = drop_ratio * Fpmaxy0[iF2F];
    Random.seed!(3iF);
    InvF = rand(0.57:1.7e-12:0.77, iF) .* drop_cap; # we are not investing only once, therefore define a <1 ratio
    Fpmaxy0[iF2F] *= (1 - drop_ratio);
    Gpmaxy0, Fpmaxy0, InvF, iF, iF2F
end;
Darray = Array{Float64}(undef, D, H, T, Y); Random.seed!(2D); for y = 1:Y
    Dref_y = (1.0 + 0.01Y * (y-1)/(Y-1))Dref
    for t = 1:T
        le, ue = 0.63 + 0.017(t-1), 1.0 + 0.0977(t-1)
        for h = 1:H
            Darray[:, h, t, y] = rand(le:1.73e-12:ue, D) .* Dref_y
        end
    end
end # generate Load data

# Master and SubBlock Assets
function drawmax!(udv, θ) # undrawn_bitvector, JuMP.value.(θ)
    still_has, max_i = false, 0
    if any(udv)
        still_has, max_v = true, -Inf
        for (i, undrawn) in enumerate(udv)
            if undrawn && max_v < θ[i]
                max_v, max_i = θ[i], i
            end
        end
        udv[max_i] = false # write! in place
    end
    return still_has, max_i # `max_i` is valid only if `still_has` == true
end; function fˈs_pen_4(ȷ, π::Array{Float64}, fpmax) # [idle] for block ȷ
    I, Y = size(π)
    m = JuMP.owner_model(fpmax[1])
    if ȷ == 1
        return JuMP.@expression(m, sum(fpmax[ı, 2, ȷ]π[ı, ȷ] for ı = 1:I))
    elseif ȷ == Y+1
        return JuMP.@expression(m, - sum(fpmax[ı, 1, ȷ]π[ı, ȷ-1] for ı = 1:I))
    end
    return JuMP.@expression(m, sum(fpmax[ı, 2, ȷ]π[ı, ȷ] - fpmax[ı, 1, ȷ]π[ı, ȷ-1] for ı = 1:I))
end; macro _pen_code()
    esc(quote
        I, Y = size(π)
        if ȷ == 1
            return JuMP.@expression(m, sum(fpmax_ȷ[ı, 2]π[ı, ȷ] for ı = 1:I))
        elseif ȷ == Y+1
            return JuMP.@expression(m, - sum(fpmax_ȷ[ı, 1]π[ı, ȷ-1] for ı = 1:I))
        end
        return JuMP.@expression(m, sum(fpmax_ȷ[ı, 2]π[ı, ȷ] - fpmax_ȷ[ı, 1]π[ı, ȷ-1] for ı = 1:I))
    end)
end; function fˈs_pen_4_(ȷ, π, fpmax_ȷ::Array{JuMP.VariableRef}) # for block ȷ
    m = JuMP.owner_model(fpmax_ȷ[1])
    @_pen_code()
end; function fˈs_pen_4_(ȷ, π::Array{JuMP.VariableRef}, fpmax_ȷ) # for block ȷ
    m = JuMP.owner_model(π[1])
    @_pen_code()
end; 

# master
COT = 1e-8
an_UB = 6.7357; out = JuMP.Model(() -> Gurobi.Optimizer(GRB_ENV)); # 🟣
JuMP.@variable(out, π[ı = 1:iF, ȷ = 1:iY-1]); # Care θ[ȷ]---π Relation
JuMP.@variable(out, θ[ȷ = 1:iY]);
JuMP.@expression(out, out_obj_tbMAX, sum(θ)); JuMP.@constraint(out, out_obj_tbMAX ≤ an_UB);
JuMP.@objective(out, Max, out_obj_tbMAX);

# subblocks
function Min_ȷ_fixed_part()
    Min_ȷ = JuMP.Model(() -> Gurobi.Optimizer(GRB_ENV)); # ✅ abs_y = iY2Y[ȷ]-1 + y
    JuMP.@variable(Min_ȷ, invf_ȷ[ı = 1:iF], Bin);
    JuMP.@expression(Min_ȷ, prim_obj_tbMIN_ȷ, sum(invf_ȷ));
    JuMP.@variable(Min_ȷ, fpmax_ȷ[ı = 1:iF, b = 1:2]);
    JuMP.set_lower_bound.(view(fpmax_ȷ, :, 2), Fpmaxy0[iF2F]); # lower bounded by the zero budget case
    JuMP.@constraint(Min_ȷ, view(fpmax_ȷ,:,2) - view(fpmax_ȷ,:,1) == InvF .* invf_ȷ);
    JuMP.@variable(Min_ȷ, false ≤ pg_ȷ[g = 1:G, h = 1:H, t = 1:T, y = 1:Y÷iY] ≤ Gpmaxy0[g]);
    JuMP.@variable(Min_ȷ, pf_ȷ[f = 1:F, h = 1:H, t = 1:T, y = 1:Y÷iY]);
    for f = 1:F # RateA limit
        if f ∈ iF2F
            ⅁ = fpmax_ȷ[findfirst(x -> x == f, iF2F), 2]
            JuMP.@constraint(Min_ȷ, view(pf_ȷ, f, :, :, :) .≥ -⅁)
            JuMP.@constraint(Min_ȷ, view(pf_ȷ, f, :, :, :) .≤  ⅁)
        else
            ⅁ = Fpmaxy0[f]
            JuMP.set_lower_bound.(view(pf_ȷ, f, :, :, :), -⅁)
            JuMP.set_upper_bound.(view(pf_ȷ, f, :, :, :),  ⅁)
        end
    end
    return Min_ȷ
end
Min_vec = [Min_ȷ_fixed_part() for _ = 1:iY]; # 🍅🍅🍅
for ȷ = 1:iY # finish building all sub_models
    Min_ȷ = Min_vec[ȷ]
    fpmax_ȷ = Min_ȷ[:fpmax_ȷ]
    pg_ȷ = Min_ȷ[:pg_ȷ]
    pf_ȷ = Min_ȷ[:pf_ȷ]
    for y = 1:Y÷iY, t = 1:T, h = 1:H # Network KCL
        JuMP_add_constr_KCL(
            view(Darray, :, h, t, iY2Y[ȷ]-1 + y),
            view(pg_ȷ,   :, h, t,             y), 
            view(pf_ȷ,   :, h, t,             y)
        )
    end
    # Set necessary bounds
    if ȷ == 1
        JuMP.fix.(view(fpmax_ȷ, :, 1), Fpmaxy0[iF2F])
    else
        JuMP.set_upper_bound.(view(fpmax_ȷ, :, 2), Fpmaxy0[iF2F] + (ȷ)InvF)
    end
end

# Main loop
JuMP.set_silent(out)
JuMP.set_silent.(Min_vec)
for ite = 1:typemax(Int)
    JuMP.optimize!(out); JuMP.assert_is_solved_and_feasible(out; allow_local = false)
    ub = JuMP.objective_bound(out)
    udv, Θ, Π = trues(iY), JuMP.value.(θ), JuMP.value.(π)
    @info "ite = $ite ▶ ub = $ub, Θ is $Θ; Π is $Π"
    outer_saturation = true
    while true
        still_has, ȷ = drawmax!(udv, Θ)
        if still_has == false
            @info "outer model saturation, you can check the convergence"
            return
        end
        Min_ȷ = Min_vec[ȷ]; @info "subblock $ȷ is selected"
        prim_obj_tbMIN_ȷ = Min_ȷ[:prim_obj_tbMIN_ȷ]
        fpmax_ȷ = Min_ȷ[:fpmax_ȷ]
        JuMP.@objective(Min_ȷ, Min, prim_obj_tbMIN_ȷ + fˈs_pen_4_(ȷ, Π, fpmax_ȷ))
        JuMP.optimize!(Min_ȷ); JuMP.assert_is_solved_and_feasible(Min_ȷ; allow_local = false)
        ObjVal_ȷ = JuMP.objective_value(Min_ȷ)
        Prim_obj_tbMIN_ȷ, Fpmax_ȷ = JuMP.value(prim_obj_tbMIN_ȷ), JuMP.value.(fpmax_ȷ)
        if ObjVal_ȷ + COT < Θ[ȷ] 
            JuMP.@constraint(out, θ[ȷ] ≤ Prim_obj_tbMIN_ȷ + fˈs_pen_4_(ȷ, π, Fpmax_ȷ))
            outer_saturation = false
            break # Go back to outer model to update Π
        end
    end
end

We can probably improve the performance of copying, but its not a high priority. Theres some overhead because we must first query all the data from the first model, which is probably the thing taking the time.

Your sleeping pattern becomes mysterious recently.

The moment when you start to worry about Oscar’s sleep patterns is probably a good time to wonder just how many questions you post on here :rofl:

because I’m a noob🫥