diff --git a/autolayout.go b/autolayout.go
index c42e4cd..1b4f681 100644
--- a/autolayout.go
+++ b/autolayout.go
@@ -5,7 +5,6 @@ import (
"github.com/nulab/autog/graph"
ig "github.com/nulab/autog/internal/graph"
- "github.com/nulab/autog/internal/graph/connected"
imonitor "github.com/nulab/autog/internal/monitor"
"github.com/nulab/autog/internal/processor"
"github.com/nulab/autog/internal/processor/postprocessor"
@@ -55,7 +54,7 @@ func Layout(source graph.Source, opts ...Option) graph.Layout {
shift := 0.0
// process each connected components and collect results into the same layout output
- for _, g := range connected.Components(G) {
+ for _, g := range G.ConnectedComponents() {
if len(g.Nodes) == 0 {
panic("autog: connected sub-graph node set is empty: this might be a bug")
}
diff --git a/autolayout_options.go b/autolayout_options.go
index 0b7c229..af59851 100644
--- a/autolayout_options.go
+++ b/autolayout_options.go
@@ -36,6 +36,7 @@ var defaultOptions = options{
NetworkSimplexThoroughness: 28,
NetworkSimplexMaxIterFactor: 0,
NetworkSimplexBalance: graph.OptionNsBalanceV,
+ VirtualNodeFixedSize: 0.0,
WMedianMaxIter: 24,
NetworkSimplexAuxiliaryGraphWeightFactor: 4,
LayerSpacing: 150.0,
diff --git a/autolayout_options_funcs.go b/autolayout_options_funcs.go
index 08c9bb2..eaeddd7 100644
--- a/autolayout_options_funcs.go
+++ b/autolayout_options_funcs.go
@@ -50,6 +50,12 @@ func WithNodeFixedSize(w, h float64) Option {
}
}
+func WithVirtualNodeFixedSize(n float64) Option {
+ return func(o *options) {
+ o.params.VirtualNodeFixedSize = n
+ }
+}
+
func WithBrandesKoepfLayout(i int) Option {
return func(o *options) {
o.params.BrandesKoepfLayout = i
diff --git a/autolayout_options_test.go b/autolayout_options_test.go
index d1ccfdc..f501b33 100644
--- a/autolayout_options_test.go
+++ b/autolayout_options_test.go
@@ -16,6 +16,7 @@ func TestOptions(t *testing.T) {
// set some random options that are different from the defaults
opts := testOptions(
WithCycleBreaking(CycleBreakingDepthFirst),
+ WithLayering(LayeringLongestPath),
WithOrdering(OrderingNoop),
WithPositioning(PositioningVAlign),
WithEdgeRouting(EdgeRoutingStraight),
@@ -25,10 +26,12 @@ func TestOptions(t *testing.T) {
WithBrandesKoepfLayout(2),
WithNodeFixedSize(100.0, 100.0),
WithNodeSize(map[string]graph.Size{"N1": {W: 20, H: 20}}),
+ WithVirtualNodeFixedSize(50.0),
WithNonDeterministicGreedyCycleBreaker(),
)
assert.Equal(t, phase1.DepthFirst, opts.p1)
+ assert.Equal(t, phase2.LongestPath, opts.p2)
assert.Equal(t, phase3.NoOrdering, opts.p3)
assert.Equal(t, phase4.VerticalAlign, opts.p4)
assert.Equal(t, phase5.Straight, opts.p5)
@@ -40,6 +43,7 @@ func TestOptions(t *testing.T) {
assert.Nil(t, opts.monitor)
assert.NotNil(t, opts.params.NodeFixedSizeFunc)
assert.NotNil(t, opts.params.NodeSizeFunc)
+ assert.Equal(t, 50.0, opts.params.VirtualNodeFixedSize)
assert.True(t, opts.params.GreedyCycleBreakerRandomNodeChoice)
assert.Equal(t, CycleBreakingGreedy, phase1.Greedy)
diff --git a/autolayout_test.go b/autolayout_test.go
new file mode 100644
index 0000000..1984768
--- /dev/null
+++ b/autolayout_test.go
@@ -0,0 +1,392 @@
+package autog
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/nulab/autog/graph"
+ ig "github.com/nulab/autog/internal/graph"
+ imonitor "github.com/nulab/autog/internal/monitor"
+ "github.com/stretchr/testify/assert"
+)
+
+var dotAbstract = [][]string{
+ {"S24", "27"},
+ {"S24", "25"},
+ {"S1", "10"},
+ {"S1", "2"},
+ {"S35", "36"},
+ {"S35", "43"},
+ {"S30", "31"},
+ {"S30", "33"},
+ {"9", "42"},
+ {"9", "T1"},
+ {"25", "T1"},
+ {"25", "26"},
+ {"27", "T24"},
+ {"2", "3"},
+ {"2", "16"},
+ {"2", "17"},
+ {"2", "T1"},
+ {"2", "18"},
+ {"10", "11"},
+ {"10", "14"},
+ {"10", "T1"},
+ {"10", "13"},
+ {"10", "12"},
+ {"31", "T1"},
+ {"31", "32"},
+ {"33", "T30"},
+ {"33", "34"},
+ {"42", "4"},
+ {"26", "4"},
+ {"3", "4"},
+ {"16", "15"},
+ {"17", "19"},
+ {"18", "29"},
+ {"11", "4"},
+ {"14", "15"},
+ {"37", "39"},
+ {"37", "41"},
+ {"37", "38"},
+ {"37", "40"},
+ {"13", "19"},
+ {"12", "29"},
+ {"43", "38"},
+ {"43", "40"},
+ {"36", "19"},
+ {"32", "23"},
+ {"34", "29"},
+ {"39", "15"},
+ {"41", "29"},
+ {"38", "4"},
+ {"40", "19"},
+ {"4", "5"},
+ {"19", "21"},
+ {"19", "20"},
+ {"19", "28"},
+ {"5", "6"},
+ {"5", "T35"},
+ {"5", "23"},
+ {"21", "22"},
+ {"20", "15"},
+ {"28", "29"},
+ {"6", "7"},
+ {"15", "T1"},
+ {"22", "23"},
+ {"22", "T35"},
+ {"29", "T30"},
+ {"7", "T8"},
+ {"23", "T24"},
+ {"23", "T1"},
+}
+
+func TestLayoutBugfix(t *testing.T) {
+ t.Run("output layout empty nodes", func(t *testing.T) {
+ src := graph.EdgeSlice([][]string{
+ {"N1", "N2"},
+ {"N2", "N3"},
+ {"N1", "N3"},
+ })
+ layout := Layout(
+ src,
+ WithPositioning(PositioningVAlign),
+ WithEdgeRouting(EdgeRoutingNoop),
+ )
+ assert.Len(t, layout.Nodes, 3)
+ assert.Len(t, layout.Edges, 3)
+ })
+
+ t.Run("self-loop", func(t *testing.T) {
+ t.Run("program halts", func(t *testing.T) {
+ src := graph.EdgeSlice([][]string{
+ {"a", "b"},
+ {"b", "c"},
+ {"b", "b"},
+ {"a", "d"},
+ })
+ assert.NotPanics(t, func() { _ = Layout(src) })
+ })
+ t.Run("successful with single node", func(t *testing.T) {
+ src := graph.EdgeSlice([][]string{
+ {"a", "a"},
+ })
+ assert.NotPanics(t, func() { _ = Layout(src) })
+ })
+ })
+
+ t.Run("greedy cycle breaker fails to break cycles", func(t *testing.T) {
+ g := graph.EdgeSlice([][]string{
+ {"N1", "N4"},
+ {"N1", "N8"},
+ {"N2", "N5"},
+ {"N2", "N8"},
+ {"N3", "N8"},
+ {"N6", "N3"},
+ {"N8", "N1"},
+ {"N8", "N2"},
+ {"N8", "N7"},
+ {"N8", "N15"},
+ {"N8", "N16"},
+ {"N9", "N10"},
+ {"N10", "N11"},
+ {"N12", "N13"},
+ {"N13", "N14"},
+ {"N15", "N1"},
+ {"N15", "N9"},
+ {"N15", "N10"},
+ {"N16", "N2"},
+ {"N16", "N12"},
+ {"N16", "N13"},
+ })
+ assert.NotPanics(t, func() {
+ _ = Layout(
+ g,
+ WithNonDeterministicGreedyCycleBreaker(),
+ )
+ })
+ })
+
+ t.Run("wmedian", func(t *testing.T) {
+ t.Run("identical edge segfault in cross counting", func(t *testing.T) {
+ src := graph.EdgeSlice([][]string{
+ {"gql", "acc"},
+ {"gql", "dia"},
+ {"gql", "edt"},
+ {"gql", "fld"},
+ {"gql", "itg"},
+ {"gql", "ntf"},
+ {"gql", "org"},
+ {"gql", "sub"},
+ {"gql", "spt"},
+ {"gql", "tmp"},
+ {"acc", "lgc"},
+ {"acc", "sub"},
+ {"fld", "acc"},
+ {"fld", "dia"},
+ {"fld", "org"},
+ {"fld", "sub"},
+ {"dia", "acc"},
+ {"dia", "fld"},
+ {"dia", "lgc"},
+ {"dia", "org"},
+ {"dia", "sub"},
+ })
+ assert.NotPanics(t, func() { _ = Layout(src) })
+ })
+
+ t.Run("wrong initialization of flat edges", func(t *testing.T) {
+ src := graph.EdgeSlice([][]string{
+ {"gql", "acc"},
+ {"gql", "dia"},
+ {"gql", "edt"},
+ {"gql", "fld"},
+ {"gql", "itg"},
+ {"gql", "ntf"},
+ {"gql", "org"},
+ {"gql", "sub"},
+ {"gql", "spt"},
+ {"gql", "tmp"},
+ {"acc", "lgc"},
+ {"acc", "sub"},
+ {"dia", "acc"},
+ {"dia", "fld"},
+ {"dia", "lgc"},
+ {"dia", "org"},
+ {"dia", "sub"},
+ {"fld", "acc"},
+ {"fld", "dia"},
+ {"fld", "org"},
+ {"fld", "sub"},
+ })
+ assert.NotPanics(t, func() { _ = Layout(src) })
+ })
+
+ t.Run("wrong handling of fixed positions in wmedian", func(t *testing.T) {
+ c := make(chan any, 1)
+ assert.NotPanics(t, func() {
+ _ = Layout(
+ graph.EdgeSlice(dotAbstract),
+ WithPositioning(PositioningNoop),
+ WithEdgeRouting(EdgeRoutingNoop),
+ WithMonitor(imonitor.NewFilteredChan(c, imonitor.MatchAll(3, "gvdot", "crossings"))),
+ )
+ })
+
+ assert.Equal(t, 46, <-c)
+ })
+ })
+
+ t.Run("network simplex positioner no panic", func(t *testing.T) {
+ src := graph.EdgeSlice(dotAbstract)
+ assert.NotPanics(t, func() {
+ _ = Layout(
+ src,
+ WithPositioning(PositioningNetworkSimplex),
+ WithEdgeRouting(EdgeRoutingNoop),
+ WithNodeFixedSize(100, 100),
+ )
+ })
+ })
+
+ t.Run("b&k no overlaps", func(t *testing.T) {
+ g := &ig.DGraph{}
+ graph.EdgeSlice([][]string{
+ {"a", "b"},
+ {"b", "c"},
+ {"a", "f"},
+ {"f", "g"},
+ {"a", "u"},
+ {"f", "c"},
+ {"c", "k"},
+ {"f", "k"},
+ }).Populate(g)
+ _ = Layout(g,
+ WithPositioning(PositioningBrandesKoepf),
+ WithNodeFixedSize(130, 60),
+ )
+
+ overlaps := 0
+ for _, l := range g.Layers {
+ for j := 1; j < l.Len(); j++ {
+ cur := l.Nodes[j]
+ prv := l.Nodes[j-1]
+
+ if prv.X+prv.W > cur.X {
+ if overlaps >= 0 {
+ // note: this isn't a strict inequality because virtual nodes have size 0x0
+ assert.Truef(t, prv.X+prv.W <= cur.X, "%s(X:%.2f) overlaps %s(X+W:%.2f)", cur, cur.X, prv, prv.X+prv.W)
+ } else {
+ fmt.Printf("warning: overlap between nodes %v and %v within tolerance\n", cur, prv)
+ }
+ overlaps++
+ }
+ }
+ }
+ })
+
+ t.Run("sink coloring program hangs", func(t *testing.T) {
+ src := graph.EdgeSlice([][]string{
+ {"N1", "N2"},
+ {"N3", "N1"},
+ {"N2", "N3"},
+ {"Nh", "N1"},
+ {"Nk", "N1"},
+ {"Na", "N2"},
+ {"Na", "N3"},
+ {"N2", "Nd"},
+ })
+ assert.NotPanics(t, func() { _ = Layout(src, WithPositioning(PositioningSinkColoring)) })
+ })
+}
+
+func TestOutputVirtualNodes(t *testing.T) {
+ src := graph.EdgeSlice([][]string{
+ {"N1", "N2"},
+ {"N2", "N3"},
+ {"N1", "N3"},
+ })
+ t.Run("keep virtual nodes", func(t *testing.T) {
+ g := &ig.DGraph{}
+ src.Populate(g)
+ layout := Layout(
+ g,
+ WithPositioning(PositioningVAlign),
+ WithEdgeRouting(EdgeRoutingNoop),
+ WithOutputVirtualNodes(true),
+ )
+ assert.Len(t, layout.Nodes, 4)
+ assert.Len(t, layout.Edges, 3)
+ })
+
+ t.Run("clip output nodes", func(t *testing.T) {
+ g := &ig.DGraph{}
+ src.Populate(g)
+ layout := Layout(
+ g,
+ WithPositioning(PositioningVAlign),
+ WithEdgeRouting(EdgeRoutingNoop),
+ WithOutputVirtualNodes(false),
+ )
+ assert.Equal(t, 3, len(layout.Nodes))
+ assert.Equal(t, 3, cap(layout.Nodes))
+ assert.Equal(t, 4, len(g.Nodes))
+
+ assert.Len(t, layout.Edges, 3)
+ })
+}
+
+func TestNoRegression(t *testing.T) {
+ t.Run("ELK", func(t *testing.T) {
+ var lib_decg_DECGPi = [][]string{
+ {"N2", "N8"},
+ {"N2", "N13"},
+ {"N2", "N15"},
+ {"N2", "N4"},
+ {"N3", "N1"},
+ {"N4", "N3"},
+ {"N5", "N16"},
+ {"N5", "N18"},
+ {"N6", "N8"},
+ {"N6", "N18"},
+ {"N7", "N6"},
+ {"N8", "N5"},
+ {"N8", "N9"},
+ {"N9", "N6"},
+ {"N9", "N7"},
+ {"N10", "N14"},
+ {"N10", "N19"},
+ {"N11", "N10"},
+ {"N12", "N10"},
+ {"N12", "N11"},
+ {"N13", "N14"},
+ {"N14", "N17"},
+ {"N14", "N12"},
+ {"N15", "N13"},
+ {"N16", "N4"},
+ {"N17", "N16"},
+ {"N17", "N19"},
+ {"N18", "N5"},
+ {"N19", "N17"},
+ }
+
+ var pn_brockackerman_BrockAckerman = [][]string{
+ {"N1", "N2"},
+ {"N2", "N9"},
+ {"N4", "N10"},
+ {"N4", "N15"},
+ {"N5", "N6"},
+ {"N6", "N14"},
+ {"N8", "N1"},
+ {"N8", "N3"},
+ {"N9", "N12"},
+ {"N10", "N11"},
+ {"N11", "N12"},
+ {"N12", "N8"},
+ {"N13", "N5"},
+ {"N13", "N7"},
+ {"N14", "N16"},
+ {"N15", "N16"},
+ {"N16", "N13"},
+ }
+ var elkTestGraphs = []struct {
+ name string
+ adj [][]string
+ }{
+ {"lib_decg_DECGPi", lib_decg_DECGPi},
+ {"pn_brockackerman_BrockAckerman", pn_brockackerman_BrockAckerman},
+ }
+
+ for _, testcase := range elkTestGraphs {
+ t.Run(testcase.name, func(t *testing.T) {
+ assert.NotPanics(t, func() { Layout(graph.EdgeSlice(testcase.adj)) })
+ })
+ }
+ })
+
+ t.Run("Dot abstract with default options", func(t *testing.T) {
+ assert.NotPanics(t, func() {
+ Layout(graph.EdgeSlice(dotAbstract))
+ })
+ })
+}
diff --git a/graph/source.go b/graph/source.go
index 5b8c472..07e0b74 100644
--- a/graph/source.go
+++ b/graph/source.go
@@ -2,8 +2,7 @@ package graph
import ig "github.com/nulab/autog/internal/graph"
-// Source represent the source of graph data. It hides the implementation details of the internal DGraph struct
-// and allows only this module to provide implementations.
-type Source interface {
- Populate(*ig.DGraph)
-}
+type (
+ Source = ig.Source
+ EdgeSlice = ig.EdgeSlice
+)
diff --git a/internal/collectors/deque_test.go b/internal/collectors/deque_test.go
index b1bc8d6..c230693 100644
--- a/internal/collectors/deque_test.go
+++ b/internal/collectors/deque_test.go
@@ -8,6 +8,7 @@ import (
func TestDeque(t *testing.T) {
deq := NewDeque[string](10)
+ // w2 w1 u p v w3
deq.PushFront("p")
deq.PushFront("u")
deq.PushBack("v")
@@ -15,9 +16,11 @@ func TestDeque(t *testing.T) {
deq.PushFront("w2")
deq.PushBack("w3")
assert.Equal(t, 6, deq.Len())
- assert.Equal(t, 6, deq.f) // 10-4
- assert.Equal(t, 11, deq.b) // 9+2
+ assert.Equal(t, 6, deq.Front()) // 10-4
+ assert.Equal(t, 11, deq.Back()) // 9+2
assert.Equal(t, "v", deq.data[10])
assert.Equal(t, "w2", deq.PopFront())
assert.Equal(t, "w3", deq.PopBack())
+ assert.Equal(t, "w1", deq.PeekFront(1))
+ assert.Equal(t, "v", deq.PeekBack(1))
}
diff --git a/internal/collectors/mat.go b/internal/collectors/mat.go
index d9bd0ec..2e6f81f 100644
--- a/internal/collectors/mat.go
+++ b/internal/collectors/mat.go
@@ -2,7 +2,11 @@ package collectors
type Mat[T any] [][]T
+// NewMat creates a new n-by-n square matrix. All entries are T's zero value. A negative n is treated as 0.
func NewMat[T any](n int) Mat[T] {
+ if n < 0 {
+ n = 0
+ }
m := make([][]T, n)
for i := range m {
m[i] = make([]T, n)
diff --git a/internal/collectors/mat_test.go b/internal/collectors/mat_test.go
new file mode 100644
index 0000000..1a1b82b
--- /dev/null
+++ b/internal/collectors/mat_test.go
@@ -0,0 +1,20 @@
+package collectors
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMat(t *testing.T) {
+ var m Mat[int]
+
+ m = NewMat[int](0)
+ assert.Len(t, m, 0)
+
+ m = NewMat[int](3)
+ assert.Len(t, m, 3)
+
+ m = NewMat[int](-1)
+ assert.Len(t, m, 0)
+}
diff --git a/internal/collectors/stack.go b/internal/collectors/stack.go
deleted file mode 100644
index 6ba8df9..0000000
--- a/internal/collectors/stack.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package collectors
-
-type stack[T any] struct {
- ts []T
-}
-
-func NewStack[T any](n int) *stack[T] {
- return &stack[T]{make([]T, 0, n)}
-}
-
-func (s *stack[T]) Push(vs ...T) {
- s.ts = append(s.ts, vs...)
-}
-
-func (s *stack[T]) Pop() (t T) {
- t = s.ts[s.Len()-1]
- s.ts = s.ts[:s.Len()-1]
- return
-}
-
-func (s *stack[T]) Peek(n int) T {
- return s.ts[s.Len()-n]
-}
-
-func (s *stack[T]) Len() int {
- return len(s.ts)
-}
diff --git a/internal/geom/point.go b/internal/geom/point.go
index 568cefd..da22531 100644
--- a/internal/geom/point.go
+++ b/internal/geom/point.go
@@ -69,7 +69,7 @@ func norm(p P) P {
return p
}
-func rotate(p P, theta float64) P {
+func rotatep(p P, theta float64) P {
cs, sn := math.Cos(theta), math.Sin(theta)
return P{
X: p.X*cs - p.Y*sn,
diff --git a/internal/geom/point_test.go b/internal/geom/point_test.go
new file mode 100644
index 0000000..aead922
--- /dev/null
+++ b/internal/geom/point_test.go
@@ -0,0 +1,66 @@
+package geom
+
+import (
+ "math"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPoint(t *testing.T) {
+ t.Run("svg", func(t *testing.T) {
+ p := P{10.8, 50.1}
+ assert.Equal(t, ``, p.SVG())
+ })
+
+ t.Run("addp", func(t *testing.T) {
+ a := P{1, 2}
+ b := P{3, 4}
+ assert.Equal(t, P{4, 6}, addp(a, b))
+ })
+
+ t.Run("subp", func(t *testing.T) {
+ a := P{5, 6}
+ b := P{3, 4}
+ assert.Equal(t, P{2, 2}, subp(a, b))
+ })
+
+ t.Run("scalep", func(t *testing.T) {
+ p := P{2, 3}
+ assert.Equal(t, P{4, 6}, scalep(p, 2.0))
+ })
+
+ t.Run("dotp", func(t *testing.T) {
+ a := P{1, 2}
+ b := P{3, 4}
+ assert.Equal(t, 11.0, dotp(a, b))
+ })
+
+ t.Run("distp", func(t *testing.T) {
+ a := P{0, 0}
+ b := P{3, 4}
+ assert.Equal(t, 5.0, distp(a, b))
+ })
+
+ t.Run("sqdistp", func(t *testing.T) {
+ a := P{0, 0}
+ b := P{3, 4}
+ assert.Equal(t, 25.0, sqdistp(a, b))
+ })
+
+ t.Run("norm", func(t *testing.T) {
+ a := P{3, 4}
+ got := norm(a)
+ want := P{0.6, 0.8}
+ assert.InDelta(t, want.X, got.X, 1e-9)
+ assert.InDelta(t, want.Y, got.Y, 1e-9)
+ })
+
+ t.Run("rotatep", func(t *testing.T) {
+ p := P{1, 0}
+ got := rotatep(p, math.Pi/2)
+ want := P{0, 1}
+ assert.InDelta(t, want.X, got.X, 1e-9)
+ assert.InDelta(t, want.Y, got.Y, 1e-9)
+ })
+}
diff --git a/internal/geom/rect.go b/internal/geom/rect.go
index 2e7784b..c66a9be 100644
--- a/internal/geom/rect.go
+++ b/internal/geom/rect.go
@@ -15,7 +15,7 @@ func (r Rect) String() string {
func (r Rect) SVG() string {
width := r.BR.X - r.TL.X
height := r.BR.Y - r.TL.Y
- return fmt.Sprintf(``, r.TL.X, r.TL.Y, width, height)
+ return fmt.Sprintf(``, r.TL.X, r.TL.Y, width, height)
}
func (r Rect) Width() float64 {
diff --git a/internal/geom/rect_test.go b/internal/geom/rect_test.go
new file mode 100644
index 0000000..181b126
--- /dev/null
+++ b/internal/geom/rect_test.go
@@ -0,0 +1,15 @@
+package geom
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRect(t *testing.T) {
+ r := Rect{TL: P{10, 20}, BR: P{50, 30}}
+ assert.Equal(t, 40.0, r.Width())
+ assert.Equal(t, 10.0, r.Height())
+ assert.Equal(t, ``, r.SVG())
+
+}
diff --git a/internal/geom/spline_make.go b/internal/geom/spline_make.go
index 290bf8a..14061fc 100644
--- a/internal/geom/spline_make.go
+++ b/internal/geom/spline_make.go
@@ -21,8 +21,8 @@ func MakeSpline(a, b P) ctrlp {
theta2 := sign(-v.X) * math.Pi * (9.0 / 10.0)
// rotate the vector
- r := rotate(v, theta1)
- q := rotate(v, theta2)
+ r := rotatep(v, theta1)
+ q := rotatep(v, theta2)
// slide the control points along the rotated vector
p1 := P{a.X + k*r.X, a.Y + k*r.Y}
p2 := P{b.X + k*q.X, b.Y + k*q.Y}
diff --git a/internal/geom/spline_make_test.go b/internal/geom/spline_make_test.go
new file mode 100644
index 0000000..568bdd3
--- /dev/null
+++ b/internal/geom/spline_make_test.go
@@ -0,0 +1,55 @@
+package geom
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMakeSpline(t *testing.T) {
+ const delta = 1e-2
+
+ t.Run("vertically aligned", func(t *testing.T) {
+ a := P{20, 30}
+ b := P{20, 50}
+ s := MakeSpline(a, b)
+ assert.True(t, s.p0 == s.p1)
+ assert.True(t, s.p2 == s.p3)
+ })
+
+ t.Run("horizontal segment", func(t *testing.T) {
+ a := P{20, 10}
+ b := P{50, 10}
+ s := MakeSpline(a, b)
+ assert.Equal(t, s.p0, a)
+ assert.InDelta(t, 25.7, s.p1.X, delta)
+ assert.InDelta(t, 8.14, s.p1.Y, delta)
+ assert.InDelta(t, 44.3, s.p2.X, delta)
+ assert.InDelta(t, 8.14, s.p2.Y, delta)
+ assert.Equal(t, s.p3, b)
+ })
+
+ t.Run("negative slope", func(t *testing.T) {
+ a := P{50, 20}
+ b := P{20, 10}
+ s := MakeSpline(a, b)
+ assert.Equal(t, s.p0, a)
+ assert.InDelta(t, 44.91, s.p1.X, delta)
+ assert.InDelta(t, 16.24, s.p1.Y, delta)
+ assert.InDelta(t, 26.32, s.p2.X, delta)
+ assert.InDelta(t, 10.04, s.p2.Y, delta)
+ assert.Equal(t, s.p3, b)
+ })
+
+ t.Run("positive slope", func(t *testing.T) {
+ a := P{20, 10}
+ b := P{50, 20}
+ s := MakeSpline(a, b)
+ assert.Equal(t, s.p0, a)
+ assert.InDelta(t, 26.32, s.p1.X, delta)
+ assert.InDelta(t, 10.04, s.p1.Y, delta)
+ assert.InDelta(t, 44.91, s.p2.X, delta)
+ assert.InDelta(t, 16.24, s.p2.Y, delta)
+ assert.Equal(t, s.p3, b)
+ })
+}
diff --git a/internal/graph/connected.go b/internal/graph/connected.go
new file mode 100644
index 0000000..56c8b31
--- /dev/null
+++ b/internal/graph/connected.go
@@ -0,0 +1,41 @@
+package graph
+
+import (
+ "maps"
+)
+
+func (g *DGraph) ConnectedComponents() []*DGraph {
+ visitedN := make(NodeSet)
+ visitedE := make(EdgeSet)
+ walkDfs(g.Nodes[0], visitedN, visitedE)
+
+ // if all nodes were visited at the first dfs
+ // then there is only one connected component and that is G itself
+ if len(visitedN) == len(g.Nodes) {
+ return []*DGraph{g}
+ }
+
+ cnncmp := make([]*DGraph, 0, 2) // this has at least 2 connected components
+ cnncmp = append(cnncmp, &DGraph{Nodes: visitedN.Keys(), Edges: visitedE.Keys()})
+
+ for _, n := range g.Nodes {
+ if !visitedN[n] {
+ ns := make(NodeSet)
+ es := make(EdgeSet)
+ walkDfs(n, ns, es)
+ cnncmp = append(cnncmp, &DGraph{Nodes: ns.Keys(), Edges: es.Keys()})
+ maps.Copy(visitedN, ns)
+ }
+ }
+ return cnncmp
+}
+
+func walkDfs(n *Node, visitedN NodeSet, visitedE EdgeSet) {
+ visitedN[n] = true
+ n.VisitEdges(func(e *Edge) {
+ if !visitedE[e] {
+ visitedE[e] = true
+ walkDfs(e.ConnectedNode(n), visitedN, visitedE)
+ }
+ })
+}
diff --git a/internal/graph/connected/connected.go b/internal/graph/connected/connected.go
deleted file mode 100644
index e327820..0000000
--- a/internal/graph/connected/connected.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package connected
-
-import (
- "maps"
-
- ig "github.com/nulab/autog/internal/graph"
-)
-
-func Components(g *ig.DGraph) []*ig.DGraph {
- visitedN := make(ig.NodeSet)
- visitedE := make(ig.EdgeSet)
- walkDfs(g.Nodes[0], visitedN, visitedE)
-
- // if all nodes were visited at the first dfs
- // then there is only one connected component and that is G itself
- if len(visitedN) == len(g.Nodes) {
- return []*ig.DGraph{g}
- }
-
- cnncmp := make([]*ig.DGraph, 0, 2) // this has at least 2 connected components
- cnncmp = append(cnncmp, &ig.DGraph{Nodes: visitedN.Keys(), Edges: visitedE.Keys()})
-
- for _, n := range g.Nodes {
- if !visitedN[n] {
- ns := make(ig.NodeSet)
- es := make(ig.EdgeSet)
- walkDfs(n, ns, es)
- cnncmp = append(cnncmp, &ig.DGraph{Nodes: ns.Keys(), Edges: es.Keys()})
- maps.Copy(visitedN, ns)
- }
- }
- return cnncmp
-}
-
-func walkDfs(n *ig.Node, visitedN ig.NodeSet, visitedE ig.EdgeSet) {
- visitedN[n] = true
- n.VisitEdges(func(e *ig.Edge) {
- if !visitedE[e] {
- visitedE[e] = true
- walkDfs(e.ConnectedNode(n), visitedN, visitedE)
- }
- })
-}
diff --git a/internal/graph/connected/connected_test.go b/internal/graph/connected_test.go
similarity index 75%
rename from internal/graph/connected/connected_test.go
rename to internal/graph/connected_test.go
index 3c40f67..64ad9d1 100644
--- a/internal/graph/connected/connected_test.go
+++ b/internal/graph/connected_test.go
@@ -1,10 +1,8 @@
-package connected
+package graph
import (
"testing"
- "github.com/nulab/autog/graph"
- ig "github.com/nulab/autog/internal/graph"
"github.com/stretchr/testify/assert"
)
@@ -14,10 +12,10 @@ func TestComponents(t *testing.T) {
{"a", "b"},
{"b", "c"},
}
- g := &ig.DGraph{}
- graph.EdgeSlice(es).Populate(g)
+ g := &DGraph{}
+ EdgeSlice(es).Populate(g)
- comp := Components(g)
+ comp := g.ConnectedComponents()
assert.Len(t, comp, 1)
assert.True(t, comp[0] == g)
})
@@ -28,10 +26,10 @@ func TestComponents(t *testing.T) {
{"b", "c"},
{"f", "g"},
}
- g := &ig.DGraph{}
- graph.EdgeSlice(es).Populate(g)
+ g := &DGraph{}
+ EdgeSlice(es).Populate(g)
- comp := Components(g)
+ comp := g.ConnectedComponents()
assert.Len(t, comp, 2)
assert.ElementsMatch(t, []string{"a", "b", "c"}, ids(comp[0].Nodes))
assert.ElementsMatch(t, []string{"f", "g"}, ids(comp[1].Nodes))
@@ -46,10 +44,10 @@ func TestComponents(t *testing.T) {
{"l", "j"},
{"z", "f"},
}
- g := &ig.DGraph{}
- graph.EdgeSlice(es).Populate(g)
+ g := &DGraph{}
+ EdgeSlice(es).Populate(g)
- comp := Components(g)
+ comp := g.ConnectedComponents()
assert.Len(t, comp, 4)
assert.ElementsMatch(t, []string{"a", "b", "c"}, ids(comp[0].Nodes))
assert.ElementsMatch(t, []string{"f", "g", "h", "i", "z"}, ids(comp[1].Nodes))
@@ -59,7 +57,7 @@ func TestComponents(t *testing.T) {
}
-func ids(ns []*ig.Node) (ids []string) {
+func ids(ns []*Node) (ids []string) {
for _, n := range ns {
ids = append(ids, n.ID)
}
diff --git a/internal/graph/dgraph.go b/internal/graph/dgraph.go
index 0ed9a92..ef3df27 100644
--- a/internal/graph/dgraph.go
+++ b/internal/graph/dgraph.go
@@ -2,7 +2,6 @@ package graph
import (
"iter"
- "strings"
)
type DGraph struct {
@@ -52,37 +51,14 @@ func (g *DGraph) Sinks() iter.Seq[*Node] {
}
}
-func (g *DGraph) String() string {
- bld := strings.Builder{}
- for _, n := range g.Nodes {
- bld.WriteString(n.ID)
- bld.WriteRune('\n')
- bld.WriteString("-IN:")
- if len(n.In) == 0 {
- bld.WriteRune('\t')
- bld.WriteString("none")
- bld.WriteRune('\n')
- }
- for _, e := range n.In {
- bld.WriteRune('\t')
- bld.WriteString(e.From.ID)
- bld.WriteString(" -> ")
- bld.WriteString(n.ID)
- bld.WriteRune('\n')
- }
- bld.WriteString("-OUT:")
- if len(n.Out) == 0 {
- bld.WriteRune('\t')
- bld.WriteString("none")
- bld.WriteRune('\n')
- }
- for _, e := range n.Out {
- bld.WriteRune('\t')
- bld.WriteString(n.ID)
- bld.WriteString(" -> ")
- bld.WriteString(e.To.ID)
- bld.WriteRune('\n')
+func (g *DGraph) VirtualNodes() iter.Seq[*Node] {
+ return func(yield func(*Node) bool) {
+ for _, n := range g.Nodes {
+ if n.IsVirtual {
+ if !yield(n) {
+ return
+ }
+ }
}
}
- return bld.String()
}
diff --git a/internal/graph/layer_test.go b/internal/graph/layer_test.go
new file mode 100644
index 0000000..1591517
--- /dev/null
+++ b/internal/graph/layer_test.go
@@ -0,0 +1,7 @@
+package graph
+
+import "testing"
+
+func TestLayer(t *testing.T) {
+
+}
diff --git a/internal/graph/params.go b/internal/graph/params.go
index f4cc9c6..e50b18b 100644
--- a/internal/graph/params.go
+++ b/internal/graph/params.go
@@ -38,6 +38,9 @@ type Params struct {
// ---- phase3 options ---
+ // Size of virtual nodes (NxN). Defaults to zero, i.e. virtual nodes are treated as points.
+ VirtualNodeFixedSize float64
+
// Maximum number of iterations of the WMedian orderer.
WMedianMaxIter uint
diff --git a/internal/graph/source.go b/internal/graph/source.go
new file mode 100644
index 0000000..736b7d6
--- /dev/null
+++ b/internal/graph/source.go
@@ -0,0 +1,7 @@
+package graph
+
+// Source represent the source of graph data. It hides the implementation details of the internal DGraph struct
+// and allows only this module to provide implementations.
+type Source interface {
+ Populate(*DGraph)
+}
diff --git a/graph/source_edgeslice.go b/internal/graph/source_edgeslice.go
similarity index 68%
rename from graph/source_edgeslice.go
rename to internal/graph/source_edgeslice.go
index 8b7d406..9c8f907 100644
--- a/graph/source_edgeslice.go
+++ b/internal/graph/source_edgeslice.go
@@ -1,17 +1,15 @@
package graph
-import ig "github.com/nulab/autog/internal/graph"
-
// EdgeSlice is a graph Source.
type EdgeSlice [][]string
var _ Source = EdgeSlice{}
-func (edges EdgeSlice) Populate(g *ig.DGraph) {
- nodeMap := map[string]*ig.Node{}
+func (edges EdgeSlice) Populate(g *DGraph) {
+ nodeMap := map[string]*Node{}
- nodeList := []*ig.Node{}
- edgeList := []*ig.Edge{}
+ nodeList := []*Node{}
+ edgeList := []*Edge{}
for _, e := range edges {
if len(e) != 2 {
@@ -22,18 +20,18 @@ func (edges EdgeSlice) Populate(g *ig.DGraph) {
sourceNode := nodeMap[sourceId]
if sourceNode == nil {
- sourceNode = &ig.Node{ID: sourceId}
+ sourceNode = &Node{ID: sourceId}
nodeList = append(nodeList, sourceNode)
nodeMap[sourceId] = sourceNode
}
targetNode := nodeMap[targetId]
if targetNode == nil {
- targetNode = &ig.Node{ID: targetId}
+ targetNode = &Node{ID: targetId}
nodeList = append(nodeList, targetNode)
nodeMap[targetId] = targetNode
}
- e := ig.NewEdge(sourceNode, targetNode, 1) // default to weight 1
+ e := NewEdge(sourceNode, targetNode, 1) // default to weight 1
edgeList = append(edgeList, e)
targetNode.In = append(targetNode.In, e)
diff --git a/internal/ns/ns.go b/internal/ns/ns.go
new file mode 100644
index 0000000..52e7156
--- /dev/null
+++ b/internal/ns/ns.go
@@ -0,0 +1,391 @@
+package ns
+
+import (
+ "math"
+ "slices"
+
+ "github.com/nulab/autog/internal/graph"
+)
+
+type Processor struct {
+ lim graph.NodeIntMap // Gansner et al.: number from a root node in spanning tree postorder traversal
+ low graph.NodeIntMap // Gansner et al.: lowest postorder traversal number among nodes reachable from the input node
+}
+
+// Exec implements a graph node layering algorithm, based on:
+// - "Emden R. Gansner, Eleftherios Koutsofios, Stephen C. North, Kiem-Phong Vo, A technique for
+// drawing directed graphs. Software Engineering 19(3), pp. 214-230, 1993."
+// https://www.researchgate.net/publication/3187542_A_Technique_for_Drawing_Directed_Graphs
+// - ELK Java code at https://github.com/eclipse/elk/blob/master/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p2layers/NetworkSimplexLayerer.java
+func (p *Processor) Exec(g *graph.DGraph, params graph.Params) {
+ p.lim = make(graph.NodeIntMap)
+ p.low = make(graph.NodeIntMap)
+
+ p.feasibleTree(g)
+
+ // ELK defines the max iterations as an arbitrary user value N times a fixed factor K times the sqroot of |V|.
+ // where |V| is the number of nodes in each connected component.
+ // N*K in ELK defaults to 28.
+ k1 := int(math.Sqrt(float64(len(g.Nodes))))
+ if params.NetworkSimplexMaxIterFactor > 0 {
+ k1 = params.NetworkSimplexMaxIterFactor
+ }
+ maxitr := int(params.NetworkSimplexThoroughness) * k1
+
+ e := negCutValueTreeEdge(g.Edges)
+ i := 0
+ for e != nil {
+ if i >= maxitr {
+ break
+ }
+ f := p.minSlackNonTreeEdge(g.Edges, e)
+ // todo: figure out why this could be nil
+ if f == nil {
+ break
+ }
+ p.exchange(e, f, g)
+ e = negCutValueTreeEdge(g.Edges)
+ i++
+ }
+ normalize(g)
+ switch params.NetworkSimplexBalance {
+ case 1:
+ vbalance(g)
+ case 2:
+ p.hbalance(g)
+ }
+}
+
+// returns the first tree edge with negative cut value;
+// may return nil if there is no such edge, meaning the solution is optimal.
+func negCutValueTreeEdge(edges []*graph.Edge) *graph.Edge {
+ for _, e := range edges {
+ if e.IsInSpanningTree && e.CutValue < 0 {
+ return e
+ }
+ }
+ return nil
+}
+
+// Replace candidates are non-tree edges that go from e's head component to its tail component (original direction).
+// The first candidate with minimum slack is chosen.
+func (p *Processor) minSlackNonTreeEdge(edges []*graph.Edge, e *graph.Edge) *graph.Edge {
+ var minSlack = math.MaxInt
+ var replaceCandidate *graph.Edge
+ for _, f := range edges {
+ if f == e || f.IsInSpanningTree {
+ continue
+ }
+ if p.inHeadComponent(f.From, e) && !p.inHeadComponent(f.To, e) {
+ slack := slack(f)
+ if slack < minSlack {
+ minSlack = slack
+ replaceCandidate = f
+ }
+ }
+ }
+ return replaceCandidate // nil if not otherwise assigned
+}
+
+// computes an initial feasible spanning tree; it's feasible if its edges are tight
+func (p *Processor) feasibleTree(g *graph.DGraph) {
+ p.initLayers(g)
+ for {
+ treeNodes := tightTree(g.Nodes[0], graph.EdgeSet{}, graph.NodeSet{})
+ if len(treeNodes) == len(g.Nodes) {
+ break
+ }
+ e := p.incidentNonTreeEdge(treeNodes)
+ // incident means that one of e's vertices belongs to the tree and one doesn't.
+ // here e's slack must be >= 0: since it points to a non-tree node, if the slack
+ // were 0 it would've been included in the tight tree.
+ // then, the layers of tree nodes are adjusted to make e's slack equal to zero.
+ // as the edge becomes tight, it will be included in the tree together with its non-tree vertex
+ // at the next iteration.
+ d := slack(e)
+ if treeNodes[e.To] {
+ d = -d
+ }
+ for n := range treeNodes {
+ n.Layer += d
+ }
+ }
+
+ p.setStreeValues(g.Nodes[0])
+ p.setCutValues(g)
+}
+
+// Starting from the graph source nodes (no incoming edges), this method assigns an initial layer
+// to each node n based on the maximum layer of nodes connected to them via incoming edges.
+// This makes all directed edges point downward.
+// Source nodes have no incoming edges, so are processed first.
+func (p *Processor) initLayers(g *graph.DGraph) {
+ // initialize the count of incoming edges for all nodes
+ unseenInEdges := make(graph.NodeIntMap)
+ for _, n := range g.Nodes {
+ unseenInEdges[n] = n.Indeg()
+ }
+
+ // sources have layer 0
+ sources := slices.Collect(g.Sources())
+
+ for len(sources) > 0 {
+ n := sources[0]
+ sources = sources[1:]
+
+ // given a directed edge e = (n,m)
+ // the target node m is assigned the layer of the source node plus delta
+ // this makes the edge tight by construction because slack(e) = 0
+ for _, e := range n.Out {
+ m := e.To
+ m.Layer = max(m.Layer, n.Layer+e.Delta)
+ unseenInEdges[m]--
+ if unseenInEdges[m] == 0 {
+ sources = append(sources, m)
+ }
+ }
+ }
+}
+
+// Starting from the given node, this constructs a spanning tree with only tight edges (slack = 0).
+// It returns visitedNodes which contains the nodes that belong to this spanning tree.
+func tightTree(n *graph.Node, visitedEdges graph.EdgeSet, visitedNodes graph.NodeSet) graph.NodeSet {
+ visitedNodes[n] = true
+ n.VisitEdges(func(e *graph.Edge) {
+ if !visitedEdges[e] {
+ visitedEdges[e] = true
+ m := e.ConnectedNode(n)
+ if e.IsInSpanningTree {
+ tightTree(m, visitedEdges, visitedNodes)
+ } else if !visitedNodes[m] && slack(e) == 0 {
+ // checking that m hasn't been seen before ensures there are no loopbacks in this spanning tree
+ e.IsInSpanningTree = true
+ tightTree(m, visitedEdges, visitedNodes)
+ }
+ }
+ })
+ return visitedNodes
+}
+
+// This finds a "non-tree edge incident on the tree with min amount of slack".
+// Incident means that only one of the edge's vertices belongs to the spanning tree.
+func (p *Processor) incidentNonTreeEdge(treeNodes graph.NodeSet) *graph.Edge {
+ var minSlack = math.MaxInt
+ var candidate *graph.Edge
+ // todo: range on map non-deterministic
+ for n := range treeNodes {
+ n.VisitEdges(func(e *graph.Edge) {
+ if e.SelfLoops() {
+ return
+ }
+ if e.IsInSpanningTree || treeNodes[e.ConnectedNode(n)] {
+ return
+ }
+ slack := slack(e)
+ if slack < minSlack {
+ minSlack = slack
+ candidate = e
+ }
+ })
+ }
+ if candidate == nil {
+ panic("network simplex: did not find adjacent non-tree edge with min slack: make sure the graph is connected")
+ }
+ return candidate
+}
+
+func (p *Processor) inHeadComponent(n *graph.Node, e *graph.Edge) bool {
+ if !e.IsInSpanningTree {
+ panic("network simplex: breaking tree around non-tree edge")
+ }
+ u, v := e.From, e.To
+
+ // the following boolean logic follows Graphviz's paper:
+ // "For example, if e = (u,v) is a tree edge and vroot is in the head component of the edge (i.e., lim(u) < lim(v)),
+ // then a node w is in the tail component of e if and only if low(u) ≤ lim(w) ≤ lim(u)."
+ if p.lim[u] < p.lim[v] {
+ if p.low[u] <= p.lim[n] && p.lim[n] <= p.lim[u] {
+ // this inequality means that n is in the subtree rooted in u;
+ // because of e's direction, it also implies that n is in the subtree rooted in v
+ return false
+ }
+ return true
+ }
+ // else vroot is in the tail component and v is lower than u in the DFS tree
+ // if n is in a subtree rooted in v, it is also in a subtree rooted in u
+ // hence it's in the head component
+ return p.low[v] <= p.lim[n] && p.lim[n] <= p.lim[v]
+}
+
+func (p *Processor) exchange(e, f *graph.Edge, g *graph.DGraph) {
+ if !e.IsInSpanningTree {
+ panic("network simplex: exchange: tree-edge not in spanning tree")
+ }
+ if f.IsInSpanningTree {
+ panic("network simplex: exchange: non-tree-edge already in spanning tree")
+ }
+
+ d := slack(f)
+ if d > 0 {
+ // adjust the layer of nodes in e's tail component
+ for _, n := range g.Nodes {
+ if !p.inHeadComponent(n, e) {
+ n.Layer -= d
+ }
+ }
+ }
+
+ // exchange the edges
+ e.IsInSpanningTree = false
+ f.IsInSpanningTree = true
+
+ // recalculate the postorder numbers and edges' cut values
+ p.setStreeValues(g.Nodes[0])
+ p.setCutValues(g)
+}
+
+func (p *Processor) setStreeValues(n *graph.Node) {
+ clear(p.lim)
+ clear(p.low)
+ p.walkStreeDfs(n, graph.EdgeSet{}, 1)
+}
+
+// Visits the nodes of the spanning tree in postorder traversal, assigning increasing indices.
+// Same as a topological sorting; in addition, each node is mapped to a number low(n)
+// which is the lowest postorder number in the subtree rooted in n.
+// The root node will have low(n) = 1 and lim(n) = |V|; leaf nodes will have lim(n) = low(n).
+func (p *Processor) walkStreeDfs(n *graph.Node, visited graph.EdgeSet, low int) int {
+ p.low[n] = low
+ lim := low
+ n.VisitEdges(func(e *graph.Edge) {
+ if e.IsInSpanningTree && !visited[e] {
+ visited[e] = true
+ lim = p.walkStreeDfs(e.ConnectedNode(n), visited, lim)
+ }
+ })
+ p.lim[n] = lim
+ return lim + 1
+}
+
+// The cut value is defined as x - y where:
+// - x = sum of the weights of all edges going from the tail to the head component, including the tree edge itself
+// - y = sum of the weights of all edges from the head to the tail component
+func (p *Processor) setCutValues(g *graph.DGraph) {
+ // todo naive implementation, optimize
+ for _, e := range g.Edges {
+ if !e.IsInSpanningTree {
+ continue
+ }
+ e.CutValue += e.Weight // e itself goes from tail to head by definition
+
+ for _, f := range g.Edges {
+ // no other tree edge connects different components, otherwise we'd have two paths to e's target
+ if f.IsInSpanningTree {
+ continue
+ }
+ if !p.inHeadComponent(f.From, e) && p.inHeadComponent(f.To, e) {
+ e.CutValue += f.Weight
+ } else if p.inHeadComponent(f.From, e) && !p.inHeadComponent(f.To, e) {
+ e.CutValue -= f.Weight
+ }
+ }
+ }
+}
+
+func slack(e *graph.Edge) int {
+ return e.To.Layer - e.From.Layer - e.Delta
+}
+
+// shifts all layers up so that the lowest layer is 0
+func normalize(g *graph.DGraph) {
+ lowest := math.MaxInt
+ for _, n := range g.Nodes {
+ lowest = min(lowest, n.Layer)
+ }
+ if lowest == 0 {
+ return
+ }
+ for _, n := range g.Nodes {
+ n.Layer -= lowest
+ }
+}
+
+// nodes are shifted to less crowded layers if the shift preserves feasibility (edge length >= edge delta)
+func vbalance(g *graph.DGraph) {
+ lsize := map[int]int{}
+ lmax := 0
+ for _, n := range g.Nodes {
+ lsize[n.Layer]++
+ lmax = max(lmax, n.Layer)
+ }
+
+ for _, n := range g.Nodes {
+ if n.Indeg() == n.Outdeg() {
+ low := 0
+ high := lmax
+ for _, e := range n.In {
+ low = max(low, e.From.Layer+e.Delta)
+ }
+ for _, e := range n.Out {
+ high = min(high, e.To.Layer-e.Delta)
+ }
+ newl := low
+
+ // if the node has only flat edges, or in/out-span 1, or is source/sink with span 1, this does nothing
+ // otherwise it may shift the node
+ for i := low + 1; i <= high; i++ {
+ if lsize[i] < lsize[newl] {
+ newl = i
+ }
+ }
+ if lsize[newl] < lsize[n.Layer] {
+ lsize[n.Layer]--
+ lsize[newl]++
+ n.Layer = newl
+ }
+ }
+ }
+}
+
+func (p *Processor) hbalance(g *graph.DGraph) {
+ for _, e := range g.Edges {
+ if !e.IsInSpanningTree {
+ continue
+ }
+ if e.CutValue == 0 {
+ f := p.minSlackNonTreeEdge(g.Edges, e)
+ if f == nil {
+ continue
+ }
+ d := slack(f)
+ if d < 1 {
+ continue
+ }
+ if p.lim[e.From] < p.lim[e.To] {
+ p.adjustLayers(e.From, d)
+ } else {
+ p.adjustLayers(e.To, -d)
+ }
+ }
+ }
+}
+
+func (p *Processor) adjustLayers(n *graph.Node, delta int) {
+ n.Layer -= delta
+ for _, e := range n.Out {
+ if !e.IsInSpanningTree {
+ continue
+ }
+ if !(p.lim[n] < p.lim[e.ConnectedNode(n)]) {
+ p.adjustLayers(e.To, delta)
+ }
+ }
+ for _, e := range n.In {
+ if !e.IsInSpanningTree {
+ continue
+ }
+ if !(p.lim[n] < p.lim[e.ConnectedNode(n)]) {
+ p.adjustLayers(e.From, delta)
+ }
+ }
+}
diff --git a/internal/ns/ns_test.go b/internal/ns/ns_test.go
new file mode 100644
index 0000000..2d1bd88
--- /dev/null
+++ b/internal/ns/ns_test.go
@@ -0,0 +1,118 @@
+package ns
+
+import (
+ "testing"
+
+ "github.com/nulab/autog/internal/graph"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSpanningTree(t *testing.T) {
+ g := &graph.DGraph{}
+ graph.EdgeSlice([][]string{
+ {"a", "b"},
+ {"b", "d"},
+ {"b", "e"},
+ {"a", "c"},
+ {"c", "f"},
+ {"c", "g"},
+ {"c", "h"},
+ {"f", "i"},
+ }).Populate(g)
+
+ for _, e := range g.Edges {
+ e.IsInSpanningTree = true
+ }
+ p := Processor{
+ lim: make(graph.NodeIntMap),
+ low: make(graph.NodeIntMap),
+ }
+ p.setStreeValues(findNode(g, "a"))
+
+ t.Run("low and lim", func(t *testing.T) {
+ type tc struct {
+ id string
+ low, lim int
+ }
+ tcs := []tc{
+ {"a", 1, 9},
+ {"b", 1, 3},
+ {"c", 4, 8},
+ {"d", 1, 1},
+ {"e", 2, 2},
+ {"f", 4, 5},
+ {"g", 6, 6},
+ {"h", 7, 7},
+ {"i", 4, 4},
+ }
+
+ for _, tc := range tcs {
+ n := findNode(g, tc.id)
+ assert.Equalf(t, tc.low, p.low[n], "wrong low for n: %s", n.ID)
+ assert.Equalf(t, tc.lim, p.lim[n], "wrong lim for n: %s", n.ID)
+ }
+ })
+
+ t.Run("node in head component", func(t *testing.T) {
+ e := findEdge(g, "c", "f")
+ assert.True(t, p.inHeadComponent(findNode(g, "i"), e))
+ assert.True(t, p.inHeadComponent(findNode(g, "f"), e))
+ assert.False(t, p.inHeadComponent(findNode(g, "c"), e))
+ assert.False(t, p.inHeadComponent(findNode(g, "d"), e))
+ assert.False(t, p.inHeadComponent(findNode(g, "e"), e))
+ assert.False(t, p.inHeadComponent(findNode(g, "h"), e))
+
+ e.Reverse()
+ assert.False(t, p.inHeadComponent(findNode(g, "i"), e))
+ assert.False(t, p.inHeadComponent(findNode(g, "f"), e))
+ assert.True(t, p.inHeadComponent(findNode(g, "c"), e))
+ assert.True(t, p.inHeadComponent(findNode(g, "d"), e))
+ assert.True(t, p.inHeadComponent(findNode(g, "e"), e))
+ assert.True(t, p.inHeadComponent(findNode(g, "h"), e))
+
+ e = findEdge(g, "a", "b")
+ assert.False(t, p.inHeadComponent(findNode(g, "i"), e))
+ assert.False(t, p.inHeadComponent(findNode(g, "f"), e))
+ assert.False(t, p.inHeadComponent(findNode(g, "c"), e))
+ assert.False(t, p.inHeadComponent(findNode(g, "a"), e))
+ assert.True(t, p.inHeadComponent(findNode(g, "d"), e))
+ assert.True(t, p.inHeadComponent(findNode(g, "e"), e))
+ assert.False(t, p.inHeadComponent(findNode(g, "h"), e))
+
+ e.Reverse()
+ assert.True(t, p.inHeadComponent(findNode(g, "i"), e))
+ assert.True(t, p.inHeadComponent(findNode(g, "f"), e))
+ assert.True(t, p.inHeadComponent(findNode(g, "c"), e))
+ assert.True(t, p.inHeadComponent(findNode(g, "a"), e))
+ assert.False(t, p.inHeadComponent(findNode(g, "d"), e))
+ assert.False(t, p.inHeadComponent(findNode(g, "e"), e))
+ assert.True(t, p.inHeadComponent(findNode(g, "h"), e))
+
+ e = findEdge(g, "b", "e")
+ for _, n := range g.Nodes {
+ if n.ID == "e" {
+ assert.True(t, p.inHeadComponent(n, e))
+ } else {
+ assert.False(t, p.inHeadComponent(n, e))
+ }
+ }
+ })
+}
+
+func findNode(g *graph.DGraph, id string) *graph.Node {
+ for _, n := range g.Nodes {
+ if n.ID == id {
+ return n
+ }
+ }
+ return nil
+}
+
+func findEdge(g *graph.DGraph, from, to string) *graph.Edge {
+ for _, e := range g.Edges {
+ if e.From.ID == from && e.To.ID == to {
+ return e
+ }
+ }
+ return nil
+}
diff --git a/internal/num/abs_test.go b/internal/num/abs_test.go
new file mode 100644
index 0000000..b2c4dc0
--- /dev/null
+++ b/internal/num/abs_test.go
@@ -0,0 +1,13 @@
+package num
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAbs(t *testing.T) {
+ assert.Equal(t, 192, Abs(-192))
+ assert.Equal(t, 56, Abs(56))
+ assert.Equal(t, 0, Abs(0))
+}
diff --git a/internal/phase1/cycle_test.go b/internal/phase1/cycle_test.go
new file mode 100644
index 0000000..f509a72
--- /dev/null
+++ b/internal/phase1/cycle_test.go
@@ -0,0 +1,7 @@
+package phase1
+
+import "testing"
+
+func TestCycle(t *testing.T) {
+
+}
diff --git a/internal/phase2/alg_test.go b/internal/phase2/alg_test.go
index 200613c..beb8f3c 100644
--- a/internal/phase2/alg_test.go
+++ b/internal/phase2/alg_test.go
@@ -3,6 +3,7 @@ package phase2
import (
"testing"
+ ig "github.com/nulab/autog/internal/graph"
"github.com/stretchr/testify/assert"
)
@@ -15,4 +16,11 @@ func TestAlg(t *testing.T) {
assert.Equal(t, 2, i.Phase())
assert.Equal(t, strs[i], i.String())
}
+ assert.Equal(t, "", _endAlg.String())
+}
+
+func fromEdgeSlice(es [][]string) *ig.DGraph {
+ g := &ig.DGraph{}
+ ig.EdgeSlice(es).Populate(g)
+ return g
}
diff --git a/internal/phase2/longest_path.go b/internal/phase2/longest_path.go
index c56653f..0131d9e 100644
--- a/internal/phase2/longest_path.go
+++ b/internal/phase2/longest_path.go
@@ -1,47 +1,34 @@
package phase2
import (
- "sort"
-
"github.com/nulab/autog/internal/graph"
)
func execLongestPath(g *graph.DGraph) {
- height := graph.NodeIntMap{}
-
for _, n := range g.Nodes {
- height[n] = -1
+ n.Layer = -1
}
- nodes := make([]*graph.Node, len(g.Nodes))
- copy(nodes, g.Nodes)
-
- sort.Slice(nodes, func(i, j int) bool {
- return nodes[i].Outdeg() > nodes[j].Outdeg() ||
- (nodes[i].Outdeg() == nodes[j].Outdeg() && nodes[i].Indeg() < nodes[j].Indeg())
- })
-
- nlayers := 0
- for _, n := range nodes {
- followLongestPath(n, height, &nlayers)
+ maxh := 0
+ for _, n := range g.Nodes {
+ h := followLongestPath(n)
+ maxh = max(maxh, h)
}
}
-func followLongestPath(n *graph.Node, height graph.NodeIntMap, nlayers *int) int {
- if height[n] >= 0 {
- return height[n]
+func followLongestPath(n *graph.Node) int {
+ if n.Layer >= 0 {
+ return n.Layer
}
- nodeh := 1
- // sinks have no out-edges, so will yield 1
- for _, e := range n.Out {
+ maxh := 0
+ for _, e := range n.In {
if e.SelfLoops() {
continue
}
- h := followLongestPath(e.ConnectedNode(n), height, nlayers)
- nodeh = max(nodeh, h+e.Delta)
+ h := followLongestPath(e.ConnectedNode(n))
+ maxh = max(maxh, h+1)
}
- *nlayers = max(*nlayers, nodeh)
- n.Layer = *nlayers - nodeh
- height[n] = nodeh
- return nodeh
+
+ n.Layer = maxh
+ return maxh
}
diff --git a/internal/phase2/longest_path_test.go b/internal/phase2/longest_path_test.go
index 1515bad..e4682ec 100644
--- a/internal/phase2/longest_path_test.go
+++ b/internal/phase2/longest_path_test.go
@@ -1,20 +1,34 @@
package phase2
-// func TestLongestPath(t *testing.T) {
-// g := graph.FromEdgeSlice([][]string{
-// {"F", "B"},
-// {"F", "Z"},
-// {"B", "A"},
-// {"B", "D"},
-// {"B", "K"},
-// {"Z", "a1"},
-// {"Z", "b1"},
-// {"A", "C"},
-// {"C", "U"},
-// })
-//
-// execLongestPath(g)
-// for _, n := range g.Nodes {
-// fmt.Println(n.ID, n.Layer)
-// }
-// }
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLongestPath(t *testing.T) {
+ g := fromEdgeSlice([][]string{
+ {"F", "B"},
+ {"F", "Z"},
+ {"B", "A"},
+ {"B", "D"},
+ {"B", "K"},
+ {"Z", "a1"},
+ {"Z", "b1"},
+ {"A", "C"},
+ {"C", "U"},
+ })
+
+ want := map[string]int{
+ "F": 0,
+ "B": 1, "Z": 1,
+ "A": 2, "D": 2, "K": 2, "a1": 2, "b1": 2,
+ "C": 3,
+ "U": 4,
+ }
+
+ execLongestPath(g)
+ for _, n := range g.Nodes {
+ assert.Equal(t, want[n.ID], n.Layer)
+ }
+}
diff --git a/internal/phase2/network_simplex.go b/internal/phase2/network_simplex.go
index 5893951..a57316b 100644
--- a/internal/phase2/network_simplex.go
+++ b/internal/phase2/network_simplex.go
@@ -1,392 +1,10 @@
package phase2
import (
- "math"
- "slices"
-
"github.com/nulab/autog/internal/graph"
+ "github.com/nulab/autog/internal/ns"
)
-type networkSimplexProcessor struct {
- lim graph.NodeIntMap // Gansner et al.: number from a root node in spanning tree postorder traversal
- low graph.NodeIntMap // Gansner et al.: lowest postorder traversal number among nodes reachable from the input node
-}
-
-// this implements a graph node layering algorithm, based on:
-// - "Emden R. Gansner, Eleftherios Koutsofios, Stephen C. North, Kiem-Phong Vo, A technique for
-// drawing directed graphs. Software Engineering 19(3), pp. 214-230, 1993."
-// https://www.researchgate.net/publication/3187542_A_Technique_for_Drawing_Directed_Graphs
-// - ELK Java code at https://github.com/eclipse/elk/blob/master/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p2layers/NetworkSimplexLayerer.java
func execNetworkSimplex(g *graph.DGraph, params graph.Params) {
- p := &networkSimplexProcessor{
- lim: make(graph.NodeIntMap),
- low: make(graph.NodeIntMap),
- }
- p.feasibleTree(g)
-
- // ELK defines the max iterations as an arbitrary user value N times a fixed factor K times the sqroot of |V|.
- // where |V| is the number of nodes in each connected component.
- // N*K in ELK defaults to 28.
- k1 := int(math.Sqrt(float64(len(g.Nodes))))
- if params.NetworkSimplexMaxIterFactor > 0 {
- k1 = params.NetworkSimplexMaxIterFactor
- }
- maxitr := int(params.NetworkSimplexThoroughness) * k1
-
- e := negCutValueTreeEdge(g.Edges)
- i := 0
- for e != nil {
- if i >= maxitr {
- break
- }
- f := p.minSlackNonTreeEdge(g.Edges, e)
- // todo: figure out why this could be nil
- if f == nil {
- break
- }
- p.exchange(e, f, g)
- e = negCutValueTreeEdge(g.Edges)
- i++
- }
- normalize(g)
- switch params.NetworkSimplexBalance {
- case 1:
- vbalance(g)
- case 2:
- p.hbalance(g)
- }
-}
-
-// returns the first tree edge with negative cut value;
-// may return nil if there is no such edge, meaning the solution is optimal.
-func negCutValueTreeEdge(edges []*graph.Edge) *graph.Edge {
- for _, e := range edges {
- if e.IsInSpanningTree && e.CutValue < 0 {
- return e
- }
- }
- return nil
-}
-
-// Replace candidates are non-tree edges that go from e's head component to its tail component (original direction).
-// The first candidate with minimum slack is chosen.
-func (p *networkSimplexProcessor) minSlackNonTreeEdge(edges []*graph.Edge, e *graph.Edge) *graph.Edge {
- var minSlack = math.MaxInt
- var replaceCandidate *graph.Edge
- for _, f := range edges {
- if f == e || f.IsInSpanningTree {
- continue
- }
- if p.inHeadComponent(f.From, e) && !p.inHeadComponent(f.To, e) {
- slack := slack(f)
- if slack < minSlack {
- minSlack = slack
- replaceCandidate = f
- }
- }
- }
- return replaceCandidate // nil if not otherwise assigned
-}
-
-// computes an initial feasible spanning tree; it's feasible if its edges are tight
-func (p *networkSimplexProcessor) feasibleTree(g *graph.DGraph) {
- p.initLayers(g)
- for {
- treeNodes := tightTree(g.Nodes[0], graph.EdgeSet{}, graph.NodeSet{})
- if len(treeNodes) == len(g.Nodes) {
- break
- }
- e := p.incidentNonTreeEdge(treeNodes)
- // incident means that one of e's vertices belongs to the tree and one doesn't.
- // here e's slack must be >= 0: since it points to a non-tree node, if the slack
- // were 0 it would've been included in the tight tree.
- // then, the layers of tree nodes are adjusted to make e's slack equal to zero.
- // as the edge becomes tight, it will be included in the tree together with its non-tree vertex
- // at the next iteration.
- d := slack(e)
- if treeNodes[e.To] {
- d = -d
- }
- for n := range treeNodes {
- n.Layer += d
- }
- }
-
- p.setStreeValues(g.Nodes[0])
- p.setCutValues(g)
-}
-
-// Starting from the graph source nodes (no incoming edges), this method assigns an initial layer
-// to each node n based on the maximum layer of nodes connected to them via incoming edges.
-// This makes all directed edges point downward.
-// Source nodes have no incoming edges, so are processed first.
-func (p *networkSimplexProcessor) initLayers(g *graph.DGraph) {
- // initialize the count of incoming edges for all nodes
- unseenInEdges := make(graph.NodeIntMap)
- for _, n := range g.Nodes {
- unseenInEdges[n] = n.Indeg()
- }
-
- // sources have layer 0
- sources := slices.Collect(g.Sources())
-
- for len(sources) > 0 {
- n := sources[0]
- sources = sources[1:]
-
- // given a directed edge e = (n,m)
- // the target node m is assigned the layer of the source node plus delta
- // this makes the edge tight by construction because slack(e) = 0
- for _, e := range n.Out {
- m := e.To
- m.Layer = max(m.Layer, n.Layer+e.Delta)
- unseenInEdges[m]--
- if unseenInEdges[m] == 0 {
- sources = append(sources, m)
- }
- }
- }
-}
-
-// Starting from the given node, this constructs a spanning tree with only tight edges (slack = 0).
-// It returns visitedNodes which contains the nodes that belong to this spanning tree.
-func tightTree(n *graph.Node, visitedEdges graph.EdgeSet, visitedNodes graph.NodeSet) graph.NodeSet {
- visitedNodes[n] = true
- n.VisitEdges(func(e *graph.Edge) {
- if !visitedEdges[e] {
- visitedEdges[e] = true
- m := e.ConnectedNode(n)
- if e.IsInSpanningTree {
- tightTree(m, visitedEdges, visitedNodes)
- } else if !visitedNodes[m] && slack(e) == 0 {
- // checking that m hasn't been seen before ensures there are no loopbacks in this spanning tree
- e.IsInSpanningTree = true
- tightTree(m, visitedEdges, visitedNodes)
- }
- }
- })
- return visitedNodes
-}
-
-// This finds a "non-tree edge incident on the tree with min amount of slack".
-// Incident means that only one of the edge's vertices belongs to the spanning tree.
-func (p *networkSimplexProcessor) incidentNonTreeEdge(treeNodes graph.NodeSet) *graph.Edge {
- var minSlack = math.MaxInt
- var candidate *graph.Edge
- // todo: range on map non-deterministic
- for n := range treeNodes {
- n.VisitEdges(func(e *graph.Edge) {
- if e.SelfLoops() {
- return
- }
- if e.IsInSpanningTree || treeNodes[e.ConnectedNode(n)] {
- return
- }
- slack := slack(e)
- if slack < minSlack {
- minSlack = slack
- candidate = e
- }
- })
- }
- if candidate == nil {
- panic("network simplex: did not find adjacent non-tree edge with min slack: make sure the graph is connected")
- }
- return candidate
-}
-
-func (p *networkSimplexProcessor) inHeadComponent(n *graph.Node, e *graph.Edge) bool {
- if !e.IsInSpanningTree {
- panic("network simplex: breaking tree around non-tree edge")
- }
- u, v := e.From, e.To
-
- // the following boolean logic follows Graphviz's paper:
- // "For example, if e = (u,v) is a tree edge and vroot is in the head component of the edge (i.e., lim(u) < lim(v)),
- // then a node w is in the tail component of e if and only if low(u) ≤ lim(w) ≤ lim(u)."
- if p.lim[u] < p.lim[v] {
- if p.low[u] <= p.lim[n] && p.lim[n] <= p.lim[u] {
- // this inequality means that n is in the subtree rooted in u;
- // because of e's direction, it also implies that n is in the subtree rooted in v
- return false
- }
- return true
- }
- // else vroot is in the tail component and v is lower than u in the DFS tree
- // if n is in a subtree rooted in v, it is also in a subtree rooted in u
- // hence it's in the head component
- return p.low[v] <= p.lim[n] && p.lim[n] <= p.lim[v]
-}
-
-func (p *networkSimplexProcessor) exchange(e, f *graph.Edge, g *graph.DGraph) {
- if !e.IsInSpanningTree {
- panic("network simplex: exchange: tree-edge not in spanning tree")
- }
- if f.IsInSpanningTree {
- panic("network simplex: exchange: non-tree-edge already in spanning tree")
- }
-
- d := slack(f)
- if d > 0 {
- // adjust the layer of nodes in e's tail component
- for _, n := range g.Nodes {
- if !p.inHeadComponent(n, e) {
- n.Layer -= d
- }
- }
- }
-
- // exchange the edges
- e.IsInSpanningTree = false
- f.IsInSpanningTree = true
-
- // recalculate the postorder numbers and edges' cut values
- p.setStreeValues(g.Nodes[0])
- p.setCutValues(g)
-}
-
-func (p *networkSimplexProcessor) setStreeValues(n *graph.Node) {
- clear(p.lim)
- clear(p.low)
- p.walkStreeDfs(n, graph.EdgeSet{}, 1)
-}
-
-// Visits the nodes of the spanning tree in postorder traversal, assigning increasing indices.
-// Same as a topological sorting; in addition, each node is mapped to a number low(n)
-// which is the lowest postorder number in the subtree rooted in n.
-// The root node will have low(n) = 1 and lim(n) = |V|; leaf nodes will have lim(n) = low(n).
-func (p *networkSimplexProcessor) walkStreeDfs(n *graph.Node, visited graph.EdgeSet, low int) int {
- p.low[n] = low
- lim := low
- n.VisitEdges(func(e *graph.Edge) {
- if e.IsInSpanningTree && !visited[e] {
- visited[e] = true
- lim = p.walkStreeDfs(e.ConnectedNode(n), visited, lim)
- }
- })
- p.lim[n] = lim
- return lim + 1
-}
-
-// The cut value is defined as x - y where:
-// - x = sum of the weights of all edges going from the tail to the head component, including the tree edge itself
-// - y = sum of the weights of all edges from the head to the tail component
-func (p *networkSimplexProcessor) setCutValues(g *graph.DGraph) {
- // todo naive implementation, optimize
- for _, e := range g.Edges {
- if !e.IsInSpanningTree {
- continue
- }
- e.CutValue += e.Weight // e itself goes from tail to head by definition
-
- for _, f := range g.Edges {
- // no other tree edge connects different components, otherwise we'd have two paths to e's target
- if f.IsInSpanningTree {
- continue
- }
- if !p.inHeadComponent(f.From, e) && p.inHeadComponent(f.To, e) {
- e.CutValue += f.Weight
- } else if p.inHeadComponent(f.From, e) && !p.inHeadComponent(f.To, e) {
- e.CutValue -= f.Weight
- }
- }
- }
-}
-
-func slack(e *graph.Edge) int {
- return e.To.Layer - e.From.Layer - e.Delta
-}
-
-// shifts all layers up so that the lowest layer is 0
-func normalize(g *graph.DGraph) {
- lowest := math.MaxInt
- for _, n := range g.Nodes {
- lowest = min(lowest, n.Layer)
- }
- if lowest == 0 {
- return
- }
- for _, n := range g.Nodes {
- n.Layer -= lowest
- }
-}
-
-// nodes are shifted to less crowded layers if the shift preserves feasibility (edge length >= edge delta)
-func vbalance(g *graph.DGraph) {
- lsize := map[int]int{}
- lmax := 0
- for _, n := range g.Nodes {
- lsize[n.Layer]++
- lmax = max(lmax, n.Layer)
- }
-
- for _, n := range g.Nodes {
- if n.Indeg() == n.Outdeg() {
- low := 0
- high := lmax
- for _, e := range n.In {
- low = max(low, e.From.Layer+e.Delta)
- }
- for _, e := range n.Out {
- high = min(high, e.To.Layer-e.Delta)
- }
- newl := low
-
- // if the node has only flat edges, or in/out-span 1, or is source/sink with span 1, this does nothing
- // otherwise it may shift the node
- for i := low + 1; i <= high; i++ {
- if lsize[i] < lsize[newl] {
- newl = i
- }
- }
- if lsize[newl] < lsize[n.Layer] {
- lsize[n.Layer]--
- lsize[newl]++
- n.Layer = newl
- }
- }
- }
-}
-
-func (p *networkSimplexProcessor) hbalance(g *graph.DGraph) {
- for _, e := range g.Edges {
- if !e.IsInSpanningTree {
- continue
- }
- if e.CutValue == 0 {
- f := p.minSlackNonTreeEdge(g.Edges, e)
- if f == nil {
- continue
- }
- d := slack(f)
- if d < 1 {
- continue
- }
- if p.lim[e.From] < p.lim[e.To] {
- p.adjustLayers(e.From, d)
- } else {
- p.adjustLayers(e.To, -d)
- }
- }
- }
-}
-
-func (p *networkSimplexProcessor) adjustLayers(n *graph.Node, delta int) {
- n.Layer -= delta
- for _, e := range n.Out {
- if !e.IsInSpanningTree {
- continue
- }
- if !(p.lim[n] < p.lim[e.ConnectedNode(n)]) {
- p.adjustLayers(e.To, delta)
- }
- }
- for _, e := range n.In {
- if !e.IsInSpanningTree {
- continue
- }
- if !(p.lim[n] < p.lim[e.ConnectedNode(n)]) {
- p.adjustLayers(e.From, delta)
- }
- }
+ new(ns.Processor).Exec(g, params)
}
diff --git a/internal/phase2/network_simplex_test.go b/internal/phase2/network_simplex_test.go
index fb7791e..839887c 100644
--- a/internal/phase2/network_simplex_test.go
+++ b/internal/phase2/network_simplex_test.go
@@ -1,131 +1,86 @@
-//go:build unit
-
package phase2
import (
"testing"
- egraph "github.com/nulab/autog/graph"
- "github.com/nulab/autog/internal/graph"
- "github.com/nulab/autog/internal/testfiles"
+ ig "github.com/nulab/autog/internal/graph"
"github.com/stretchr/testify/assert"
)
-func TestSpanningTree(t *testing.T) {
- g := fromEdgeSlice([][]string{
- {"a", "b"},
- {"b", "d"},
- {"b", "e"},
- {"a", "c"},
- {"c", "f"},
- {"c", "g"},
- {"c", "h"},
- {"f", "i"},
- })
- for _, e := range g.Edges {
- e.IsInSpanningTree = true
- }
- p := newNsProcessor()
- p.setStreeValues(findNode(g, "a"))
-
- t.Run("low and lim", func(t *testing.T) {
- type tc struct {
- id string
- low, lim int
- }
- tcs := []tc{
- {"a", 1, 9},
- {"b", 1, 3},
- {"c", 4, 8},
- {"d", 1, 1},
- {"e", 2, 2},
- {"f", 4, 5},
- {"g", 6, 6},
- {"h", 7, 7},
- {"i", 4, 4},
- }
-
- for _, tc := range tcs {
- n := findNode(g, tc.id)
- assert.Equalf(t, tc.low, p.low[n], "wrong low for n: %s", n.ID)
- assert.Equalf(t, tc.lim, p.lim[n], "wrong lim for n: %s", n.ID)
- }
- })
-
- t.Run("node in head component", func(t *testing.T) {
- e := findEdge(g, "c", "f")
- assert.True(t, p.inHeadComponent(findNode(g, "i"), e))
- assert.True(t, p.inHeadComponent(findNode(g, "f"), e))
- assert.False(t, p.inHeadComponent(findNode(g, "c"), e))
- assert.False(t, p.inHeadComponent(findNode(g, "d"), e))
- assert.False(t, p.inHeadComponent(findNode(g, "e"), e))
- assert.False(t, p.inHeadComponent(findNode(g, "h"), e))
-
- e.Reverse()
- assert.False(t, p.inHeadComponent(findNode(g, "i"), e))
- assert.False(t, p.inHeadComponent(findNode(g, "f"), e))
- assert.True(t, p.inHeadComponent(findNode(g, "c"), e))
- assert.True(t, p.inHeadComponent(findNode(g, "d"), e))
- assert.True(t, p.inHeadComponent(findNode(g, "e"), e))
- assert.True(t, p.inHeadComponent(findNode(g, "h"), e))
-
- e = findEdge(g, "a", "b")
- assert.False(t, p.inHeadComponent(findNode(g, "i"), e))
- assert.False(t, p.inHeadComponent(findNode(g, "f"), e))
- assert.False(t, p.inHeadComponent(findNode(g, "c"), e))
- assert.False(t, p.inHeadComponent(findNode(g, "a"), e))
- assert.True(t, p.inHeadComponent(findNode(g, "d"), e))
- assert.True(t, p.inHeadComponent(findNode(g, "e"), e))
- assert.False(t, p.inHeadComponent(findNode(g, "h"), e))
-
- e.Reverse()
- assert.True(t, p.inHeadComponent(findNode(g, "i"), e))
- assert.True(t, p.inHeadComponent(findNode(g, "f"), e))
- assert.True(t, p.inHeadComponent(findNode(g, "c"), e))
- assert.True(t, p.inHeadComponent(findNode(g, "a"), e))
- assert.False(t, p.inHeadComponent(findNode(g, "d"), e))
- assert.False(t, p.inHeadComponent(findNode(g, "e"), e))
- assert.True(t, p.inHeadComponent(findNode(g, "h"), e))
-
- e = findEdge(g, "b", "e")
- for _, n := range g.Nodes {
- if n.ID == "e" {
- assert.True(t, p.inHeadComponent(n, e))
- } else {
- assert.False(t, p.inHeadComponent(n, e))
- }
- }
- })
-}
-
-func newNsProcessor() *networkSimplexProcessor {
- return &networkSimplexProcessor{
- lim: graph.NodeIntMap{},
- low: graph.NodeIntMap{},
- }
-}
-
-func findNode(g *graph.DGraph, id string) *graph.Node {
- for _, n := range g.Nodes {
- if n.ID == id {
- return n
- }
- }
- return nil
-}
-
-func findEdge(g *graph.DGraph, from, to string) *graph.Edge {
- for _, e := range g.Edges {
- if e.From.ID == from && e.To.ID == to {
- return e
- }
- }
- return nil
-}
-
func TestNSLayering(t *testing.T) {
- g := fromEdgeSlice(testfiles.DotAbstract)
- execNetworkSimplex(g, graph.Params{NetworkSimplexThoroughness: 28, NetworkSimplexBalance: 1})
+ g := &ig.DGraph{}
+ ig.EdgeSlice([][]string{
+ {"S24", "27"},
+ {"S24", "25"},
+ {"S1", "10"},
+ {"S1", "2"},
+ {"S35", "36"},
+ {"S35", "43"},
+ {"S30", "31"},
+ {"S30", "33"},
+ {"9", "42"},
+ {"9", "T1"},
+ {"25", "T1"},
+ {"25", "26"},
+ {"27", "T24"},
+ {"2", "3"},
+ {"2", "16"},
+ {"2", "17"},
+ {"2", "T1"},
+ {"2", "18"},
+ {"10", "11"},
+ {"10", "14"},
+ {"10", "T1"},
+ {"10", "13"},
+ {"10", "12"},
+ {"31", "T1"},
+ {"31", "32"},
+ {"33", "T30"},
+ {"33", "34"},
+ {"42", "4"},
+ {"26", "4"},
+ {"3", "4"},
+ {"16", "15"},
+ {"17", "19"},
+ {"18", "29"},
+ {"11", "4"},
+ {"14", "15"},
+ {"37", "39"},
+ {"37", "41"},
+ {"37", "38"},
+ {"37", "40"},
+ {"13", "19"},
+ {"12", "29"},
+ {"43", "38"},
+ {"43", "40"},
+ {"36", "19"},
+ {"32", "23"},
+ {"34", "29"},
+ {"39", "15"},
+ {"41", "29"},
+ {"38", "4"},
+ {"40", "19"},
+ {"4", "5"},
+ {"19", "21"},
+ {"19", "20"},
+ {"19", "28"},
+ {"5", "6"},
+ {"5", "T35"},
+ {"5", "23"},
+ {"21", "22"},
+ {"20", "15"},
+ {"28", "29"},
+ {"6", "7"},
+ {"15", "T1"},
+ {"22", "23"},
+ {"22", "T35"},
+ {"29", "T30"},
+ {"7", "T8"},
+ {"23", "T24"},
+ {"23", "T1"},
+ }).Populate(g)
+
+ execNetworkSimplex(g, ig.Params{NetworkSimplexThoroughness: 28, NetworkSimplexBalance: 1})
want := expectedLayersAbstract()
for _, n := range g.Nodes {
@@ -152,9 +107,3 @@ func expectedLayersAbstract() map[string]int {
"T8": 8,
}
}
-
-func fromEdgeSlice(es [][]string) *graph.DGraph {
- g := &graph.DGraph{}
- egraph.EdgeSlice(es).Populate(g)
- return g
-}
diff --git a/internal/phase3/alg_test.go b/internal/phase3/alg_test.go
index b8bbbb7..8161b93 100644
--- a/internal/phase3/alg_test.go
+++ b/internal/phase3/alg_test.go
@@ -15,4 +15,5 @@ func TestAlg(t *testing.T) {
assert.Equal(t, 3, i.Phase())
assert.Equal(t, strs[i], i.String())
}
+ assert.Equal(t, "", _endAlg.String())
}
diff --git a/internal/phase3/wmedian.go b/internal/phase3/wmedian.go
index 5f27ebc..3d552f3 100644
--- a/internal/phase3/wmedian.go
+++ b/internal/phase3/wmedian.go
@@ -35,6 +35,13 @@ func execWeightedMedian(g *graph.DGraph, params graph.Params) {
// insert virtual nodes so that edges with length >1 have length 1
breakLongEdges(g)
+ // set size to virtual nodes if needed
+ if s := params.VirtualNodeFixedSize; s > 0.0 {
+ for n := range g.VirtualNodes() {
+ n.W = s
+ n.H = s
+ }
+ }
maxiter := int(params.WMedianMaxIter)
fixedPositions := initFixedPositions(g.Edges)
diff --git a/internal/phase3/wmedian_fixedpos.go b/internal/phase3/wmedian_fixedpos.go
index eca5168..4f9b109 100644
--- a/internal/phase3/wmedian_fixedpos.go
+++ b/internal/phase3/wmedian_fixedpos.go
@@ -74,14 +74,6 @@ func initFixedPositions(edges []*graph.Edge) fixedPositions {
return fixedPositions{mustAfter, mustBefore}
}
-func walkFlat(n *graph.Node, visited graph.EdgeSet) {
- for _, f := range n.Out {
- if visited[f] {
- continue
- }
- }
-}
-
// head returns the first element in a same-layer transitive closure to which k belongs, and the number of edges
// that separate k and the head;
// or returns k itself and 0 if k doesn't belong to any such closure
diff --git a/internal/phase4/network_simplex.go b/internal/phase4/network_simplex.go
index 9b75af0..4d9ece8 100644
--- a/internal/phase4/network_simplex.go
+++ b/internal/phase4/network_simplex.go
@@ -5,7 +5,7 @@ import (
"strconv"
"github.com/nulab/autog/internal/graph"
- "github.com/nulab/autog/internal/phase2"
+ "github.com/nulab/autog/internal/ns"
)
type networkSimplexProcessor struct {
@@ -30,7 +30,7 @@ func execNetworkSimplex(g *graph.DGraph, params graph.Params) {
// todo: if there are flat edges, dot adds auxiliary edges
aux := p.auxiliaryGraph(g)
- phase2.NetworkSimplex.Process(
+ new(ns.Processor).Exec(
aux,
graph.Params{
NetworkSimplexThoroughness: params.NetworkSimplexThoroughness,
diff --git a/internal/phase5/route_merge_test.go b/internal/phase5/route_merge_test.go
index 8a64c47..6e640c1 100644
--- a/internal/phase5/route_merge_test.go
+++ b/internal/phase5/route_merge_test.go
@@ -4,7 +4,6 @@ import (
"sort"
"testing"
- egraph "github.com/nulab/autog/graph"
"github.com/nulab/autog/internal/graph"
"github.com/stretchr/testify/assert"
)
@@ -151,6 +150,6 @@ func inIds(n *graph.Node) []string {
func fromEdgeSlice(es [][]string) *graph.DGraph {
g := &graph.DGraph{}
- egraph.EdgeSlice(es).Populate(g)
+ graph.EdgeSlice(es).Populate(g)
return g
}
diff --git a/internal/phase5/splines.go b/internal/phase5/splines.go
index 8b3f0e9..a18e441 100644
--- a/internal/phase5/splines.go
+++ b/internal/phase5/splines.go
@@ -85,7 +85,7 @@ func rectBetweenLayers(l1, l2 *graph.Layer) geom.Rect {
h1, h2 := l1.Head(), l2.Head()
t1, t2 := l2.Tail(), l2.Tail()
return geom.Rect{
- TL: geom.P{min(h1.X, h2.X), h1.Y + h1.H},
+ TL: geom.P{min(h1.X, h2.X), h1.Y + max(h1.H, l1.H)},
BR: geom.P{max(t1.X+t1.W, t2.X+t2.W), t2.Y},
}
}
@@ -104,8 +104,8 @@ func rectVirtualNode(vn *graph.Node, vl *graph.Layer) geom.Rect {
// this p-1 access is safe: a layer cannot contain only one virtual node
n := vl.Nodes[p-1]
return geom.Rect{
- TL: geom.P{n.X + n.W, n.Y},
- BR: geom.P{vn.X + 10, n.Y + n.H},
+ TL: geom.P{n.X + n.W + 10, n.Y},
+ BR: geom.P{vn.X, n.Y + n.H},
}
default:
diff --git a/internal/testfiles/adjacency_lists.go b/internal/testfiles/adjacency_lists.go
deleted file mode 100644
index 33b7117..0000000
--- a/internal/testfiles/adjacency_lists.go
+++ /dev/null
@@ -1,247 +0,0 @@
-//go:build unit
-
-package testfiles
-
-// This file contains graph adjancency lists used in regression tests
-
-var issues1and4 = [][]string{
- {"N1", "N2"},
- {"N3", "N1"},
- {"N2", "N3"},
- {"Nh", "N1"},
- {"Nk", "N1"},
- {"Na", "N2"},
- {"Na", "N3"},
- {"N2", "Nd"},
-}
-
-var issue9 = [][]string{
- {"N1", "N4"},
- {"N1", "N8"},
- {"N2", "N5"},
- {"N2", "N8"},
- {"N3", "N8"},
- {"N6", "N3"},
- {"N8", "N1"},
- {"N8", "N2"},
- {"N8", "N7"},
- {"N8", "N15"},
- {"N8", "N16"},
- {"N9", "N10"},
- {"N10", "N11"},
- {"N12", "N13"},
- {"N13", "N14"},
- {"N15", "N1"},
- {"N15", "N9"},
- {"N15", "N10"},
- {"N16", "N2"},
- {"N16", "N12"},
- {"N16", "N13"},
-}
-
-var simpleVirtualNodes = [][]string{
- {"N1", "N2"},
- {"N2", "N3"},
- {"N1", "N3"},
-}
-
-var cacooArch = [][]string{
- {"gql", "acc"},
- {"gql", "dia"},
- {"gql", "edt"},
- {"gql", "fld"},
- {"gql", "itg"},
- {"gql", "ntf"},
- {"gql", "org"},
- {"gql", "sub"},
- {"gql", "spt"},
- {"gql", "tmp"},
- {"acc", "lgc"},
- {"acc", "sub"},
- {"fld", "acc"},
- {"fld", "dia"},
- {"fld", "org"},
- {"fld", "sub"},
- {"dia", "acc"},
- {"dia", "fld"},
- {"dia", "lgc"},
- {"dia", "org"},
- {"dia", "sub"},
-}
-
-var cacooArch2 = [][]string{
- {"gql", "acc"},
- {"gql", "dia"},
- {"gql", "edt"},
- {"gql", "fld"},
- {"gql", "itg"},
- {"gql", "ntf"},
- {"gql", "org"},
- {"gql", "sub"},
- {"gql", "spt"},
- {"gql", "tmp"},
- {"acc", "lgc"},
- {"acc", "sub"},
- {"dia", "acc"},
- {"dia", "fld"},
- {"dia", "lgc"},
- {"dia", "org"},
- {"dia", "sub"},
- {"fld", "acc"},
- {"fld", "dia"},
- {"fld", "org"},
- {"fld", "sub"},
-}
-
-var DotAbstract = [][]string{
- {"S24", "27"},
- {"S24", "25"},
- {"S1", "10"},
- {"S1", "2"},
- {"S35", "36"},
- {"S35", "43"},
- {"S30", "31"},
- {"S30", "33"},
- {"9", "42"},
- {"9", "T1"},
- {"25", "T1"},
- {"25", "26"},
- {"27", "T24"},
- {"2", "3"},
- {"2", "16"},
- {"2", "17"},
- {"2", "T1"},
- {"2", "18"},
- {"10", "11"},
- {"10", "14"},
- {"10", "T1"},
- {"10", "13"},
- {"10", "12"},
- {"31", "T1"},
- {"31", "32"},
- {"33", "T30"},
- {"33", "34"},
- {"42", "4"},
- {"26", "4"},
- {"3", "4"},
- {"16", "15"},
- {"17", "19"},
- {"18", "29"},
- {"11", "4"},
- {"14", "15"},
- {"37", "39"},
- {"37", "41"},
- {"37", "38"},
- {"37", "40"},
- {"13", "19"},
- {"12", "29"},
- {"43", "38"},
- {"43", "40"},
- {"36", "19"},
- {"32", "23"},
- {"34", "29"},
- {"39", "15"},
- {"41", "29"},
- {"38", "4"},
- {"40", "19"},
- {"4", "5"},
- {"19", "21"},
- {"19", "20"},
- {"19", "28"},
- {"5", "6"},
- {"5", "T35"},
- {"5", "23"},
- {"21", "22"},
- {"20", "15"},
- {"28", "29"},
- {"6", "7"},
- {"15", "T1"},
- {"22", "23"},
- {"22", "T35"},
- {"29", "T30"},
- {"7", "T8"},
- {"23", "T24"},
- {"23", "T1"},
-}
-
-var elkTestGraphs = []struct {
- name string
- adj [][]string
-}{
- {"lib_decg_DECGPi", lib_decg_DECGPi},
- {"pn_brockackerman_BrockAckerman", pn_brockackerman_BrockAckerman},
-}
-
-var lib_decg_DECGPi = [][]string{
- {"N2", "N8"},
- {"N2", "N13"},
- {"N2", "N15"},
- {"N2", "N4"},
- {"N3", "N1"},
- {"N4", "N3"},
- {"N5", "N16"},
- {"N5", "N18"},
- {"N6", "N8"},
- {"N6", "N18"},
- {"N7", "N6"},
- {"N8", "N5"},
- {"N8", "N9"},
- {"N9", "N6"},
- {"N9", "N7"},
- {"N10", "N14"},
- {"N10", "N19"},
- {"N11", "N10"},
- {"N12", "N10"},
- {"N12", "N11"},
- {"N13", "N14"},
- {"N14", "N17"},
- {"N14", "N12"},
- {"N15", "N13"},
- {"N16", "N4"},
- {"N17", "N16"},
- {"N17", "N19"},
- {"N18", "N5"},
- {"N19", "N17"},
-}
-
-var pn_brockackerman_BrockAckerman = [][]string{
- {"N1", "N2"},
- {"N2", "N9"},
- {"N4", "N10"},
- {"N4", "N15"},
- {"N5", "N6"},
- {"N6", "N14"},
- {"N8", "N1"},
- {"N8", "N3"},
- {"N9", "N12"},
- {"N10", "N11"},
- {"N11", "N12"},
- {"N12", "N8"},
- {"N13", "N5"},
- {"N13", "N7"},
- {"N14", "N16"},
- {"N15", "N16"},
- {"N16", "N13"},
-}
-
-var singleSelfLoop = [][]string{
- {"a", "a"},
-}
-
-var withSelfLoop = [][]string{
- {"a", "b"},
- {"b", "c"},
- {"b", "b"},
- {"a", "d"},
-}
-
-var bkWrongAlignment = [][]string{
- {"a", "b"},
- {"b", "c"},
- {"a", "f"},
- {"f", "g"},
- {"a", "u"},
- {"f", "c"},
- {"c", "k"},
- {"f", "k"},
-}
diff --git a/internal/testfiles/bugfix_test.go b/internal/testfiles/bugfix_test.go
deleted file mode 100644
index 845b243..0000000
--- a/internal/testfiles/bugfix_test.go
+++ /dev/null
@@ -1,130 +0,0 @@
-//go:build unit
-
-package testfiles
-
-import (
- "fmt"
- "testing"
-
- "github.com/nulab/autog"
- "github.com/nulab/autog/graph"
- ig "github.com/nulab/autog/internal/graph"
- imonitor "github.com/nulab/autog/internal/monitor"
- "github.com/stretchr/testify/assert"
-)
-
-func TestCrashers(t *testing.T) {
- t.Run("phase1", func(t *testing.T) {
- t.Run("greedy cycle breaker fails to break cycles", func(t *testing.T) {
- assert.NotPanics(t, func() {
- _ = autog.Layout(
- graph.EdgeSlice(issue9),
- autog.WithNonDeterministicGreedyCycleBreaker(),
- )
- })
- })
- })
-
- t.Run("phase3 WMedian", func(t *testing.T) {
- t.Run("identical edge segfault in cross counting", func(t *testing.T) {
- src := graph.EdgeSlice(cacooArch)
- assert.NotPanics(t, func() { _ = autog.Layout(src) })
- })
-
- t.Run("wrong initialization of flat edges", func(t *testing.T) {
- src := graph.EdgeSlice(cacooArch2)
- assert.NotPanics(t, func() { _ = autog.Layout(src) })
- })
-
- t.Run("wrong handling of fixed positions in wmedian", func(t *testing.T) {
- c := make(chan any, 1)
- assert.NotPanics(t, func() {
- _ = autog.Layout(
- graph.EdgeSlice(DotAbstract),
- autog.WithPositioning(autog.PositioningNoop),
- autog.WithEdgeRouting(autog.EdgeRoutingNoop),
- autog.WithMonitor(imonitor.NewFilteredChan(c, imonitor.MatchAll(3, "gvdot", "crossings"))),
- )
- })
-
- assert.Equal(t, 46, <-c)
- })
- })
-
- t.Run("phase4 SinkColoring", func(t *testing.T) {
- t.Run("program hangs", func(t *testing.T) {
- src := graph.EdgeSlice(issues1and4)
- assert.NotPanics(t, func() { _ = autog.Layout(src, autog.WithPositioning(autog.PositioningSinkColoring)) })
- })
- })
-
- t.Run("phase4 NetworkSimplex", func(t *testing.T) {
- g := &ig.DGraph{}
- graph.EdgeSlice(DotAbstract).Populate(g)
- assert.NotPanics(t, func() {
- _ = autog.Layout(
- g,
- autog.WithPositioning(autog.PositioningNetworkSimplex),
- autog.WithEdgeRouting(autog.EdgeRoutingNoop),
- autog.WithNodeFixedSize(100, 100),
- )
- })
- // reduce expectations due to non-determinism
- // assertNoOverlaps(t, g, 1)
- })
-
- t.Run("phase4 B&K", func(t *testing.T) {
- t.Run("no overlaps", func(t *testing.T) {
- g := &ig.DGraph{}
- graph.EdgeSlice(bkWrongAlignment).Populate(g)
- _ = autog.Layout(g,
- autog.WithPositioning(autog.PositioningBrandesKoepf),
- autog.WithNodeFixedSize(130, 60),
- )
-
- assertNoOverlaps(t, g, 0)
- })
- })
-
- t.Run("output layout empty nodes", func(t *testing.T) {
- src := graph.EdgeSlice(simpleVirtualNodes)
- layout := autog.Layout(
- src,
- autog.WithPositioning(autog.PositioningVAlign),
- autog.WithEdgeRouting(autog.EdgeRoutingNoop),
- )
- assert.Len(t, layout.Nodes, 3)
- assert.Len(t, layout.Edges, 3)
- })
-
- t.Run("self-loop", func(t *testing.T) {
- t.Run("program halts", func(t *testing.T) {
- src := graph.EdgeSlice(withSelfLoop)
- assert.NotPanics(t, func() { _ = autog.Layout(src) })
- })
- t.Run("successful with single node", func(t *testing.T) {
- src := graph.EdgeSlice(singleSelfLoop)
- assert.NotPanics(t, func() { _ = autog.Layout(src) })
- })
- })
-}
-
-func assertNoOverlaps(t *testing.T, g *ig.DGraph, tolerance int) {
- overlaps := 0
- for _, l := range g.Layers {
- for j := 1; j < l.Len(); j++ {
- cur := l.Nodes[j]
- prv := l.Nodes[j-1]
-
- if prv.X+prv.W > cur.X {
- if overlaps >= tolerance {
- // note: this isn't a strict inequality because virtual nodes have size 0x0
- assert.Truef(t, prv.X+prv.W <= cur.X, "%s(X:%.2f) overlaps %s(X+W:%.2f)", cur, cur.X, prv, prv.X+prv.W)
- } else {
- fmt.Printf("warning: overlap between nodes %v and %v within tolerance\n", cur, prv)
- }
- overlaps++
- }
- }
- }
-}
diff --git a/internal/testfiles/feature_test.go b/internal/testfiles/feature_test.go
deleted file mode 100644
index 444f219..0000000
--- a/internal/testfiles/feature_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-//go:build unit
-
-package testfiles
-
-import (
- "testing"
-
- "github.com/nulab/autog"
- "github.com/nulab/autog/graph"
- ig "github.com/nulab/autog/internal/graph"
- "github.com/stretchr/testify/assert"
-)
-
-func TestOutputVirtualNodes(t *testing.T) {
- t.Run("keep virtual nodes", func(t *testing.T) {
- g := &ig.DGraph{}
- graph.EdgeSlice(simpleVirtualNodes).Populate(g)
- layout := autog.Layout(
- g,
- autog.WithPositioning(autog.PositioningVAlign),
- autog.WithEdgeRouting(autog.EdgeRoutingNoop),
- autog.WithOutputVirtualNodes(true),
- )
- assert.Len(t, layout.Nodes, 4)
- assert.Len(t, layout.Edges, 3)
- })
-
- t.Run("clip output nodes", func(t *testing.T) {
- g := &ig.DGraph{}
- graph.EdgeSlice(simpleVirtualNodes).Populate(g)
- layout := autog.Layout(
- g,
- autog.WithPositioning(autog.PositioningVAlign),
- autog.WithEdgeRouting(autog.EdgeRoutingNoop),
- autog.WithOutputVirtualNodes(false),
- )
- assert.Equal(t, 3, len(layout.Nodes))
- assert.Equal(t, 3, cap(layout.Nodes))
- assert.Equal(t, 4, len(g.Nodes))
-
- assert.Len(t, layout.Edges, 3)
- })
-}
diff --git a/internal/testfiles/regression_test.go b/internal/testfiles/regression_test.go
deleted file mode 100644
index 99b990e..0000000
--- a/internal/testfiles/regression_test.go
+++ /dev/null
@@ -1,25 +0,0 @@
-//go:build unit
-
-package testfiles
-
-import (
- "testing"
-
- "github.com/nulab/autog"
- "github.com/nulab/autog/graph"
- "github.com/stretchr/testify/assert"
-)
-
-func TestNoRegression(t *testing.T) {
- t.Run("ELK", func(t *testing.T) {
- for _, testcase := range elkTestGraphs {
- t.Run(testcase.name, func(t *testing.T) {
- assert.NotPanics(t, func() { autog.Layout(graph.EdgeSlice(testcase.adj)) })
- })
- }
- })
-
- t.Run("Dot", func(t *testing.T) {
- assert.NotPanics(t, func() { autog.Layout(graph.EdgeSlice(DotAbstract)) })
- })
-}