From 79a171323d62846012cb95763d97e47e338d8edc Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sat, 25 Nov 2017 14:32:48 -0700 Subject: [PATCH] implement basic constraint engine in gim, which will be used to determine positioning of vertices --- gg/gg.go | 1 + gim/constraint/constraint.go | 109 ++++++++++++++++++++++++++++++ gim/constraint/constraint_test.go | 94 ++++++++++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 gim/constraint/constraint.go create mode 100644 gim/constraint/constraint_test.go diff --git a/gg/gg.go b/gg/gg.go index 2ddd4ff..a2eb3f0 100644 --- a/gg/gg.go +++ b/gg/gg.go @@ -455,6 +455,7 @@ func Equal(g1, g2 *Graph) bool { // passed to the callback and used as the starting point of the traversal. If // the callback returns false the traversal is stopped. func (g *Graph) Walk(startWith *Vertex, callback func(*Vertex) bool) { + // TODO figure out how to make Walk deterministic g.makeView() if len(g.view) == 0 { return diff --git a/gim/constraint/constraint.go b/gim/constraint/constraint.go new file mode 100644 index 0000000..f0c7d20 --- /dev/null +++ b/gim/constraint/constraint.go @@ -0,0 +1,109 @@ +// Package constraint implements an extremely simple constraint engine. +// Elements, and constraints on those elements, are given to the engine, which +// uses those constraints to generate an output. Elements are defined as a +// string +package constraint + +import ( + "github.com/mediocregopher/ginger/gg" +) + +// Constraint describes a constraint on an element. The Elem field must be +// filled in, as well as exactly one other field +type Constraint struct { + Elem string + + // LT says that Elem is less than this element + LT string +} + +const ltEdge = gg.Str("lt") + +// Engine processes sets of constraints to generate an output +type Engine struct { + g *gg.Graph +} + +// NewEngine initializes and returns an empty Engine +func NewEngine() *Engine { + return &Engine{g: gg.Null} +} + +// AddConstraint adds the given constraint to the engine's set, returns false if +// the constraint couldn't be added due to a conflict with a previous constraint +func (e *Engine) AddConstraint(c Constraint) bool { + elem := gg.Str(c.Elem) + g := e.g.AddValueIn(gg.ValueOut(elem, ltEdge), gg.Str(c.LT)) + + // Check for loops in g starting at c.Elem, bail if there are any + { + seen := map[*gg.Vertex]bool{} + start := g.Value(elem) + var hasLoop func(v *gg.Vertex) bool + hasLoop = func(v *gg.Vertex) bool { + if seen[v] { + return v == start + } + seen[v] = true + for _, out := range v.Out { + if hasLoop(out.To) { + return true + } + } + return false + } + if hasLoop(start) { + return false + } + } + + e.g = g + return true +} + +// Solve uses the constraints which have been added to the engine to give a +// possible solution. The given element is one which has been added to the +// engine and whose value is known to be zero. +func (e *Engine) Solve() map[string]int { + m := map[string]int{} + if len(e.g.Values()) == 0 { + return m + } + + vElem := func(v *gg.Vertex) string { + return string(v.Value.(gg.Str)) + } + + // first the roots are determined to be the elements with no In edges, which + // _must_ exist since the graph presumably has no loops + var roots []*gg.Vertex + e.g.Walk(nil, func(v *gg.Vertex) bool { + if len(v.In) == 0 { + roots = append(roots, v) + m[vElem(v)] = 0 + } + return true + }) + + // sanity check + if len(roots) == 0 { + panic("no roots found in graph somehow") + } + + // a vertex's value is then the length of the longest path from it to one of + // the roots + var walk func(*gg.Vertex, int) + walk = func(v *gg.Vertex, val int) { + if elem := vElem(v); val > m[elem] { + m[elem] = val + } + for _, out := range v.Out { + walk(out.To, val+1) + } + } + for _, root := range roots { + walk(root, 0) + } + + return m +} diff --git a/gim/constraint/constraint_test.go b/gim/constraint/constraint_test.go new file mode 100644 index 0000000..f1e5a94 --- /dev/null +++ b/gim/constraint/constraint_test.go @@ -0,0 +1,94 @@ +package constraint + +import ( + . "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEngineAddConstraint(t *T) { + { + e := NewEngine() + assert.True(t, e.AddConstraint(Constraint{Elem: "0", LT: "1"})) + assert.True(t, e.AddConstraint(Constraint{Elem: "1", LT: "2"})) + assert.True(t, e.AddConstraint(Constraint{Elem: "-1", LT: "0"})) + assert.False(t, e.AddConstraint(Constraint{Elem: "1", LT: "0"})) + assert.False(t, e.AddConstraint(Constraint{Elem: "2", LT: "0"})) + assert.False(t, e.AddConstraint(Constraint{Elem: "2", LT: "-1"})) + } + + { + e := NewEngine() + assert.True(t, e.AddConstraint(Constraint{Elem: "0", LT: "1"})) + assert.True(t, e.AddConstraint(Constraint{Elem: "0", LT: "2"})) + assert.True(t, e.AddConstraint(Constraint{Elem: "1", LT: "2"})) + assert.True(t, e.AddConstraint(Constraint{Elem: "2", LT: "3"})) + } +} + +func TestEngineSolve(t *T) { + assertSolve := func(exp map[string]int, cc ...Constraint) { + e := NewEngine() + for _, c := range cc { + assert.True(t, e.AddConstraint(c), "c:%#v", c) + } + assert.Equal(t, exp, e.Solve()) + } + + // basic + assertSolve( + map[string]int{"a": 0, "b": 1, "c": 2}, + Constraint{Elem: "a", LT: "b"}, + Constraint{Elem: "b", LT: "c"}, + ) + + // "triangle" graph + assertSolve( + map[string]int{"a": 0, "b": 1, "c": 2}, + Constraint{Elem: "a", LT: "b"}, + Constraint{Elem: "a", LT: "c"}, + Constraint{Elem: "b", LT: "c"}, + ) + + // "hexagon" graph + assertSolve( + map[string]int{"a": 0, "b": 1, "c": 1, "d": 2, "e": 2, "f": 3}, + Constraint{Elem: "a", LT: "b"}, + Constraint{Elem: "a", LT: "c"}, + Constraint{Elem: "b", LT: "d"}, + Constraint{Elem: "c", LT: "e"}, + Constraint{Elem: "d", LT: "f"}, + Constraint{Elem: "e", LT: "f"}, + ) + + // "hexagon" with centerpoint graph + assertSolve( + map[string]int{"a": 0, "b": 1, "c": 1, "center": 2, "d": 3, "e": 3, "f": 4}, + Constraint{Elem: "a", LT: "b"}, + Constraint{Elem: "a", LT: "c"}, + Constraint{Elem: "b", LT: "d"}, + Constraint{Elem: "c", LT: "e"}, + Constraint{Elem: "d", LT: "f"}, + Constraint{Elem: "e", LT: "f"}, + + Constraint{Elem: "c", LT: "center"}, + Constraint{Elem: "b", LT: "center"}, + Constraint{Elem: "center", LT: "e"}, + Constraint{Elem: "center", LT: "d"}, + ) + + // multi-root, using two triangles which end up connecting + assertSolve( + map[string]int{"a": 0, "b": 1, "c": 2, "d": 0, "e": 1, "f": 2, "g": 3}, + Constraint{Elem: "a", LT: "b"}, + Constraint{Elem: "a", LT: "c"}, + Constraint{Elem: "b", LT: "c"}, + + Constraint{Elem: "d", LT: "e"}, + Constraint{Elem: "d", LT: "f"}, + Constraint{Elem: "e", LT: "f"}, + + Constraint{Elem: "f", LT: "g"}, + ) + +}