okami.mom/ultraviolet

git clone https://git.nkp.pet/okami.mom/ultraviolet

Ayman BagabasMerge Layout with Cassowary constraints (#88)2c67e98

upstream
5.0 KiB275 linesraw
1package casso
2
3import (
4	"errors"
5	"math"
6)
7
8type tag struct {
9	priority Priority
10	marker   Symbol
11	other    Symbol
12}
13
14// Solver implements the Cassowary constraint solving algorithm.
15type Solver struct {
16	tabs map[Symbol]Constraint
17	tags map[Symbol]tag
18
19	infeasible []Symbol
20
21	objective  expr
22	artificial expr
23}
24
25// NewSolver creates a new constraint solver.
26func NewSolver() *Solver {
27	return &Solver{
28		tabs: make(map[Symbol]Constraint),
29		tags: make(map[Symbol]tag),
30	}
31}
32
33// Val returns the current value of a symbol.
34func (s *Solver) Val(id Symbol) float64 {
35	row, ok := s.tabs[id]
36	if !ok {
37		return 0
38	}
39	return row.expr.constant
40}
41
42// Add adds a constraint to the solver with the given priority.
43func (s *Solver) Add(priority Priority, cell Constraint) (Symbol, error) {
44	t := tag{priority: priority}
45
46	c := cell
47	c.expr.terms = make([]Term, 0, len(c.expr.terms))
48
49	for _, term := range cell.expr.terms {
50		if eqz(term.coeff) {
51			continue
52		}
53		if term.id.isZero() {
54			return zero, errBadTerm
55		}
56		resolved, exists := s.tabs[term.id]
57		if !exists {
58			c.expr.addSymbol(term.coeff, term.id)
59			continue
60		}
61		c.expr.addExpr(term.coeff, resolved.expr)
62	}
63
64	switch c.op {
65	case LTE, GTE:
66		coeff := 1.0
67		if c.op == GTE {
68			coeff = -1.0
69		}
70
71		t.marker = next(slack)
72		c.expr.addSymbol(coeff, t.marker)
73
74		if priority < required {
75			t.other = next(errorSym)
76			c.expr.addSymbol(-coeff, t.other)
77			s.objective.addSymbol(float64(priority), t.other)
78		}
79	case EQ:
80		if priority < required {
81			t.marker = next(errorSym)
82			t.other = next(errorSym)
83
84			c.expr.addSymbol(-1.0, t.marker)
85			c.expr.addSymbol(1.0, t.other)
86
87			s.objective.addSymbol(float64(priority), t.marker)
88			s.objective.addSymbol(float64(priority), t.other)
89		} else {
90			t.marker = next(dummy)
91			c.expr.addSymbol(1.0, t.marker)
92		}
93	}
94
95	if c.expr.constant < 0.0 {
96		c.expr.negate()
97	}
98
99	subject, err := s.findSubject(c, t)
100	if err != nil {
101		return zero, err
102	}
103
104	if subject.isZero() {
105		if err := s.augmentArtificialVariable(c); err != nil {
106			return t.marker, err
107		}
108	} else {
109		c.expr.solveFor(subject)
110		s.substitute(subject, c.expr)
111		s.tabs[subject] = c
112	}
113
114	s.tags[t.marker] = t
115
116	return t.marker, s.optimizeAgainst(&s.objective)
117}
118
119const required Priority = 1e9
120
121func (s *Solver) findSubject(cell Constraint, t tag) (Symbol, error) {
122	for _, term := range cell.expr.terms {
123		if term.id.external() {
124			return term.id, nil
125		}
126	}
127
128	if t.marker.restricted() {
129		idx := cell.expr.find(t.marker)
130		if idx != -1 && cell.expr.terms[idx].coeff < 0.0 {
131			return t.marker, nil
132		}
133	}
134
135	if t.other.restricted() {
136		idx := cell.expr.find(t.other)
137		if idx != -1 && cell.expr.terms[idx].coeff < 0.0 {
138			return t.other, nil
139		}
140	}
141
142	for _, term := range cell.expr.terms {
143		if !term.id.isDummy() {
144			return zero, nil
145		}
146	}
147
148	if !eqz(cell.expr.constant) {
149		return zero, errUnsatisfiable
150	}
151
152	return t.marker, nil
153}
154
155func (s *Solver) substitute(id Symbol, e expr) {
156	for symbol := range s.tabs {
157		row := s.tabs[symbol]
158		row.expr.substitute(id, e)
159		s.tabs[symbol] = row
160		if symbol.external() || row.expr.constant >= 0.0 {
161			continue
162		}
163		s.infeasible = append(s.infeasible, symbol)
164	}
165	s.objective.substitute(id, e)
166	s.artificial.substitute(id, e)
167}
168
169func (s *Solver) optimizeAgainst(objective *expr) error {
170	for {
171		entry := zero
172		exit := zero
173
174		for _, term := range objective.terms {
175			if !term.id.isDummy() && term.coeff < 0.0 {
176				entry = term.id
177				break
178			}
179		}
180		if entry.isZero() {
181			return nil
182		}
183
184		ratio := math.MaxFloat64
185
186		for symbol := range s.tabs {
187			if symbol.external() {
188				continue
189			}
190			idx := s.tabs[symbol].expr.find(entry)
191			if idx == -1 {
192				continue
193			}
194			coeff := s.tabs[symbol].expr.terms[idx].coeff
195			if coeff >= 0.0 {
196				continue
197			}
198			r := -s.tabs[symbol].expr.constant / coeff
199			if r < ratio {
200				ratio, exit = r, symbol
201			}
202		}
203
204		row := s.tabs[exit]
205		delete(s.tabs, exit)
206
207		row.expr.solveForSymbols(exit, entry)
208
209		s.substitute(entry, row.expr)
210		s.tabs[entry] = row
211	}
212}
213
214func (s *Solver) augmentArtificialVariable(row Constraint) error {
215	art := next(slack)
216
217	s.tabs[art] = row.clone()
218	s.artificial = row.expr.clone()
219
220	if err := s.optimizeAgainst(&s.artificial); err != nil {
221		return err
222	}
223
224	success := eqz(s.artificial.constant)
225	s.artificial = newExpr(0.0)
226
227	artificial, ok := s.tabs[art]
228	if ok {
229		delete(s.tabs, art)
230
231		if len(artificial.expr.terms) == 0 {
232			return nil
233		}
234
235		entry := zero
236		for _, term := range artificial.expr.terms {
237			if term.id.restricted() {
238				entry = term.id
239				break
240			}
241		}
242		if entry.isZero() {
243			return errUnsatisfiable
244		}
245
246		artificial.expr.solveForSymbols(art, entry)
247
248		s.substitute(entry, artificial.expr)
249		s.tabs[entry] = artificial
250	}
251
252	for symbol, row := range s.tabs {
253		idx := row.expr.find(art)
254		if idx == -1 {
255			continue
256		}
257		row.expr.delete(idx)
258		s.tabs[symbol] = row
259	}
260
261	idx := s.objective.find(art)
262	if idx != -1 {
263		s.objective.delete(idx)
264	}
265
266	if !success {
267		return errUnsatisfiable
268	}
269	return nil
270}
271
272var (
273	errUnsatisfiable = errors.New("casso: constraint is unsatisfiable")
274	errBadTerm       = errors.New("casso: term references a nil symbol")
275)