implement basic constraint engine in gim, which will be used to determine positioning of vertices

This commit is contained in:
Brian Picciano 2017-11-25 14:32:48 -07:00
parent 286c2fbb35
commit 79a171323d
3 changed files with 204 additions and 0 deletions

View File

@ -455,6 +455,7 @@ func Equal(g1, g2 *Graph) bool {
// passed to the callback and used as the starting point of the traversal. If // passed to the callback and used as the starting point of the traversal. If
// the callback returns false the traversal is stopped. // the callback returns false the traversal is stopped.
func (g *Graph) Walk(startWith *Vertex, callback func(*Vertex) bool) { func (g *Graph) Walk(startWith *Vertex, callback func(*Vertex) bool) {
// TODO figure out how to make Walk deterministic
g.makeView() g.makeView()
if len(g.view) == 0 { if len(g.view) == 0 {
return return

View File

@ -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
}

View File

@ -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"},
)
}