diff --git a/src/stress.jl b/src/stress.jl index c0fcc07..73579d5 100644 --- a/src/stress.jl +++ b/src/stress.jl @@ -1,4 +1,4 @@ -using LinearAlgebra: checksquare, norm, pinv, mul! +using LinearAlgebra: checksquare, norm, pinv, mul!, nullspace export Stress, stress @@ -57,8 +57,13 @@ The main equation to solve is (8) in Gansner, Koren and North (2005, Create rng based on seed. Defaults to `MersenneTwister`, can be specified by overwriting `DEFAULT_RNG[]` + +- `uncon_dist=(maxdist, Ncomps)->maxdist*Ncomps^(1/3)` + + Per default, unconnected vertices in the graph get a pairwise "ideal" distance which scales + with the number of connected components and the maximum distance within the components. """ -@addcall struct Stress{Dim,Ptype,IT<:Union{Symbol,Int},FT<:AbstractFloat,M<:AbstractMatrix,RNG} <: +@addcall struct Stress{Dim,Ptype,IT<:Union{Symbol,Int},FT<:AbstractFloat,M<:AbstractMatrix,F,RNG} <: IterativeLayout{Dim,Ptype} iterations::IT abstols::FT @@ -67,6 +72,7 @@ The main equation to solve is (8) in Gansner, Koren and North (2005, weights::M initialpos::Dict{Int,Point{Dim,Ptype}} pin::Dict{Int,SVector{Dim,Bool}} + uncon_dist::F rng::RNG end @@ -76,6 +82,7 @@ function Stress(; dim=2, abstols=0.0, reltols=10e-6, abstolx=10e-6, + uncon_dist=(maxd, N)->maxd*N^(1/3), weights=Array{Float64}(undef, 0, 0), initialpos=[], pin=[], seed=1, rng=DEFAULT_RNG[](seed)) @@ -86,8 +93,8 @@ function Stress(; dim=2, _initialpos, _pin = _sanitize_initialpos_pin(dim, Ptype, initialpos, pin) - IT, FT, WT, RNG = typeof(iterations), typeof(abstols), typeof(weights), typeof(rng) - Stress{dim,Ptype,IT,FT,WT,RNG}(iterations, abstols, reltols, abstolx, weights, _initialpos, _pin, rng) + IT, FT, WT, RNG, F = typeof(iterations), typeof(abstols), typeof(weights), typeof(rng), typeof(uncon_dist) + Stress{dim,Ptype,IT,FT,WT,F,RNG}(iterations, abstols, reltols, abstolx, weights, _initialpos, _pin, uncon_dist, rng) end function Base.iterate(iter::LayoutIterator{<:Stress{Dim,Ptype,IT,FT}}) where {Dim,Ptype,IT,FT} @@ -116,6 +123,20 @@ function Base.iterate(iter::LayoutIterator{<:Stress{Dim,Ptype,IT,FT}}) where {Di # if user provided weights not empty try those make_symmetric!(δ) distances = pairwise_distance(δ, FT) + + # check for unconnected commponents and set pairwise distances + if any(isequal(typemax(FT)), distances) + maxd = maximum(filter(isfinite, distances)) + laplacian = weightedlaplacian(δ) + Ncomponents = size(nullspace(laplacian),2) + _dist = algo.uncon_dist(maxd, Ncomponents) + for i in eachindex(distances) + if isinf(distances[i]) + distances[i] = _dist + end + end + end + weights = isempty(algo.weights) ? distances .^ (-2) : algo.weights @assert length(startpos) == size(δ, 1) == size(δ, 2) == size(weights, 1) == size(weights, 2) "Wrong size of weights?" diff --git a/test/runtests.jl b/test/runtests.jl index 190fbda..42efe2f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,6 +7,9 @@ using StaticArrays using StableRNGs using Test using Random +using LinearAlgebra + +NetworkLayout.DEFAULT_RNG[] = StableRNG function jagmesh() jagmesh_path = joinpath(dirname(@__FILE__), "jagmesh1.mtx") @@ -148,6 +151,40 @@ jagmesh_adj = jagmesh() 0 0 0 0 0]) end + + @testset "test unconnected graphs" begin + _δ = [0 1 1; + 1 0 1; + 1 1 0] + N = 10# 10 fully connected 3-node graphs + δ = kron(Matrix(I, N, N), _δ); + g = SimpleGraph(δ) + pos = Stress()(g) + @test all(norm.(pos) .< 2) + # graphplot(g; layout=Stress()) + + sgkeys = [:bull, :chvatal, :cubical, :desargues, + :diamond, :dodecahedral, :frucht, :heawood, + :house, :housex, :icosahedral, :karate, :krackhardtkite, + :moebiuskantor, :octahedral, :pappus, :petersen, + :sedgewickmaze, :tetrahedral, :truncatedcube, + :truncatedtetrahedron, :truncatedtetrahedron_dir, :tutte] + gs = filter(!is_directed, smallgraph.(sgkeys)) + absdim = mapreduce(nv, +, gs) + adj = zeros(Int, absdim, absdim); + let i = 1 + for g in gs + r = i:i+nv(g)-1 + adj[r, r] .= adjacency_matrix(g) + i += nv(g) + end + end + g = SimpleGraph(adj) + + pos = Stress()(g) + @test all(norm.(pos) .< 20) + # graphplot(g; layout=Stress()) + end end @testset "Testing Spring Algorithm" begin