# Basic Data Utilities in PowerModels.jl---some more methods?

**URL:** <https://discourse.julialang.org/t/basic-data-utilities-in-powermodels-jl-some-more-methods/129291>\
**Category:** Optimization (Mathematical)\
**Tags:** question, graph-layout, powermodels\
**Created:** [May 24, 2025, 11:31am UTC](https://discourse.julialang.org/t/basic-data-utilities-in-powermodels-jl-some-more-methods/129291 "2025-05-24T11:31:06Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![WalterMadelim](https://avatars.discourse-cdn.com/v4/letter/w/3e96dc/32.png) [@WalterMadelim](https://discourse.julialang.org/u/WalterMadelim)\
**Post date:** [May 24, 2025, 11:31am UTC](https://discourse.julialang.org/t/basic-data-utilities-in-powermodels-jl-some-more-methods/129291/1 "2025-05-24T11:31:06Z")

</div>

This post is a feature request related to [Basic Data Utilities · PowerModels](https://lanl-ansi.github.io/PowerModels.jl/stable/basic-data-utilities/#PowerModels.calc_basic_incidence_matrix).

Although this sparse incidence matrix (denoted `A`) is useful, sometimes we may need another more succinct matrix—I call it `bft`, branch\_from\_to matrix—can be derived as follows

```julia
function A_2_bft(A) # from the incidence matrix to a B-by-2 branch_from_to matrix
    B, N = size(A)
    return bft = [findfirst(x -> x == (c == 1 ? -1 : 1), view(A, r, :)) for r in 1:B, c in 1:2]
end

```

Note that this matrix, aside from being more succinct, it is less ambiguous (-1, +1 in `A` is stipulated by the developer).  
Could PowerModels.jl add a method that retrieve `bft` matrix directly?  
And the conversion APIs `A_2_bft`, `bft_2_A`?  
I would write (the +1, -1 might be swapped due to different conventions)

```julia
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

```

We may further check the normality of a `bft` matrix

```julia
function assert_bft_is_normal(bft)
    minimum(bft) != 1 && error("smallest node label isn't 1")
    isempty(setdiff(1:maximum(bft), vec(bft))) || error("there is a gap between node labels")
end

```

---

<div class="post-metadata">

**Author:** ![ccoffrin](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ccoffrin/32/400_2.png) [@ccoffrin](https://discourse.julialang.org/u/ccoffrin)\
**Post date:** [May 24, 2025, 4:49pm UTC](https://discourse.julialang.org/t/basic-data-utilities-in-powermodels-jl-some-more-methods/129291/2 "2025-05-24T16:49:19Z")

</div>

@WalterMadelim, thanks for you interest in PowerModels!

First, it is best to make feature requests in the [PowerModels github issues](https://github.com/lanl-ansi/PowerModels.jl/issues) so we can debate and track them all in a common place.

Regarding this feature specifically. It am not immediately convinced this is an essential feature for the basic data utilities in PowerModels. The objective of basic data utilities was to provide a small collection of features that would support teaching of the most common power system analysis workflows. I worked closely with a few professors in the US power system community to determine what should be included or not. The incidence matrix as defined was determined as an important standard to support, as it supports workflows for computing PTDF and LODF matrices). I can imagine that your proposed `branch_from_to` matrix is useful, but if it is not a widely adopted standing in the power system classroom, then it is not a good fit for the basic data utilities of PowerModels.

---

<div class="post-metadata">

**Author:** ![WalterMadelim](https://avatars.discourse-cdn.com/v4/letter/w/3e96dc/32.png) [@WalterMadelim](https://discourse.julialang.org/u/WalterMadelim)\
**Post date:** [May 24, 2025, 11:21pm UTC](https://discourse.julialang.org/t/basic-data-utilities-in-powermodels-jl-some-more-methods/129291/3 "2025-05-24T23:21:18Z")

</div>

I see.

Are you aware that if there is a tool that can draw a node-branch diagram of the power system, given a specific incidence matrix `A`, or equivalently the `bft`?  
I’ve just tried AI, the diagrams they generate are dishonest and unusable.

---

<div class="post-metadata">

**Author:** ![ccoffrin](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ccoffrin/32/400_2.png) [@ccoffrin](https://discourse.julialang.org/u/ccoffrin)\
**Post date:** [May 25, 2025, 4:40pm UTC](https://discourse.julialang.org/t/basic-data-utilities-in-powermodels-jl-some-more-methods/129291/4 "2025-05-25T16:40:50Z")

</div>

Possibly [GitHub - WISPO-POP/PowerPlots.jl: Functions plot PowerModels networks](https://github.com/WISPO-POP/PowerPlots.jl) is what you are looking for?

---

<div class="post-metadata">

**Author:** ![WalterMadelim](https://avatars.discourse-cdn.com/v4/letter/w/3e96dc/32.png) [@WalterMadelim](https://discourse.julialang.org/u/WalterMadelim)\
**Post date:** [May 25, 2025, 11:29pm UTC](https://discourse.julialang.org/t/basic-data-utilities-in-powermodels-jl-some-more-methods/129291/5 "2025-05-25T23:29:38Z")

</div>

Not exactly. It totally depends on the established data format in those “cases(.m)”.  
It would be better if it only takes an incidence matrix as input and draw the associated diagram.  
Since I may make some custom modifications which not necessarily follow the data format of those "case.m"s.

> **""**
>
> ```julia
> # import LinearAlgebra.dot as dot
> # import JuMP, Gurobi
> import SparseArrays, PowerModels # ✅ branch numbering is more immediate, and node number lags behind
> 
> function relabel!(A, o, n) return @. A[A == o] = n end;
> function A_2_bft(A) # from the incidence matrix to a B-by-2 branch_from_to matrix
> B, N = size(A)
> return bft = [findfirst(x -> x == (c == 1 ? -1 : 1), view(A, r, :)) for r in 1:B, c in 1:2]
> 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 assert_A_has_no_zero_col(A)
> for c in eachcol(A)
> isempty(SparseArrays.findnz(c)[1]) && error("A has a zero col")
> end
> end;
> function get_single_node_vec(A) return [n for n in 1:size(A, 2) if sum(abs.(view(A, :, n))) == 1] end;
> function assert_is_A_row_normal(A)
> B, N = size(A)
> for b in 1:B # Make sure that each branch goes from one node to another different node.
> ns, vs = SparseArrays.findnz(A[b, :])
> vs == [1, -1] && continue
> vs == [-1, 1] && continue
> error("A is row-abnormal")
> end
> end;
> function assert_is_A_row_normal!(A)
> B, N = size(A)
> for b in 1:B
> ns, vs = SparseArrays.findnz(A[b, :])
> if vs[1] == 1
> A[b, :] *= -1
> end
> end
> end;
> function assert_is_A_row_normal2(A)
> B, N = size(A)
> for b in 1:B # Make sure that each branch goes from one node to another different node.
> ns, vs = SparseArrays.findnz(A[b, :])
> vs == [-1, 1] && continue
> error("2: A is row-abnormal")
> end
> end;
> function rectify_ref_dir_of_branches!(A)
> assert_is_A_row_normal(A)
> assert_is_A_row_normal!(A)
> assert_is_A_row_normal2(A)
> end;
> function get_b_on_of_single_node(bft, n)
> b, c = findfirst(x -> x == n, bft).I # one and only one
> on = bft[b, 3 - c]
> return b, on # branch, the other node
> end;
> function get_parallel_branches(bft)
> B, pbs = size(bft, 1), zeros(Int, 1, 2) # each row is a parallel branch pair
> for b in 1:B, ♭ in b+1:B
> (bft[♭, 1] == bft[b, 1] && bft[♭, 2] == bft[b, 2]) && (pbs = [pbs; b ♭])
> end
> size(pbs, 1) > 1 && return pbs[2:end, :]
> error("there is no parallel branches found")
> end;
> function get_pure_joint_bus(A, nvg, nvl)
> v = Int[]
> for n in setdiff(1:size(A, 2), union(nvg, nvl)) # loop over those "bare" nodes
> d = sum(abs.(view(A, :, n)))
> if d >= 3
> continue
> elseif d == 2
> push!(v, n)
> else
> error("node degree ≤ 1")
> end
> end
> return v
> end;
> function assert_is_connected(A)
> bft = A_2_bft(A);
> B, N = size(A)
> pb = Vector(1:B) # primal bs
> sn = [1] # subnet
> for ite in 1:B+1
> 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:N ⊆ sn && return # The graph is proved to be connected
> end
> error("here shouldn't be reached")
> end;
> 
> # initial raw data
> PowerModels.silence(); ⅁ = PowerModels.make_basic_network(PowerModels.parse_file("data/case118.m"));
> nvg = [⅁["gen"]["$g"]["gen_bus"] for g in 1:length(⅁["gen"])]; # 🟠 a bus_index vector that has generators
> nvl = [⅁["load"]["$l"]["load_bus"] for l in 1:length(⅁["load"])]; # 🟠 a bus index vector that has loads
> A = -PowerModels.calc_basic_incidence_matrix(⅁);
> 𝐑 = [⅁["branch"]["$b"]["rate_a"] for b in 1:size(A, 1)]; # 🟠
> 
> assert_A_has_no_zero_col(A)
> rectify_ref_dir_of_branches!(A)
> single_node_vec = get_single_node_vec(A)
> 
> @assert issubset(single_node_vec, union(nvg, nvl))
> issubset(single_node_vec, intersect(nvg, nvl)) || @info "the current topology can be enhanced"
> # analyze the local structure
> real_single_node_vec = setdiff(single_node_vec, intersect(nvl, nvg)) # these are single nodes to be removed, as they are simple
> load_single_node_vec, gen_single_node_vec = intersect(real_single_node_vec, nvl), intersect(real_single_node_vec, nvg)
> bft = A_2_bft(A); # 🟠
> f = n -> get_b_on_of_single_node(bft, n)[1];
> b_vec = f.(real_single_node_vec); # these branches are to be deleted!
> f = n -> get_b_on_of_single_node(bft, n)[2];
> load_single_on_vec, gen_single_on_vec = f.(load_single_node_vec), f.(gen_single_node_vec); # these are the end nodes after the action! to be done
> # actions here!
> nvl = union(setdiff(nvl, load_single_node_vec), load_single_on_vec);
> nvg = union(setdiff(nvg, gen_single_node_vec), gen_single_on_vec);
> ø = setdiff(1:size(bft, 1), b_vec); bft, 𝐑 = bft[ø, :], 𝐑[ø, :];
> @info "these node_labels are absent: $(setdiff(1:maximum(bft), bft)), current max node_label = $(maximum(bft))"
> let # 3 br-node deleted!
> relabel!(nvl, 115, 10);
> relabel!(nvg, 115, 10);
> relabel!(bft, 115, 10);
> relabel!(nvl, 116, 87);
> relabel!(nvg, 116, 87);
> relabel!(bft, 116, 87);
> relabel!(nvl, 118, 111);
> relabel!(nvg, 118, 111);
> relabel!(bft, 118, 111);
> end;
> A = bft_2_A(bft);
> assert_A_has_no_zero_col(A)
> rectify_ref_dir_of_branches!(A)
> single_node_vec = get_single_node_vec(A)
> @assert issubset(single_node_vec, union(nvg, nvl))
> issubset(single_node_vec, intersect(nvg, nvl)) || @info "the current topology can be enhanced"
> # analyze the local structure
> real_single_node_vec = setdiff(single_node_vec, intersect(nvl, nvg)) # these are single nodes to be removed, as they are simple
> load_single_node_vec, gen_single_node_vec = intersect(real_single_node_vec, nvl), intersect(real_single_node_vec, nvg)
> bft = A_2_bft(A);
> f = n -> get_b_on_of_single_node(bft, n)[1];
> b_vec = f.(real_single_node_vec); # these branches are to be deleted!
> f = n -> get_b_on_of_single_node(bft, n)[2];
> load_single_on_vec, gen_single_on_vec = f.(load_single_node_vec), f.(gen_single_node_vec); # these are the end nodes after the action! to be done
> # actions here!
> nvg = union(setdiff(nvg, gen_single_node_vec), gen_single_on_vec);
> ø = setdiff(1:size(bft, 1), b_vec); bft, 𝐑 = bft[ø, :], 𝐑[ø, :];
> @info "these node_labels are absent: $(setdiff(1:maximum(bft), bft)), current max node_label = $(maximum(bft))"
> let
> relabel!(nvl, 114, 9);
> relabel!(nvg, 114, 9);
> relabel!(bft, 114, 9);
> end;
> A = bft_2_A(bft);
> assert_A_has_no_zero_col(A)
> rectify_ref_dir_of_branches!(A)
> single_node_vec = get_single_node_vec(A)
> @assert issubset(single_node_vec, union(nvg, nvl))
> issubset(single_node_vec, intersect(nvg, nvl)) || @info "the current topology can be enhanced"
> bft = A_2_bft(A); # ✅ until here, we've finished deleting real_single_nodes
> pbs = get_parallel_branches(bft);
> for r in 1:size(pbs, 1)
> i, j = view(pbs, r, :)
> 𝐑[i] += 𝐑[j]
> end;
> rsl = setdiff(1:size(bft, 1), view(pbs, :, 2)); # remnant rows
> 𝐑, bft = 𝐑[rsl], bft[rsl, :]; # remove! the redundant parallel branches
> A = bft_2_A(bft);
> 
> nvi = get_pure_joint_bus(A, nvg, nvl); # a bus_index vector that is isolated (to be eliminated)
> let
> @info "Visualize the nodes to be merged"
> for n in nvi
> (b1, c1), (b2, c2) = getproperty.(findall(x -> x == n, bft), :I)
> n1c = bft[b1, 3 - c1]
> n2c = bft[b2, 3 - c2]
> println("$n1c --($b1)-- $n --($b2)-- $n2c")
> end
> end;
> # add! new branches and delete! old branches
> bft = [bft; 59 64]; push!(𝐑, min(𝐑[88], 𝐑[89]));
> bft = [bft; 68 80]; push!(𝐑, min(𝐑[119], 𝐑[120]));
> old_br_ind_vec = [88, 89, 119, 120];
> deleteat!(𝐑, old_br_ind_vec);
> bft = bft[setdiff(1:size(bft, 1), old_br_ind_vec), :];
> let
> relabel!(bft, 112, 63)
> relabel!(bft, 113, 81)
> relabel!(nvl, 112, 63)
> relabel!(nvl, 113, 81)
> relabel!(nvg, 112, 63)
> relabel!(nvg, 113, 81)
> end;
> A = bft_2_A(bft);
> assert_A_has_no_zero_col(A)
> rectify_ref_dir_of_branches!(A)
> bft = A_2_bft(A);
> assert_is_connected(A);
> sort!(nvg); sort!(nvl);
> @info "Till here, we have derived a basic network that has none of 1. elongating line 2. parallel line 3. pure joint bus"
> 
> ```

---

<div class="post-metadata">

**Author:** ![ccoffrin](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ccoffrin/32/400_2.png) [@ccoffrin](https://discourse.julialang.org/u/ccoffrin)\
**Post date:** [May 26, 2025, 6:02pm UTC](https://discourse.julialang.org/t/basic-data-utilities-in-powermodels-jl-some-more-methods/129291/6 "2025-05-26T18:02:21Z")

</div>

I do not know the Julia graph libraries well. @odow, do you have a suggestion on this topic?

---

<div class="post-metadata">

**Author:** ![WalterMadelim](https://avatars.discourse-cdn.com/v4/letter/w/3e96dc/32.png) [@WalterMadelim](https://discourse.julialang.org/u/WalterMadelim)\
**Post date:** [May 27, 2025, 5:13am UTC](https://discourse.julialang.org/t/basic-data-utilities-in-powermodels-jl-some-more-methods/129291/7 "2025-05-27T05:13:38Z")

</div>

Thanks for your willing to help. I’ve figured out a way.  
This is the code to generate `bft` describing the graph.

```julia
import JuMP, Gurobi
import LinearAlgebra.dot as dot
import SparseArrays.sparse as sparse
import Random
function adjnode(n)
    nv = Int[]
    for ci in findall(x -> x == n, bft)
        b, c = ci.I
        on = bft[b, 3 - c] # the other node on branch b
        if on in nv
            error("in Function adjnode(), shouldn't happen")
        else
            push!(nv, on)
        end
    end
    return sort(nv)
end
function search_extreme_cases_code() # 📘 invoke this code on demand
    sd_vec, obj_vec = Int[], Float64[]
    while true
        sd = rand(Int) # seed
        Random.seed!(sd)
        JuMP.delete(ø, c)
        LD = rand(0.03:7e-15:1) * lrand(L) .* R_lnv; # randomly generate a load vector at an instance
        c = JuMP.@constraint(ø, [n in 1:N], ı_at(n) + dot( 𝐟 , view(A, :, n)) == w_at(n)); # 📘 KCL
        JuMP.optimize!(ø)
        if JuMP.termination_status(ø) == JuMP.OPTIMAL
            push!(sd_vec, sd)
            push!(obj_vec, JuMP.objective_value(ø))
            l = length(sd_vec)
            print("\rl = $l")
            l == 1 && break
        end
    end
    while true
        sd = rand(Int) # seed
        Random.seed!(sd)
        JuMP.delete(ø, c)
        LD = rand(0.03:7e-15:1) * lrand(L) .* R_lnv; # randomly generate a load vector at an instance
        c = JuMP.@constraint(ø, [n in 1:N], ı_at(n) + dot( 𝐟 , view(A, :, n)) == w_at(n)); # 📘 KCL
        JuMP.optimize!(ø)
        if JuMP.termination_status(ø) == JuMP.OPTIMAL
            o = JuMP.objective_value(ø)
            if o < obj_vec[1]
                pushfirst!(sd_vec, sd)
                pushfirst!(obj_vec, o)
            elseif o > obj_vec[end]
                push!(sd_vec, sd)
                push!(obj_vec, o)
            else
                continue 
            end
            l = length(sd_vec)
            print("\rl = $l")
            l == 20 && break
        end
    end
end
function lrand() return sin(rand(0.1:7e-15:1.57)) end;
function lrand(N) return sin.(rand(0.1:7e-15:1.57, N)) end;
function bft_2_A(bft)
    B, N = size(bft, 1), maximum(bft)
    return sparse([Vector(1:B); Vector(1:B)], vec(bft), [-ones(Int, B); ones(Int, B)], B, N)
end;
function load_111_data()
    R = [10.76, 25.35, 137.5, 10.15, 20.32, 52.75, 42.08, 15.63, 15.79, 54.85, 17.45, 6.72, 32.03, 14.7, 15.21, 4.4, 5.51, 13.06, 24.61, 6.05, 21.62, 22.23, 9.39, 27.28, 12.94, 11.32, 6.91, 22.02, 13.79, 29.41, 6.77, 12.82, 11.56, 28.96, 22.21, 13.01, 6.88, 32.27, 9.4, 10.92, 14.24, 8.64, 4.35, 107.59, 22.07, 7.59, 39.87, 115.33, 29.96, 10.15, 6.31, 20.73, 17.77, 22.11, 5.88, 7.96, 4.44, 6.49, 12.1, 7.95, 8.48, 5.67, 17.19, 6.79, 5.67, 20.97, 14.08, 7.73, 18.06, 6.67, 9.0, 7.47, 15.46, 113.06, 70.8, 10.96, 7.91, 10.96, 14.73, 4.79, 8.71, 5.09, 7.57, 7.32, 81.68, 19.56, 29.19, 41.93, 11.35, 37.06, 24.0, 5.03, 9.38, 30.37, 10.81, 69.97, 3.87, 3.32, 30.37, 8.61, 2.73, 30.72, 5.56, 6.06, 24.31, 8.13, 7.63, 8.74, 26.49, 7.27, 10.64, 5.38, 86.71, 44.94, 32.17, 15.58, 12.44, 29.32, 7.69, 7.29, 15.86, 8.79, 10.81, 6.43, 15.49, 16.72, 12.86, 28.73, 8.45, 12.68, 6.8, 14.68, 24.77, 6.06, 20.27, 12.35, 11.81, 10.16, 5.33, 3.72, 18.52, 19.61, 12.46, 6.13, 13.49, 8.7, 19.63, 9.8, 20.47, 5.38, 6.81, 6.57, 4.74, 28.75, 19.9, 5.9, 14.98, 5.9, 36.65, 6.06, 13.85, 16.38, 35.72, 5.3, 17.93, 14.81, 105.49, 276.46, 22.37, 19.78, 29.11, 30.37]
    gnv = [1, 4, 6, 8, 12, 15, 18, 19, 24, 25, 26, 27, 31, 32, 34, 36, 40, 42, 46, 49, 54, 55, 56, 59, 61, 62, 63, 65, 66, 69, 70, 72, 73, 74, 76, 77, 80, 81, 85, 86, 87, 89, 90, 91, 92, 99, 100, 103, 104, 105, 107, 110]
    lnv = [1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 27, 28, 29, 31, 32, 33, 34, 35, 36, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 62, 63, 66, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111]
    bft = [1 2; 1 3; 4 5; 3 5; 5 6; 6 7; 5 8; 4 11; 5 11; 11 12; 2 12; 3 12; 7 12; 11 13; 12 14; 13 15; 14 15; 12 16; 15 17; 16 17; 17 18; 18 19; 19 20; 15 19; 20 21; 21 22; 22 23; 23 24; 23 25; 25 26; 25 27; 27 28; 28 29; 17 30; 8 30; 26 30; 17 31; 29 31; 23 32; 31 32; 27 32; 15 33; 19 34; 35 36; 35 37; 33 37; 34 36; 34 37; 37 38; 37 39; 37 40; 30 38; 39 40; 40 41; 40 42; 41 42; 43 44; 34 43; 44 45; 45 46; 46 47; 46 48; 47 49; 42 49; 45 49; 48 49; 49 50; 49 51; 51 52; 52 53; 53 54; 49 54; 54 55; 54 56; 55 56; 56 57; 50 57; 56 58; 51 58; 54 59; 56 59; 55 59; 59 60; 59 61; 60 61; 60 62; 61 62; 61 64; 38 65; 64 65; 49 66; 62 66; 62 67; 65 66; 66 67; 65 68; 47 69; 49 69; 68 69; 69 70; 24 70; 70 71; 24 72; 71 72; 71 73; 70 74; 70 75; 69 75; 74 75; 76 77; 69 77; 75 77; 77 78; 78 79; 77 80; 79 80; 77 82; 82 83; 83 84; 83 85; 84 85; 85 86; 85 88; 85 89; 88 89; 89 90; 90 91; 89 92; 91 92; 92 93; 92 94; 93 94; 94 95; 80 96; 82 96; 94 96; 80 97; 80 98; 80 99; 92 100; 94 100; 95 96; 96 97; 98 100; 99 100; 100 101; 92 102; 101 102; 100 103; 100 104; 103 104; 103 105; 100 106; 104 105; 105 106; 105 107; 105 108; 106 107; 108 109; 103 110; 109 110; 63 110; 17 81; 32 81; 9 32; 10 27; 9 10; 68 87; 75 111; 76 111; 59 64; 68 80]
    return R, gnv, lnv, bft # RateA, generator_node_vector, load_node_vector, branch_from_to
end;
function ı_at(n) # return the power injection Given a node index
    i = findfirst(x -> x == n, gnv) # injection index, NOT a generator index
    b = isnothing(i)
    if b
        return !b # no injection
    else
        return ı[i] # a VariableRef
    end
end;
function w_at(n) # return the power withdrawal Given a node index
    l = findfirst(x -> x == n, lnv) # load index
    b = isnothing(l)
    if b
        return !b # no injection
    else
        return LD[l] # load data
    end
end;
R, gnv, lnv, bft = load_111_data(); A = bft_2_A(bft);

```

And this is the code that generate a graph that can be understood by human.

```julia

using Graphs, GraphPlot, CairoMakie
function clockwise_rotate(A, t)
    c, s = cos(t), sin(t)
    [c s; -s c] * A
end
function relocate_as_mean!(xv, yv, n, n1, n2)
    xv[n] = (xv[n1] + xv[n2])/2
    yv[n] = (yv[n1] + yv[n2])/2
end
g = SimpleGraph(maximum(bft)); for (n, m) in eachrow(bft) # graph construction
    add_edge!(g, n, m)
end
function custom_modification!(xv, yv)
    relocate_as_mean!(xv, yv, 48, 66, 68)
    relocate_as_mean!(xv, yv, 47, 46, 69)
    relocate_as_mean!(xv, yv, 50, 49, 57)
    relocate_as_mean!(xv, yv, 92, 91, 94)
    relocate_as_mean!(xv, yv, 25, 23, 27)
    relocate_as_mean!(xv, yv, 9, 29, 32)
    relocate_as_mean!(xv, yv, 86, 83, 89)
    relocate_as_mean!(xv, yv, 79, 77, 97)
    relocate_as_mean!(xv, yv, 87, 69, 80)
    relocate_as_mean!(xv, yv, 51, 42, 50)
    relocate_as_mean!(xv, yv, 73, 71, 74)
    relocate_as_mean!(xv, yv, 43, 15, 44)
    relocate_as_mean!(xv, yv, 1, 2, 14)
    relocate_as_mean!(xv, yv, 108, 55, 107)
    relocate_as_mean!(xv, yv, 109, 108, 110)
    relocate_as_mean!(xv, yv, 63, 91, 110)
    relocate_as_mean!(xv, yv, 88, 86, 89)
    relocate_as_mean!(xv, yv, 90, 89, 91)
    relocate_as_mean!(xv, yv, 72, 23, 69)
end; Random.seed!(2527829590547944688);
xv, yv = spring_layout(g); # supported by the layout algorithm, crude
angle = 39; # choose an apt angle
temp = clockwise_rotate([xv'; yv'], π/180 * angle);
xv, yv = temp[1, :], temp[2, :];
custom_modification!(xv, yv);

line_colors = [
    :chocolate, # Yellow
    :gold1, # Yellow
    :blue, # blue 
    :cyan, # blue
    :lime, # green lemon
    :darkgreen, # green
    :olive, # green
    :deeppink, # Red
    :red4 # Red
];
label_size = 8

f = Figure(); ax = Axis(f[1, 1]); # 🖼️
Random.seed!(2)
for (n, m) in eachrow(bft) # Draw lines
    lines!(ax, [xv[n], xv[m]], [yv[n], yv[m]]; linewidth = 1, color = rand(line_colors))
end
for n in 1:maximum(bft) # Draw Points
    if n in gnv
        if n in lnv
            text!(ax, [xv[n]], [yv[n]]; color = :purple, text = "$n", fontsize = label_size)
        else
            text!(ax, [xv[n]], [yv[n]]; color = :red, text = "$n", fontsize = label_size)
        end
    elseif n in lnv
        text!(ax, [xv[n]], [yv[n]]; color = :skyblue, text = "$n", fontsize = label_size)
    else
        text!(ax, [xv[n]], [yv[n]]; color = :black, text = "$n", fontsize = label_size)
    end
end
hidedecorations!(ax)
f # After typing this in REPL, it will generate the picture

```

The picture derived is

 ![image](https://global.discourse-cdn.com/julialang/original/3X/0/1/01293a605408bd1d6b609675acb7eb4210ffa4a0.jpeg)

The good ideas include

1. use diverse line colors (I use 2 Yellow + 2 Blue + 2 Red + 3 Green)
2. as a user, you need to do some custom relocations inevitably. First, Givens rotation can be employed to make the layout look upright. Second, adjust specific point locations, as I had done in `custom_modification!`.

---

<div class="post-metadata">

**Author:** ![odow](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/odow/32/28685_2.png) [@odow](https://discourse.julialang.org/u/odow)\
**Post date:** [May 27, 2025, 5:52am UTC](https://discourse.julialang.org/t/basic-data-utilities-in-powermodels-jl-some-more-methods/129291/8 "2025-05-27T05:52:08Z")

</div>

I don’t have any specific suggestions

---

<div class="post-metadata">

**Author:** ![hexaeder](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/hexaeder/32/24403_2.png) [@hexaeder](https://discourse.julialang.org/u/hexaeder)\
**Post date:** [May 27, 2025, 2:28pm UTC](https://discourse.julialang.org/t/basic-data-utilities-in-powermodels-jl-some-more-methods/129291/9 "2025-05-27T14:28:30Z")

</div>

Shameless plug: you might want to check out [`GraphMakie.jl`](https://github.com/MakieOrg/GraphMakie.jl), which is doing something quite similar to what you did here: take a graph and style it according to some rules, including node size, edge thickness, labels, colors, …

For bus placement its especially helpful to look at the `pin` keyword arguments of [`Stress`](https://juliagraphs.org/NetworkLayout.jl/stable/#Stress-Majorization) and [`Spring`](https://juliagraphs.org/NetworkLayout.jl/stable/#Spring/Repulsion-Model) layouts, which can be used to place some of the vertices manually, while the rest are layouted automaticially around and between the fixed nodes.

---

<div class="post-metadata">

**Author:** ![slwu89](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/slwu89/32/217323_2.png) [@slwu89](https://discourse.julialang.org/u/slwu89)\
**Post date:** [May 27, 2025, 2:28pm UTC](https://discourse.julialang.org/t/basic-data-utilities-in-powermodels-jl-some-more-methods/129291/10 "2025-05-27T14:28:38Z")

</div>

Perhaps you know about it already, but sometimes for highly detailed graphs with not too many nodes I prefer GraphViz ([https://graphviz.org/](https://graphviz.org/)). It’s easy to build the DOT language string in Julia and send it to the command line tool to render it into SVG, PDF, etc. I did this for the multi-commodity network problem solved with JuMP here [JuMP-ing with AlgebraicJulia II: A practical optimization model – AlgebraicJulia blog](https://blog.algebraicjulia.org/post/2025/03/optim-netflow/) (see end of post for the visualization)
