diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d92f26..9b6f17b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,7 @@ jobs: strategy: matrix: version: - - '1.0' - - '1.6' + - '1' - 'nightly' os: - ubuntu-latest diff --git a/Project.toml b/Project.toml index 9bab082..8ec7fb1 100644 --- a/Project.toml +++ b/Project.toml @@ -4,21 +4,25 @@ authors = ["Lyndon White and contributors"] version = "0.1.0" [deps] -Gtk = "4c0ca9eb-093a-5379-98c5-f87ac0bbbf44" -ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" -ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +ECOS = "e2685f51-7e38-5353-a97d-a921fd2c8199" Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9" IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LightGraphs = "093fc24a-ae57-5d10-9952-331d41423f4d" -Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" -VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [compat] julia = "1" [extras] +ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" +ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +SimpleWeightedGraphs = "47aef6b3-ad0c-573a-a1e2-d07658019622" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" [targets] -test = ["Test"] +test = ["ImageIO", "ImageMagick", "Plots", "SimpleWeightedGraphs", "Test", "VisualRegressionTests"] diff --git a/src/LayeredLayouts.jl b/src/LayeredLayouts.jl index 05e8190..2c53637 100644 --- a/src/LayeredLayouts.jl +++ b/src/LayeredLayouts.jl @@ -1,15 +1,24 @@ module LayeredLayouts +using Dates using JuMP +using ECOS using Ipopt +using Cbc using IterTools: IterTools using LightGraphs +using Random -export LayeredMinDistOne +export LayeredMinDistOne, Zarate export solve_positions abstract type AbstractLayout end +include("utils.jl") + include("graph_properties.jl") +include("layering.jl") + include("min_dist_one.jl") +include("zarate.jl") end diff --git a/src/layering.jl b/src/layering.jl new file mode 100644 index 0000000..3536ef5 --- /dev/null +++ b/src/layering.jl @@ -0,0 +1,48 @@ +# This contains algorithms for breaking up a DAG into layers + +function layer_by_longest_path_to_source(graph) + dists = longest_paths(graph, sources(graph)) + layer_groups = IterTools.groupby(i->dists[i], sort(vertices(graph), by=i->dists[i])) + return collect.(layer_groups) +end + + +################### +# helpers + + +function node2layer_lookup(layer2nodes) + flat_map = [node=>layer_ind for (layer_ind, nodes) in enumerate(layer2nodes) for node in nodes] + sort!(flat_map) + @assert first.(flat_map) == 1:length(flat_map) + return last.(sort(flat_map)) +end + +"add nodes so that no edges span multiple layers, returns mask" +function add_dummy_nodes!(graph, layer2nodes) + dag_or_error(graph) + nondummy_nodes = vertices(graph) + node2layer = node2layer_lookup(layer2nodes) # doesn't have dummy nodes, doesn't need them + for cur_node in vertices(graph) + cur_layer = node2layer[cur_node] + for out_node in copy(outneighbors(graph, cur_node)) # need to copy as outwise will mutate when the graph is mutated + out_layer = node2layer[out_node] + cur_layer < out_layer || DomainError(node2layer, "Layer assigmenment must be strictly monotonic") + if out_layer != cur_layer + 1 + rem_edge!(graph, cur_node, out_node) || error("removing edge failed") + prev_node = cur_node + for step_layer in (cur_layer + 1) : (out_layer - 1) + add_vertex!(graph) || throw(OverflowError("Could not add vertices to graph.")) + step_node = nv(graph) + push!(layer2nodes[step_layer], step_node) + add_edge!(graph, prev_node, step_node) + prev_node = step_node + end + add_edge!(graph, prev_node, out_node) + end + end + end + is_dummy_mask = trues(nv(graph)) + is_dummy_mask[nondummy_nodes] .= false + return is_dummy_mask +end \ No newline at end of file diff --git a/src/min_dist_one.jl b/src/min_dist_one.jl index 4551a47..6e062d1 100644 --- a/src/min_dist_one.jl +++ b/src/min_dist_one.jl @@ -4,17 +4,15 @@ Base.@kwdef struct LayeredMinDistOne <: AbstractLayout intralayer_seperation::Float64 = 1.0 end -function determine_layers(::LayeredMinDistOne, graph) - dists = longest_paths(graph, sources(graph)) - layer_groups = IterTools.groupby(i->dists[i], sort(vertices(graph), by=i->dists[i])) - return layer_groups -end function solve_positions(layout::LayeredMinDistOne, graph) - layer_groups = determine_layers(layout, graph) + layer_groups = layer_by_longest_path_to_source(graph) m = Model(Ipopt.Optimizer) set_silent(m) + set_optimizer_attribute(m, "print_level", 0) # TODO this can be deleted once the version of IPOpt that actually supports `set_silent` is released + + ys = map(enumerate(layer_groups)) do (layer, nodes) y_min = 0 # IPOpt can't find a solution without this @@ -25,9 +23,12 @@ function solve_positions(layout::LayeredMinDistOne, graph) end node_vars = Dict{Int, VariableRef}() # lookup list from vertex index to variable - for (y, nodes) in zip(ys, layer_groups) + node_layer_indexes = Dict{Int, Int}() + for (layer_ind, (y, nodes)) in enumerate(zip(ys, layer_groups)) for n in nodes @assert !haskey(node_vars, n) + @assert !haskey(node_layer_indexes, n) + node_layer_indexes[n] = layer_ind node_vars[n] = y[n] # remember this for later end end @@ -49,14 +50,14 @@ function solve_positions(layout::LayeredMinDistOne, graph) )) optimize!(m) + @show termination_status(m) xs = Float64[] ys = Float64[] - for (layer, nodes) in enumerate(layer_groups) - for node in nodes - push!(xs, layer) - push!(ys, value(node_vars[node])) - end + for node in vertices(graph) + layer_ind = node_layer_indexes[node] + push!(xs, layout.interlayer_seperation * layer_ind) + push!(ys, value(node_vars[node])) end return xs, ys end diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..375419b --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,10 @@ +# for things that are not actually related to the problem itself + +""" + try_get(collection, inds...) + +Like `getindex`, except returns `nothing` if not found or +if the collection itsself is `nothing`. +""" +try_get(::Nothing, inds...) = nothing +try_get(x, inds...) = get(x, inds, nothing) diff --git a/src/zarate.jl b/src/zarate.jl new file mode 100644 index 0000000..ab5fd67 --- /dev/null +++ b/src/zarate.jl @@ -0,0 +1,240 @@ +""" + Zarate + +A Sugiyaqma style layout for DAGs and Sankey diagrams. +Based on + +Zarate, D. C., Le Bodic, P., Dwyer, T., Gange, G., & Stuckey, P. (2018, April). +Optimal sankey diagrams via integer programming. +In 2018 IEEE Pacific Visualization Symposium (PacificVis) (pp. 135-139). IEEE. + +# Fields + - `time_limit::Dates.Period`: how long to spend trying alternative orderings. + There are often many possible orderings that have the same amount of crossings. + There is a chance that one of these will allow a better arrangement than others. + Mostly the first solution tends to be very good. + By setting a `time_limit > Second(0)`, multiple will be tried til the time limit is exceeded. + (Note: that this is not a maximum time, but rather a limit that once exceeded no more + attempts will be made.). + If you have a `time_limit` greater than `Second(0)` set then the result is no longer determenistic. + Note also that this is heavily affected by first call compilation time. +""" +Base.@kwdef struct Zarate <: AbstractLayout + time_limit::Dates.Period = Dates.Second(0) + ordering_solver::Any = ()->Cbc.Optimizer(; randomSeed=1, randomCbcSeed=1) + arranging_solver::Any = ECOS.Optimizer +end + + +function solve_positions(layout::Zarate, original_graph) + graph = copy(original_graph) + + # 1. Layer Assigment + layer2nodes = layer_by_longest_path_to_source(graph) + is_dummy_mask = add_dummy_nodes!(graph, layer2nodes) + + # 2. Layer Ordering + start_time = Dates.now() + min_total_distance = Inf + min_num_crossing = Inf + local best_pos + ordering_model, is_before = ordering_problem(layout, graph, layer2nodes) + for round in 1:typemax(Int) + round > 1 && forbid_solution!(ordering_model, is_before) + + optimize!(ordering_model) + # No need to keep banning solutions if not finding optimal ones anymore + round > 1 && termination_status(ordering_model) != MOI.OPTIMAL && break + num_crossings = objective_value(ordering_model) + # we are not interested in arrangements that have more crossings, only in + # alternatives with same number of crossings. + num_crossings > min_num_crossing && break + min_num_crossing = num_crossings + order_layers!(layer2nodes, is_before) + + # 3. Node Arrangement + xs, ys, total_distance = assign_coordinates(layout, graph, layer2nodes) + if total_distance < min_total_distance + min_total_distance = total_distance + best_pos = (xs, ys) + end + Dates.now() - start_time > layout.time_limit && break + end + xs, ys = best_pos + return xs[.!is_dummy_mask], ys[.!is_dummy_mask] +end + +""" + ordering_problem(::Zarate, graph, layer2nodes) + +Formulates the problem of working out optimal ordering as a MILP. + +Returns: + - `model::Model`: the JuMP model that when optized will find the optimal ordering + - `is_before::AbstractVector{AbstractVector{Variable}}`: the variables of the model, + which once solved will have `value(is_before[n1][n2]) == true` + if `n1` is best arrange before `n2`. +""" +function ordering_problem(layout::Zarate, graph, layer2nodes) + m = Model(layout.ordering_solver) + set_silent(m) + + T = JuMP.Containers.DenseAxisArray{VariableRef,1,Tuple{Vector{Int64}},Tuple{Dict{Int64,Int64}}} + node_is_before = Vector{T}(undef, nv(graph)) + for (layer, nodes) in enumerate(layer2nodes) + before = @variable(m, [nodes, nodes], Bin, base_name="befores_$layer") + for n1 in nodes + node_is_before[n1] = before[n1, :] + for n2 in nodes + n1 === n2 && continue + # can't have n1 0.5 + add_to_expression!(cur_trues, var) + total_trues += 1 + end + end + end + # for it to be a different order some of the ones that are currently true must swap to being false + @constraint(m, sum(cur_trues) <= total_trues - 1) + + return m +end + +""" + assign_coordinates(graph, layer2nodes) + +Works out the `x` and `y` coordinates for each node in the `graph`. +This is via formulating the problem as a QP, and minimizing total distance +of links. +It maintains the order given in `layer2nodes`s. +returns: `xs, ys, total_distance` +""" +function assign_coordinates(layout, graph, layer2nodes) + m = Model(layout.arranging_solver) + set_silent(m) + set_optimizer_attribute(m, "print_level", 0) # TODO this can be deleted once the version of IPOpt that actually supports `set_silent` is released + + node2y = Dict{Int, VariableRef}() + for (layer, nodes) in enumerate(layer2nodes) + first_node, other_nodes = Iterators.peel(nodes) + prev_y = node2y[first_node] = @variable(m, base_name="y_$first_node") + for node in other_nodes + # each must be greater than the last, with a gap of 1 unit + y = @variable(m, base_name="y_$node") + @constraint(m, prev_y + 1.0 <= y) + prev_y = node2y[node] = y + end + end + + all_distances = AffExpr[] + # minimze link distances + for cur in vertices(graph) + for out in outneighbors(graph, cur) + distance = (node2y[cur] - node2y[out]) + push!(all_distances, distance) + end + # to prevent going way off-sqcale, also keep things near x-axis + end + # for first layer minimize distance to origin + for cur in first(layer2nodes) + distance = node2y[cur] + push!(all_distances, distance) + end + @variable(m, total_distance) + distance_cone = [1; total_distance; all_distances] + @constraint(m, distance_cone in RotatedSecondOrderCone()) + @objective(m, Min, total_distance); + optimize!(m) + #termination_status(m) + score = objective_value(m) + + ### + xs = Vector{Float64}(undef, nv(graph)) + ys = Vector{Float64}(undef, nv(graph)) + for (xi, nodes) in enumerate(layer2nodes) + for node in nodes + xs[node] = xi + ys[node] = value(node2y[node]) + end + end + return xs, ys, score +end diff --git a/test/demos.jl b/test/demos.jl new file mode 100644 index 0000000..f29fbb5 --- /dev/null +++ b/test/demos.jl @@ -0,0 +1,44 @@ +"helper for testing" +function quick_plot(graph, xs, ys) + nv(graph)==length(xs) == length(ys) || error("need 1 position per vertex") + scatter(xs, ys; markeralpha=0, text=string.(vertices(graph))) + + weights_mat = weights(graph) + # now draw connections + for edge in edges(graph) + lxs = [xs[edge.src], xs[edge.dst]] + lys = [ys[edge.src], ys[edge.dst]] + w = 5*weights_mat[edge.src, edge.dst] + plot!(lxs, lys; linewidth=w, alpha=0.7, legend=false) + end +end + +quick_plot_solve(layout, graph) = quick_plot(graph, solve_positions(layout, graph)...) + +@testset "quick_plot" begin + ref_filename = joinpath(@__DIR__, "references", "test_utils", "$quick_plot.png") + @plottest quick_plot(SimpleDiGraph(Edge.([1=>2, 2=>3])), [1,2,5], [1,2,3]) ref_filename true 0.05 +end + +function test_example(layout, graph_name, tol=0.05) + @testset "$graph_name" begin + graph = getfield(Examples, graph_name) + ref_filename = joinpath(@__DIR__, "references", string(typeof(layout)), "$graph_name.png") + mkpath(dirname(ref_filename)) + @plottest quick_plot_solve(layout, graph) ref_filename true tol + end +end + +@testset "$layout Demos" for layout in (Zarate(),)# LayeredMinDistOne()) + test_example(layout, :cross) + test_example(layout, :loop) + test_example(layout, :medium_pert) + test_example(layout, :sankey_3twos) + test_example(layout, :two_lines, 0.02) + test_example(layout, :xcross) + + test_example(layout, :tree, 0.07) + + #test_example(layout, :large_depgraph) # too big + #test_example(layout, :extra_large_depgraph) # too big +end diff --git a/test/examples.jl b/test/examples.jl index b0c1613..26781fc 100644 --- a/test/examples.jl +++ b/test/examples.jl @@ -1,5 +1,6 @@ module Examples using LightGraphs + using SimpleWeightedGraphs medium_pert = SimpleDiGraph(Edge.([ 1 => 2 1 => 3 @@ -18,4 +19,470 @@ module Examples 9 => 10 10 => 11 ])) -end \ No newline at end of file + + two_lines = SimpleDiGraph(Edge.([ + 1 => 3, 3=>5, 5=>7, 7=>9, + 2 => 4, 4=>6, 6=>8, + ])) + + loop = SimpleDiGraph(Edge.([ + 1 .=> [2, 3]; + 2 => 4; 4 => 6; + 3 => 5; 5 => 7; + [6, 7] .=> 8 + ])) + + cross = SimpleDiGraph(Edge.([ + 1 .=> [2, 3]; + 2 .=> [3, 4]; + 3 => 4; + ])) + + xcross = SimpleDiGraph(Edge.([ + 1 .=> [2, 3, 4]; + 2 .=> [3, 4]; + 3 => 4; + ])) + + sankey_3twos = SimpleWeightedDiGraph(6) + add_edge!(sankey_3twos, 1, 3, 1.0) + add_edge!(sankey_3twos, 1, 4, 2.0) + add_edge!(sankey_3twos, 2, 3, 3.0) + add_edge!(sankey_3twos, 2, 4, 8.0) + add_edge!(sankey_3twos, 3, 5, 3.0) + add_edge!(sankey_3twos, 3, 6, 1.0) + add_edge!(sankey_3twos, 4, 5, 10.0) + + tree = SimpleDiGraph(Edge.([1 => 2, 2 => 3, 4 => 5, 4 => 6, 4 => 7, 4 => 8, 4 => 9, 4 => 10, 5 => 11, 5 => 12, 8 => 15, 8 => 16, 8 => 17, 8 => 18, 8 => 19, 9 => 20, 9 => 21, 10 => 22, 12 => 13, 13 => 14, 23 => 4, 23 => 24, 23 => 25, 23 => 26, 23 => 27, 23 => 28, 23 => 29, 23 => 30, 23 => 31, 28 => 32, 28 => 33, 29 => 35, 30 => 1, 30 => 38, 31 => 40, 33 => 34, 35 => 36, 35 => 37, 38 => 39, 40 => 41, 41 => 42])) + + tiny_depgraph = SimpleDiGraph(Edge.([ + 1 => 2 + 2 => 4 + 2 => 5 + 2 => 6 + 3 => 2 + 3 => 4 + 3 => 7 + 3 => 5 + 3 => 8 + 3 => 6 + 4 => 9 + 4 => 10 + 4 => 11 + ])) + + + large_depgraph = SimpleDiGraph(Edge.([ + 1 => 2 + 2 => 3 + 3 => 4 + 3 => 5 + 5 => 6 + 2 => 7 + 2 => 5 + 2 => 8 + 8 => 9 + 9 => 10 + 9 => 11 + 11 => 12 + 12 => 13 + 9 => 12 + 8 => 14 + 14 => 10 + 8 => 15 + 8 => 13 + 8 => 16 + 16 => 17 + 17 => 13 + 16 => 18 + 16 => 19 + 16 => 13 + 16 => 20 + 16 => 21 + 8 => 22 + 22 => 23 + 22 => 24 + 8 => 25 + 25 => 18 + 25 => 16 + 25 => 26 + 26 => 18 + 26 => 19 + 26 => 16 + 26 => 20 + 26 => 21 + 25 => 21 + 8 => 11 + 2 => 27 + 27 => 4 + 27 => 11 + 2 => 28 + 28 => 29 + 29 => 4 + 28 => 8 + 28 => 30 + 30 => 11 + 28 => 27 + 28 => 31 + 31 => 32 + 31 => 33 + 33 => 34 + 28 => 33 + 28 => 35 + 35 => 8 + 35 => 30 + 35 => 33 + 35 => 36 + 36 => 37 + 36 => 38 + 35 => 11 + 35 => 34 + 28 => 4 + 28 => 39 + 39 => 3 + 39 => 5 + 39 => 8 + 39 => 31 + 39 => 33 + 39 => 40 + 40 => 41 + 40 => 26 + 28 => 40 + 28 => 42 + 42 => 8 + 42 => 27 + 42 => 31 + 42 => 33 + 42 => 35 + 42 => 4 + 42 => 43 + 43 => 4 + 43 => 44 + 44 => 14 + 44 => 32 + 44 => 31 + 44 => 22 + 44 => 11 + 44 => 45 + 45 => 31 + 45 => 11 + 45 => 32 + 43 => 31 + 43 => 11 + 43 => 34 + 42 => 11 + 28 => 11 + 2 => 31 + 2 => 33 + 2 => 39 + 2 => 4 + 2 => 46 + 46 => 8 + 46 => 47 + 46 => 31 + 46 => 33 + 46 => 48 + 46 => 4 + 46 => 44 + 46 => 42 + 46 => 11 + 2 => 49 + 49 => 8 + 49 => 32 + 49 => 31 + 49 => 40 + 49 => 44 + 49 => 11 + 49 => 45 + 49 => 38 + 2 => 40 + 2 => 50 + 2 => 44 + 2 => 11 + 2 => 45 + 2 => 26 + 2 => 38 + ])) + + extra_large_depgraph = SimpleDiGraph(Edge.([ + 1 => 2 + 2 => 3 + 3 => 4 + 3 => 5 + 2 => 6 + 6 => 7 + 2 => 8 + 8 => 9 + 9 => 10 + 10 => 11 + 10 => 12 + 12 => 13 + 13 => 7 + 10 => 13 + 9 => 14 + 14 => 10 + 14 => 15 + 15 => 11 + 14 => 16 + 14 => 7 + 14 => 17 + 17 => 18 + 18 => 7 + 17 => 19 + 17 => 20 + 17 => 7 + 17 => 21 + 17 => 22 + 14 => 3 + 14 => 23 + 23 => 19 + 23 => 17 + 23 => 24 + 24 => 19 + 24 => 20 + 24 => 17 + 24 => 21 + 24 => 22 + 23 => 22 + 14 => 12 + 9 => 25 + 25 => 26 + 25 => 27 + 27 => 28 + 9 => 17 + 9 => 24 + 8 => 25 + 8 => 14 + 8 => 29 + 2 => 14 + 2 => 30 + 30 => 31 + 31 => 32 + 32 => 33 + 31 => 14 + 31 => 34 + 34 => 12 + 31 => 35 + 35 => 33 + 35 => 12 + 31 => 25 + 31 => 27 + 31 => 36 + 36 => 14 + 36 => 34 + 36 => 27 + 36 => 37 + 37 => 38 + 37 => 39 + 36 => 12 + 36 => 28 + 31 => 33 + 31 => 40 + 40 => 41 + 41 => 33 + 41 => 42 + 42 => 43 + 40 => 42 + 40 => 14 + 40 => 25 + 40 => 27 + 40 => 44 + 44 => 45 + 44 => 24 + 31 => 44 + 31 => 46 + 46 => 14 + 46 => 35 + 46 => 25 + 46 => 27 + 46 => 36 + 46 => 33 + 46 => 47 + 47 => 33 + 47 => 48 + 48 => 15 + 48 => 26 + 48 => 25 + 48 => 3 + 48 => 12 + 48 => 49 + 49 => 25 + 49 => 12 + 49 => 26 + 47 => 25 + 47 => 12 + 47 => 28 + 46 => 12 + 31 => 12 + 30 => 50 + 50 => 41 + 50 => 51 + 50 => 42 + 50 => 14 + 50 => 35 + 50 => 31 + 50 => 25 + 50 => 27 + 50 => 40 + 50 => 33 + 50 => 52 + 52 => 14 + 52 => 53 + 52 => 25 + 52 => 27 + 52 => 54 + 52 => 33 + 52 => 48 + 52 => 46 + 52 => 12 + 50 => 55 + 55 => 14 + 55 => 26 + 55 => 25 + 55 => 44 + 55 => 48 + 55 => 12 + 55 => 49 + 55 => 39 + 50 => 44 + 50 => 56 + 50 => 48 + 50 => 12 + 50 => 49 + 50 => 24 + 50 => 39 + 30 => 57 + 57 => 41 + 57 => 42 + 57 => 14 + 57 => 34 + 57 => 35 + 57 => 25 + 57 => 58 + 57 => 27 + 57 => 36 + 57 => 33 + 57 => 40 + 57 => 47 + 57 => 59 + 59 => 41 + 59 => 60 + 59 => 42 + 57 => 46 + 57 => 12 + 30 => 51 + 30 => 25 + 30 => 40 + 30 => 52 + 30 => 61 + 61 => 51 + 61 => 42 + 61 => 25 + 61 => 62 + 62 => 41 + 62 => 59 + 62 => 63 + 62 => 42 + 62 => 64 + 62 => 65 + 65 => 66 + 61 => 44 + 2 => 67 + 2 => 68 + 2 => 69 + 69 => 25 + 69 => 70 + 69 => 12 + 69 => 49 + 2 => 71 + 71 => 7 + 71 => 72 + 72 => 23 + 72 => 17 + 2 => 44 + 2 => 17 + 2 => 73 + 73 => 17 + 73 => 74 + 74 => 17 + 73 => 25 + 2 => 75 + 75 => 31 + 75 => 50 + 75 => 57 + 75 => 51 + 75 => 25 + 75 => 76 + 76 => 41 + 76 => 42 + 76 => 14 + 76 => 25 + 76 => 27 + 76 => 77 + 77 => 42 + 77 => 78 + 77 => 79 + 79 => 43 + 77 => 80 + 80 => 41 + 80 => 42 + 80 => 81 + 81 => 33 + 81 => 42 + 81 => 65 + 80 => 60 + 80 => 33 + 80 => 65 + 80 => 59 + 77 => 12 + 77 => 43 + 76 => 36 + 76 => 33 + 76 => 40 + 76 => 44 + 76 => 46 + 76 => 12 + 75 => 52 + 75 => 61 + 2 => 39 + 2 => 23 + 2 => 25 + 2 => 48 + 2 => 82 + 82 => 10 + 82 => 50 + 82 => 14 + 82 => 45 + 82 => 25 + 82 => 7 + 82 => 83 + 83 => 84 + 83 => 26 + 83 => 80 + 83 => 44 + 83 => 12 + 82 => 55 + 82 => 80 + 82 => 56 + 82 => 12 + 82 => 49 + 82 => 39 + 2 => 19 + 2 => 50 + 2 => 85 + 85 => 6 + 2 => 86 + 86 => 10 + 86 => 69 + 86 => 25 + 86 => 68 + 86 => 17 + 86 => 23 + 86 => 24 + 2 => 72 + 2 => 7 + 2 => 24 + ])) +end # module + + diff --git a/test/min_dist_one.jl b/test/min_dist_one.jl index f3b22aa..9e0c11c 100644 --- a/test/min_dist_one.jl +++ b/test/min_dist_one.jl @@ -1,6 +1,4 @@ +@plottest quick_plot_solve(LayeredMinDistOne(), Examples.medium_pert) ref"medium_pert" -ref(fn) = joinpath(@__DIR__, "references", fn * ".png") - -xs, ys = solve_positions(LayeredMinDistOne(), Examples.medium_pert) - -@plottest quick_plot(Examples.medium_pert, xs, ys) ref("1") \ No newline at end of file +# Lays out very poorly due to running out of iterations +@plottest quick_plot_solve(LayeredMinDistOne(), Examples.chainrule_dependants) ref"chainrule_dependants" diff --git a/test/references/.DS_Store b/test/references/.DS_Store new file mode 100644 index 0000000..f5493e1 Binary files /dev/null and b/test/references/.DS_Store differ diff --git a/test/references/1.png b/test/references/1.png deleted file mode 100644 index 57ac036..0000000 Binary files a/test/references/1.png and /dev/null differ diff --git a/test/references/LayeredMinDistOne/cross.png b/test/references/LayeredMinDistOne/cross.png new file mode 100644 index 0000000..caa723e Binary files /dev/null and b/test/references/LayeredMinDistOne/cross.png differ diff --git a/test/references/LayeredMinDistOne/loop.png b/test/references/LayeredMinDistOne/loop.png new file mode 100644 index 0000000..b390bcc Binary files /dev/null and b/test/references/LayeredMinDistOne/loop.png differ diff --git a/test/references/LayeredMinDistOne/medium_pert.png b/test/references/LayeredMinDistOne/medium_pert.png new file mode 100644 index 0000000..1223242 Binary files /dev/null and b/test/references/LayeredMinDistOne/medium_pert.png differ diff --git a/test/references/LayeredMinDistOne/sankey_3twos.png b/test/references/LayeredMinDistOne/sankey_3twos.png new file mode 100644 index 0000000..c6382e4 Binary files /dev/null and b/test/references/LayeredMinDistOne/sankey_3twos.png differ diff --git a/test/references/LayeredMinDistOne/tiny_depgraph.png b/test/references/LayeredMinDistOne/tiny_depgraph.png new file mode 100644 index 0000000..4a8c161 Binary files /dev/null and b/test/references/LayeredMinDistOne/tiny_depgraph.png differ diff --git a/test/references/LayeredMinDistOne/tree.png b/test/references/LayeredMinDistOne/tree.png new file mode 100644 index 0000000..32cd898 Binary files /dev/null and b/test/references/LayeredMinDistOne/tree.png differ diff --git a/test/references/LayeredMinDistOne/two_lines.png b/test/references/LayeredMinDistOne/two_lines.png new file mode 100644 index 0000000..55a4173 Binary files /dev/null and b/test/references/LayeredMinDistOne/two_lines.png differ diff --git a/test/references/LayeredMinDistOne/xcross.png b/test/references/LayeredMinDistOne/xcross.png new file mode 100644 index 0000000..7295eb1 Binary files /dev/null and b/test/references/LayeredMinDistOne/xcross.png differ diff --git a/test/references/Zarate/cross.png b/test/references/Zarate/cross.png new file mode 100644 index 0000000..0f22ab2 Binary files /dev/null and b/test/references/Zarate/cross.png differ diff --git a/test/references/Zarate/loop.png b/test/references/Zarate/loop.png new file mode 100644 index 0000000..96c4927 Binary files /dev/null and b/test/references/Zarate/loop.png differ diff --git a/test/references/Zarate/medium_pert.png b/test/references/Zarate/medium_pert.png new file mode 100644 index 0000000..3a4fac7 Binary files /dev/null and b/test/references/Zarate/medium_pert.png differ diff --git a/test/references/Zarate/sankey_3twos.png b/test/references/Zarate/sankey_3twos.png new file mode 100644 index 0000000..de93d91 Binary files /dev/null and b/test/references/Zarate/sankey_3twos.png differ diff --git a/test/references/Zarate/tiny_depgraph.png b/test/references/Zarate/tiny_depgraph.png new file mode 100644 index 0000000..0001413 Binary files /dev/null and b/test/references/Zarate/tiny_depgraph.png differ diff --git a/test/references/Zarate/tree.png b/test/references/Zarate/tree.png new file mode 100644 index 0000000..3bab9c9 Binary files /dev/null and b/test/references/Zarate/tree.png differ diff --git a/test/references/Zarate/two_lines.png b/test/references/Zarate/two_lines.png new file mode 100644 index 0000000..72a8544 Binary files /dev/null and b/test/references/Zarate/two_lines.png differ diff --git a/test/references/Zarate/xcross.png b/test/references/Zarate/xcross.png new file mode 100644 index 0000000..aa67d77 Binary files /dev/null and b/test/references/Zarate/xcross.png differ diff --git a/test/references/quick_plot.png b/test/references/quick_plot.png deleted file mode 100644 index a9ca01c..0000000 Binary files a/test/references/quick_plot.png and /dev/null differ diff --git a/test/references/test_utils/quick_plot.png b/test/references/test_utils/quick_plot.png new file mode 100644 index 0000000..19344f7 Binary files /dev/null and b/test/references/test_utils/quick_plot.png differ diff --git a/test/runtests.jl b/test/runtests.jl index 4767b94..b231d55 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,10 +4,8 @@ using LayeredLayouts using Test using VisualRegressionTests -@testset "LayeredLayouts.jl" begin - include("test_utils.jl") - include("examples.jl") +include("examples.jl") - include("min_dist_one.jl") - +@testset "LayeredLayouts.jl" begin + include("demos.jl") end diff --git a/test/test_utils.jl b/test/test_utils.jl deleted file mode 100644 index 2302d0b..0000000 --- a/test/test_utils.jl +++ /dev/null @@ -1,21 +0,0 @@ -function quick_plot(graph, xs, ys) - nv(graph)==length(xs) == length(ys) || error("need 1 position per vertex") - scatter(xs, ys; markeralpha=0, text=string.(vertices(graph))) - - # now draw connections - lxs = Float64[] - lys = Float64[] - for edge in edges(graph) - append!(lxs, [xs[edge.src], xs[edge.dst], NaN]) - append!(lys, [ys[edge.src], ys[edge.dst], NaN]) - end - plot!(lxs, lys; legend=false) -end - -ref(fn) = joinpath(@__DIR__, "references", fn * ".png") - -@testset "test_utils.jl" begin - @testset "quick_plot" begin - @plottest quick_plot(SimpleDiGraph(Edge.([1=>2, 2=>3])), [1,2,5], [1,2,3]) joinpath(@__DIR__, "references", "quick_plot.png") - end -end