From 4cfad002de7cc7edbbde7b383793b917f57b660a Mon Sep 17 00:00:00 2001 From: Caroline Rogers Date: Fri, 28 Oct 2022 12:17:20 -0400 Subject: [PATCH] ENH: Layouts generated by majoriztion algorithm --- experiments/layout/Project.toml | 4 + experiments/layout/src/Majorization.jl | 187 ++++++++++++++++++++++++ experiments/layout/src/Setup.jl | 68 +++++++++ experiments/layout/src/Visualization.jl | 153 +++++++++++++++++++ experiments/layout/src/layout.jl | 5 + 5 files changed, 417 insertions(+) create mode 100644 experiments/layout/Project.toml create mode 100644 experiments/layout/src/Majorization.jl create mode 100644 experiments/layout/src/Setup.jl create mode 100644 experiments/layout/src/Visualization.jl create mode 100644 experiments/layout/src/layout.jl diff --git a/experiments/layout/Project.toml b/experiments/layout/Project.toml new file mode 100644 index 000000000..b2e802954 --- /dev/null +++ b/experiments/layout/Project.toml @@ -0,0 +1,4 @@ +name = "layout" +uuid = "8dadf337-1fa2-4dfe-88ac-366e9b2dd86c" +authors = ["Caroline Rogers "] +version = "0.1.0" diff --git a/experiments/layout/src/Majorization.jl b/experiments/layout/src/Majorization.jl new file mode 100644 index 000000000..a4b295519 --- /dev/null +++ b/experiments/layout/src/Majorization.jl @@ -0,0 +1,187 @@ +using Catlab +import Graphs as grs +import Catlab.Graphs as cat_grs +import Catlab.Graphics: Graphviz +using Catlab.Graphics, Catlab.CategoricalAlgebra, Catlab.Graphics.GraphvizGraphs +using LinearAlgebra +using ..Visualization +using NLsolve +using Random + +# TO ADD: ideal path vs shortest path +# look into figuring out how to specify rotation (think square) +# need right angles for squares and one right angle for triangle + +# graph generators - Owen + + +#======= file I/O to get positions =======# + +#= +function get_positions(g) + to_graphviz(prog="dot", g) +end +=# + +#======= create weighted laplacian =======# +function weighted_laplacian(g, num_vert) + # calculate shortest distance + weighted_lap = - grs.floyd_warshall_shortest_paths(g).dists + the_sum = sum(weighted_lap) + + # check for i = j + n = num_vert + for i in 1:n + for j in 1:n + if(weighted_lap[i,j] == 0) + weighted_lap[i,j] = the_sum + end + end + end + + return weighted_lap +end + +#current pos is vector of positions (x,y) + +#======= create X(t) laplacian =======# +function x_laplacian(g, current_pos, num_vert) + n = num_vert + x_lap = zeros(n,n) + # might want to change floyd warshall to something else - meant to be the IDEAL path + dist = grs.floyd_warshall_shortest_paths(g).dists + + for i in 1:n + current = current_pos[i] + + if(i == n) + neighbor = current_pos[i-1] + else + neighbor = current_pos[i+1] + end + + for j in 1:n + if(i != j) + # node i is located at Xi + # confused on the x,y of positions --- FIX THIS + x_lap[i,j] = -1 * dist[i,j] * inv(norm(current - neighbor)) + end + end + end + + the_sum = sum(x_lap) + + #find zeros + for i in n + for j in n + if(x_lap[i,j] == 0) + x_lap[i,j] = -1 * the_sum + end + end + end + + return x_lap +end + +#current_pos is vector of positions (x,y) + +#======= create stress function =======# +function stress_function(graph, current_pos, num_vert) + stress = [] + + # constant, calculate once + w_lap = weighted_laplacian(graph, num_vert) + n = num_vert + + # recomputed at every iteration (bc it includes position) + x_lap = x_laplacian(graph, current_pos, num_vert) + + #equation + (x, y) -> (inv(weighted_lap[i,j]) * x_lap[i,j] * x) + new_pos = 0; + + for i in 1:n + current = current_pos[i] + for j in 1:n + + function f!(F, x) + F[1] = (inv(w_lap[i,j]) * x_lap[i,j] * x[1]) + F[2] = (inv(w_lap[i,j]) * x_lap[i,j] * x[2]) + end + new_pos = nlsolve(f!, current) + end + push!(stress, new_pos.zero) + end + + return stress +end + +# current_pos is vector of positions (vector of vectors?) + +#======= calculate for all nodes =======# +function stress_majorization(original_pos, old_graph, num_vert) + n = num_vert + new_positions = Vector{String}() + + #end when [stress(X(t)) - stress(X(t+1))] / stress(X(t)) < 10^-4 + + #gives a vector of NL solve results + original_stress = stress_function(old_graph, original_pos, n) + + new_graph = old_graph + new_pos = original_pos + current_stress = original_stress + + # FIX ME!!!!! while loop broken + #while((original_stress[n][1] - current_stress[n][1]) / original_stress[n][1] < 10^(-4)) + #for i in 1:2 + + empty!(new_positions) + + # gives a vector of (x,y) vectors + stress = stress_function(new_graph, new_pos, n) + new_pos = stress + + for i in 1:n + #gives the solution vector (x,y) + sln = new_pos[i] + string_sln = string(sln[1], ",", sln[2]) + + push!(new_positions, string_sln) + end + + + return new_positions +end + + +#======= TESTING =======# + + +#original graph + +nv = 4 + +cat_g = cat_grs.cycle_graph(cat_grs.Graph, nv) +gv = to_graphviz(cat_g) + +g = grs.cycle_graph(nv) + +# new graph + +#good guess +#curr = [[1.0, 0.0], [2.0, 1.0], [3.0, 1.0], [4.0, 1.0], [5.0, 0.0]] + +curr = [] +#bad guess +for i in 1:nv + push!(curr, [float(rand((1:nv))), float(rand((0:nv)))]) +end + +pos_string = stress_majorization(curr, g, nv) + +new_G = to_graphviz_property_graph(cat_g; prog="neato", node_attrs=Dict(:pos=>pos_string)) +#new_G = to_graphviz_property_graph(cat_g; prog="neato") + +to_graphviz(new_G) + diff --git a/experiments/layout/src/Setup.jl b/experiments/layout/src/Setup.jl new file mode 100644 index 000000000..25000d14a --- /dev/null +++ b/experiments/layout/src/Setup.jl @@ -0,0 +1,68 @@ +module Setup +export BW, is_mat, is_normal + +using AlgebraicRewriting +using Catlab, Catlab.CategoricalAlgebra, Catlab.Present, Catlab.Theories, Catlab.Graphs + +using Catlab.Meta: Expr0 + +import Base: (*) + +@present TheoryWeightedLabeledGraphCospan <: SchGraph begin + (Weight, Color)::AttrType + weight::Attr(E,Weight) + color::Attr(V,Color) + (I,O)::Ob + i::Hom(I, V) + o::Hom(O, V) + # Not enforced, but: + # i ⋅ color == false + # o ⋅ color == true +end + +@abstract_acset_type AbstractWeightedLabeledGraphCospan <: AbstractGraph + +@acset_type WeightedLabeledGraphCospan( + TheoryWeightedLabeledGraphCospan, index=[:src,:tgt,] + ) <: AbstractWeightedLabeledGraphCospan + +const BW = WeightedLabeledGraphCospan{Union{Expr0, Var, Int, Rational}, + Union{Expr0, Var, Bool}} + +function BW(s,t,color=false,weight=1;i=[],o=[]) + i = i isa AbstractVector ? i : [i] + o = o isa AbstractVector ? o : [o] + color = color isa AbstractVector ? color : repeat([color], max(vcat(s,t))) + weight = weight isa AbstractVector ? weight : repeat([weight], length(s)) + @acset BW begin + V = length(color); E=length(s); I=length(i); O=length(o); + i=i; o=o; color=color; weight=weight; src=s; tgt=t + end +end + +function (*)(g::BW, scalar::Union{Int,Rational}) + g = deepcopy(g) + set_subpart!(g, :weight, [a*scalar for a in g[:weight]]) + return g +end + +(*)(scalar::Union{Int,Rational}, g::BW) = g * scalar + + +function is_normal(g::BW)::Bool + for v in vertices(g) + if g[v, :color] # white node + if !isempty(incident(g, v, :src)) return false end + else + if !isempty(incident(g, v, :tgt)) return false end + end + end + st = collect(zip(g[:src], g[:tgt])) # at most one edge btw two nodes + return length(st) == length(unique(st)) +end + +is_mat(g::BW) = (is_normal(g) + && sort(g[:i]) == findall((!).(g[:color])) + && sort(g[:o]) == findall(g[:color])) + +end # module diff --git a/experiments/layout/src/Visualization.jl b/experiments/layout/src/Visualization.jl new file mode 100644 index 000000000..b18a61f06 --- /dev/null +++ b/experiments/layout/src/Visualization.jl @@ -0,0 +1,153 @@ +module Visualization +export graphviz, view_window + +using Blink +using Interact +using AlgebraicRewriting +using Catlab.CategoricalAlgebra + +using Catlab.Graphs +using Catlab.Graphics.Graphviz: Statement +import Catlab.Graphics: to_graphviz, to_graphviz_property_graph +import Catlab.Graphics.GraphvizGraphs: node_label, default_node_attrs +import Catlab.Graphics.Graphviz: pprint +import Catlab.Graphics.Graphviz +import Catlab.Graphics: to_graphviz + +using AlgebraicRewriting +using ..Setup + +struct Title <: Statement + title::String +end + +function pprint(io, titl::Title, n::Int; directed::Bool=false) + print(io, """labelloc="t"; + label="$(titl.title)";""") +end + +function to_graphviz(g::AbstractPropertyGraph)::Graphviz.Graph + gv_name(v::Int) = "n$v" + gv_path(e::Int) = [gv_name(src(g,e)), gv_name(tgt(g,e))] + + # Add Graphviz node for each vertex in property graph. + + stmts = Graphviz.Statement[gprops(g)[:stmts]...] + + for v in vertices(g) + push!(stmts, Graphviz.Node(gv_name(v), vprops(g, v))) + end + + # Add Graphviz edge for each edge in property graph. + is_directed = !(g isa SymmetricPropertyGraph) + for e in edges(g) + # In undirected case, only include one edge from each pair. + if is_directed || (e <= inv(g,e)) + push!(stmts, Graphviz.Edge(gv_path(e), eprops(g, e))) + end + end + + attrs = gprops(g) + ga = Graphviz.as_attributes(get(attrs, :graph, Dict())) + na = Graphviz.as_attributes(get(attrs, :node, Dict())) + ea = Graphviz.as_attributes(get(attrs, :edge, Dict())) + Graphviz.Graph( + name = get(attrs, :name, "G"), + directed = is_directed, + prog = get(attrs, :prog, is_directed ? "dot" : "neato"), + stmts = stmts, + graph_attrs = ga, + node_attrs = na, + edge_attrs = ea, + ) +end +function to_graphviz_property_graph(g::AbstractGraph; + prog::AbstractString="dot", graph_attrs::AbstractDict=Dict(), + node_attrs::AbstractDict=Dict(), edge_attrs::AbstractDict=Dict(), + node_labels::Union{Symbol,Bool, Pair{Symbol, <:Function}}=false, + edge_labels::Union{Symbol,Bool, Pair{Symbol, <:Function}}=false, + title::Union{Nothing, String} = nothing + ) + grph = merge!(Dict{Any,Any}(:rankdir => "LR",), graph_attrs) + nde = merge!(Dict{Any,Any}(default_node_attrs(node_labels)), + filter(x->!(x[2] isa AbstractVector), node_attrs)) + + edg = merge!(Dict{Any,Any}(:arrowsize => "0.5"), + filter(x->!(x[2] isa AbstractVector),edge_attrs)) + pg = PropertyGraph{Any}( + g, + v -> node_label(g, node_labels, v), + e -> edge_label(g, edge_labels, e); + prog = prog, + graph = grph, + stmts= [Title(isnothing(title) ? "" : title)], + node = nde, + edge = edg, + ) +for (prop, vec) in filter(x->x[2] isa AbstractVector, node_attrs) + length(vec) == nv(g) || error("Bad node attr vector length for prop $prop: $(length(vec))!=$(nv(g))") + for (v,val) in zip(vertices(g), vec) + set_vprops!(pg, v, Dict([prop=>string(val)])) + end +end +for (prop, vec) in filter(x->x[2] isa AbstractVector, edge_attrs) + length(vec) == ne(g) || error("Bad edge attr vector length for prop $prop") + for (e,val) in zip(edges(g), vec) + set_eprops!(pg, e, Dict([prop=>string(val)])) + end +end + +return pg +end +node_label(g, labels::Bool, v::Int) = labels ? Dict(:label => string(v)) : Dict{Symbol,String}() +node_label(g, namef::Pair{Symbol, <:Function}, e::Int) = node_label(g, namef[1], namef[2], e) +node_label(g, name::Symbol, f::Function, e::Int) = begin +lab = string(f(g[e, name])) +return lab == "" ? Dict() : Dict(:label => lab, :shape=>"circle") +end + +edge_label(g, labels::Bool, v::Int) = labels ? Dict(:label => string(v)) : Dict{Symbol,String}() +edge_label(g, namef::Pair{Symbol, <:Function}, e::Int) = edge_label(g, namef[1], namef[2], e) +edge_label(g, name::Symbol, f::Function, e::Int) = Dict(:label => string(f(g[e, name]))) +function default_node_attrs(labels::Union{Symbol,Bool, Pair{Symbol, <:Function}}) + shape = labels isa Symbol ? "ellipse" : "point" #((!(labels isa Bool) || labels) ? "circle" : "point") + Dict(:shape => shape, :width => "0.05", :height => "0.05", :margin => "0") +end + + + +function graphviz(G::BW, pos=nothing; title=nothing) + G = deepcopy(G) + fc = Any[(!(c isa Bool) || c) ? "white" : "black" for c in G[:color]] + n_v, n_e, ni, no = [nparts(G,x) for x in [:V,:E,:I,:O]] + if !isnothing(pos) + if length(pos) == n_v + append!(pos, fill("", ni+no)) + elseif length(pos)!=n_v+ni+no + error("pos $pos not length $n_v nor $(n_v+ni+no)") + end + end + append!(fc, fill(:red, ni), fill(:blue, no)) + add_parts!(G,:V,ni+no; color=true) + add_parts!(G,:E, ni+no; src=vcat(n_v+1:n_v+ni, G[:o]), + tgt=vcat(G[:i],n_v+ni+1:nv(G)), weight=1//1) + sty = vcat(fill("filled", n_v), fill("dotted", ni+no)) # why doesn't this work? + n_a = Dict([:width=>".2", :height=>".2", :penwidth=>"3", :fillcolor=>fc, + :style=>sty, :color=>"black"]) + if !isnothing(pos) + n_a[:pos] = pos + end + esty = vcat(fill("solid", n_e), fill("dotted", ni+no)) + edir = vcat(fill("forward", n_e+ni), fill("forward", no)) # CHANGED + e_a = Dict([:style=>esty, :dir=>edir]) + to_graphviz(G, title=title, prog=isnothing(pos) ? "dot" : "neato", node_attrs=n_a, edge_attrs=e_a, + edge_labels= :weight => (x -> x == 1 ? "" : x), + node_labels=:color => (x -> (x isa Var || x isa Expr) ? string(x) : "")) +end; + +function view_window(x,y; positions=nothing) + w = Window() + body!(w, view_traj(x,y; positions=positions)) +end + +end # module diff --git a/experiments/layout/src/layout.jl b/experiments/layout/src/layout.jl new file mode 100644 index 000000000..53ae989a2 --- /dev/null +++ b/experiments/layout/src/layout.jl @@ -0,0 +1,5 @@ +module layout + +greet() = print("Hello World!") + +end # module