implement basic constraint engine in gim, which will be used to determine positioning of vertices
This commit is contained in:
parent
286c2fbb35
commit
79a171323d
1
gg/gg.go
1
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
|
||||
|
109
gim/constraint/constraint.go
Normal file
109
gim/constraint/constraint.go
Normal 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
|
||||
}
|
94
gim/constraint/constraint_test.go
Normal file
94
gim/constraint/constraint_test.go
Normal 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"},
|
||||
)
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user