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