Skip to content

Commit 6c2039c

Browse files
authored
Merge pull request #40 from JuliaGraphs/hw/fix_stress
fix stress layout
2 parents fef0acb + 0c5ba55 commit 6c2039c

File tree

4 files changed

+128
-27
lines changed

4 files changed

+128
-27
lines changed

docs/src/index.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,16 +111,22 @@ Stress
111111
```
112112
### Example
113113
```@example layouts
114-
g = complete_graph(10)
114+
g = SimpleGraph(936)
115+
for l in eachline(joinpath(@__DIR__,"..","..","test","jagmesh1.mtx"))
116+
s = split(l, " ")
117+
src, dst = parse(Int, s[1]), parse(Int, s[2])
118+
src != dst && add_edge!(g, src, dst)
119+
end
120+
115121
layout = Stress(Ptype=Float32)
116-
f, ax, p = graphplot(g, layout=layout)
122+
f, ax, p = graphplot(g; layout=layout, node_size=3, edge_width=1)
117123
hidedecorations!(ax); hidespines!(ax); ax.aspect = DataAspect(); f #hide
118124
```
119125

120126
### Iterator Example
121127
```@example layouts
122128
iterator = LayoutIterator(layout, g)
123-
record(f, "stress_animation.mp4", iterator; framerate = 10) do pos
129+
record(f, "stress_animation.mp4", iterator; framerate = 7) do pos
124130
p[:node_pos][] = pos
125131
autolimits!(ax)
126132
end

src/NetworkLayout.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,28 @@ function assertsquare(M::AbstractMatrix)
107107
return a
108108
end
109109

110+
"""
111+
make_symmetric!(M::AbstractMatrix)
112+
113+
Pairwise check [i,j] and [j,i]. If one is zero, make symmetric.
114+
If both are different and nonzero throw ArgumentError.
115+
"""
116+
function make_symmetric!(A::AbstractMatrix)
117+
indsm, indsn = axes(A)
118+
for i in first(indsn):last(indsn), j in (i):last(indsn)
119+
if A[i,j] == A[j,i] # allready symmetric
120+
continue
121+
elseif iszero(A[i,j])
122+
A[i,j] = A[j,i]
123+
elseif iszero(A[j,i])
124+
A[j,i] = A[i,j]
125+
else
126+
throw(ArgumentError("Matrix can't be symmetrized!"))
127+
end
128+
end
129+
return A
130+
end
131+
110132
"""
111133
@addcall
112134

src/stress.jl

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,15 @@ The main equation to solve is (8) of:
7272
seed::UInt
7373
end
7474

75-
function Stress(; dim=2, Ptype=Float64, iterations=:auto, abstols=((eps(Float64))),
76-
reltols=((eps(Float64))), abstolx=((eps(Float64))), weights=Array{Float64}(undef, 0, 0),
77-
initialpos=Point{dim,Ptype}[], seed=1)
75+
function Stress(; dim=2,
76+
Ptype=Float64,
77+
iterations=:auto,
78+
abstols=0.0,
79+
reltols=10e-6,
80+
abstolx=10e-6,
81+
weights=Array{Float64}(undef, 0, 0),
82+
initialpos=Point{dim,Ptype}[],
83+
seed=1)
7884
if !isempty(initialpos)
7985
initialpos = Point.(initialpos)
8086
Ptype = eltype(eltype(initialpos))
@@ -86,13 +92,6 @@ function Stress(; dim=2, Ptype=Float64, iterations=:auto, abstols=(√(eps(Float
8692
Stress{dim,Ptype,IT,FT,WT}(iterations, abstols, reltols, abstolx, weights, initialpos, seed)
8793
end
8894

89-
function initialweights(D, T)::SparseMatrixCSC{T,Int64}
90-
map(D) do d
91-
x = T(d^(-2.0))
92-
return isfinite(x) ? x : zero(T)
93-
end
94-
end
95-
9695
function Base.iterate(iter::LayoutIterator{<:Stress{Dim,Ptype,IT,FT}}) where {Dim,Ptype,IT,FT}
9796
algo, δ = iter.algorithm, iter.adj_matrix
9897
N = size(δ, 1)
@@ -109,45 +108,46 @@ function Base.iterate(iter::LayoutIterator{<:Stress{Dim,Ptype,IT,FT}}) where {Di
109108
end
110109

111110
# calculate iteration if :auto
112-
maxiter = algo.iterations === :auto ? 400 * size(δ, 1)^2 : algo.iterations
111+
maxiter = algo.iterations === :auto ? 400 * N^2 : algo.iterations
113112
@assert maxiter > 0 "Iterations need to be > 0"
114113

115114
# if user provided weights not empty try those
116-
weights = isempty(algo.weights) ? initialweights(δ, FT) : algo.weights
115+
make_symmetric!(δ)
116+
distances = pairwise_distance(δ, FT)
117+
weights = isempty(algo.weights) ? distances .^ (-2) : algo.weights
117118

118119
@assert length(startpos) == size(δ, 1) == size(δ, 2) == size(weights, 1) == size(weights, 2) "Wrong size of weights?"
119120

120121
Lw = weightedlaplacian(weights)
121122
pinvLw = pinv(Lw)
122-
s = stress(startpos, δ, weights)
123+
oldstress = stress(startpos, distances, weights)
123124

124-
# the `state` of the iterator is (#iter, old stress, old pos, weights, pinvLw, stopflag)
125-
return startpos, (1, s, startpos, weights, pinvLw, maxiter, false)
125+
# the `state` of the iterator is (#iter, old stress, old pos, weights, distances pinvLw, stopflag)
126+
return startpos, (1, oldstress, startpos, weights, distances, pinvLw, maxiter, false)
126127
end
127128

128129
function Base.iterate(iter::LayoutIterator{<:Stress{Dim,Ptype}}, state) where {Dim,Ptype}
129130
algo, δ = iter.algorithm, iter.adj_matrix
130-
i, oldstress, oldpos, weights, pinvLw, maxiter, stopflag = state
131-
# newstress, oldstress, X0, i = state
131+
i, oldstress, oldpos, weights, distances, pinvLw, maxiter, stopflag = state
132132

133133
if i >= maxiter || stopflag
134134
return nothing
135135
end
136136

137137
# TODO the faster way is to drop the first row and col from the iteration
138-
t = LZ(oldpos, δ, weights)
138+
t = LZ(oldpos, distances, weights)
139139
positions = similar(oldpos) # allocate new array but keep type of oldpos
140140
mul!(positions, pinvLw, (t * oldpos))
141141
@assert all(x -> all(map(isfinite, x)), positions)
142-
newstress = stress(positions, δ, weights)
142+
newstress = stress(positions, distances, weights)
143143

144144
if abs(newstress - oldstress) < algo.reltols * newstress ||
145145
abs(newstress - oldstress) < algo.abstols ||
146146
norm(positions - oldpos) < algo.abstolx
147147
stopflag = true
148148
end
149149

150-
return positions, (i + 1, newstress, positions, weights, pinvLw, maxiter, stopflag)
150+
return positions, (i + 1, newstress, positions, weights, distances, pinvLw, maxiter, stopflag)
151151
end
152152

153153
"""
@@ -160,8 +160,7 @@ Input:
160160
161161
See (1) of Reference
162162
"""
163-
function stress(positions::AbstractArray{Point{T,N}}, d=ones(T, length(positions), length(positions)),
164-
weights=initialweights(d, T)) where {T,N}
163+
function stress(positions::AbstractArray{Point{T,N}}, d, weights) where {T,N}
165164
s = zero(T)
166165
n = length(positions)
167166
@assert n == size(d, 1) == size(d, 2) == size(weights, 1) == size(weights, 2)
@@ -196,8 +195,8 @@ end
196195
Computes L^Z defined in (5) of the Reference
197196
198197
Input: Z: current layout (coordinates)
199-
d: Ideal distances (default: all 1)
200-
weights: weights (default: d.^-2)
198+
d: Ideal distances
199+
weights: weights
201200
"""
202201
function LZ(Z::AbstractVector{Point{N,T}}, d, weights) where {N,T}
203202
n = length(Z)
@@ -217,3 +216,32 @@ function LZ(Z::AbstractVector{Point{N,T}}, d, weights) where {N,T}
217216
end
218217
return L
219218
end
219+
220+
"""
221+
pairwise_distance(δ)
222+
223+
Calculate the pairwise distances of a the graph from a adjacency matrix
224+
using the Floyd-Warshall algorithm.
225+
226+
https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm
227+
"""
228+
function pairwise_distance(δ, ::Type{T}=Float64) where {T}
229+
N = size(δ, 1)
230+
d = Matrix{T}(undef, N, N)
231+
@inbounds for j in 1:N, i in 1:N
232+
if i == j
233+
d[i, j] = zero(eltype(d))
234+
elseif iszero(δ[i, j])
235+
d[i, j] = typemax(eltype(d))
236+
else
237+
d[i, j] = δ[i, j]
238+
end
239+
end
240+
241+
@inbounds for k in 1:N, i in 1:N, j in 1:N
242+
if d[i, k] + d[k, j] < d[i, j]
243+
d[i, j] = d[i, k] + d[k, j]
244+
end
245+
end
246+
return d
247+
end

test/runtests.jl

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,23 @@ jagmesh_adj = jagmesh()
112112
@test typeof(positions) == Vector{Point3f}
113113
@test positions == stress(adj_matrix; iterations=10, dim=3, Ptype=Float32)
114114
end
115+
116+
@testset "test pairwise_distance" begin
117+
using NetworkLayout: make_symmetric!, pairwise_distance
118+
δ = [0 1 0 0 1;
119+
1 0 0 1 0;
120+
0 0 0 1 1;
121+
0 1 1 0 0;
122+
1 0 1 0 0]
123+
d = pairwise_distance(δ)
124+
125+
@test d == make_symmetric!([0 1 2 2 1;
126+
0 0 2 1 2;
127+
0 0 0 1 1;
128+
0 0 0 0 2;
129+
0 0 0 0 0])
130+
131+
end
115132
end
116133

117134
@testset "Testing Spring Algorithm" begin
@@ -299,4 +316,32 @@ jagmesh_adj = jagmesh()
299316
@test_throws ArgumentError squaregrid(M1)
300317
@test_throws ArgumentError stress(M1)
301318
end
319+
320+
@testset "make_symmetric" begin
321+
using LinearAlgebra: issymmetric
322+
using NetworkLayout: make_symmetric!
323+
324+
M = [1 0; 0 1]
325+
make_symmetric!(M)
326+
@test issymmetric(M)
327+
328+
M = [0 1; 0 0]
329+
make_symmetric!(M)
330+
@test issymmetric(M)
331+
332+
M = [0 0; 1 0]
333+
make_symmetric!(M)
334+
@test issymmetric(M)
335+
336+
M = [0 -1; 1 0]
337+
@test_throws ArgumentError make_symmetric!(M)
338+
339+
M = [1 0 2;
340+
6 2 4;
341+
2 0 3]
342+
make_symmetric!(M)
343+
@test M == [1 6 2;
344+
6 2 4;
345+
2 4 3]
346+
end
302347
end

0 commit comments

Comments
 (0)