cassowary-ruby 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/LICENSE +20 -0
- data/README.md +18 -0
- data/lib/cassowary.rb +25 -0
- data/lib/constraint/edit_or_stay_constraint.rb +40 -0
- data/lib/constraint/linear_constraint.rb +23 -0
- data/lib/constraint.rb +30 -0
- data/lib/ext/float.rb +24 -0
- data/lib/ext/numeric.rb +11 -0
- data/lib/ext/object.rb +11 -0
- data/lib/linear_expression.rb +163 -0
- data/lib/simplex_solver.rb +704 -0
- data/lib/strength.rb +31 -0
- data/lib/symbolic_weight.rb +115 -0
- data/lib/utils/equalities.rb +27 -0
- data/lib/variables/abstract_variable.rb +35 -0
- data/lib/variables/dummy_variable.rb +21 -0
- data/lib/variables/objective_variable.rb +17 -0
- data/lib/variables/slack_variable.rb +17 -0
- data/lib/variables/variable.rb +56 -0
- data/lib/variables.rb +5 -0
- data/test/test_abstract_methods.rb +22 -0
- data/test/test_cassowary.rb +184 -0
- data/test/test_ext.rb +38 -0
- data/test/test_helper.rb +12 -0
- data/test/test_variables.rb +33 -0
- metadata +106 -0
@@ -0,0 +1,704 @@
|
|
1
|
+
# Copyright (C) 2012 by Tim Felgentreff
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module Cassowary
|
6
|
+
class SimplexSolver
|
7
|
+
|
8
|
+
attr_accessor :rows, :columns, :objective, :infeasible_rows,
|
9
|
+
:stay_plus_error_vars, :stay_minus_error_vars, :edit_vars,
|
10
|
+
:edit_constraints, :edit_plus_error_vars, :edit_minus_error_vars,
|
11
|
+
:prev_edit_constants, :new_edit_constants, :marker_vars,
|
12
|
+
:error_vars, :auto_solve
|
13
|
+
|
14
|
+
Epsilon = 1.0e-8
|
15
|
+
|
16
|
+
def add_bounds(var, lower = nil, upper = nil)
|
17
|
+
add_constraint lower.cn_leq(var) if lower
|
18
|
+
add_constraint var.cn_leq(upper) if upper
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_constraint(constraint)
|
22
|
+
expr = make_expression(constraint)
|
23
|
+
unless try_adding_directly(expr)
|
24
|
+
add_with_artificial_variable(expr)
|
25
|
+
end
|
26
|
+
if auto_solve
|
27
|
+
optimize(objective)
|
28
|
+
set_external_variables
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def remove_constraint(cn)
|
33
|
+
reset_stay_constants
|
34
|
+
|
35
|
+
# remove any error variables from the objective function
|
36
|
+
evars = error_vars.delete(cn) || []
|
37
|
+
zrow = objective
|
38
|
+
obj = rows[zrow]
|
39
|
+
evars.each do |v|
|
40
|
+
expr = rows[v]
|
41
|
+
if expr.nil?
|
42
|
+
obj.add_variable(v, cn.strength.symbolic_weight * -cn.weight, zrow, self)
|
43
|
+
else
|
44
|
+
obj.add_expression(expr, cn.strength.symbolic_weight * -cn.weight, zrow, self)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
exit_var = nil
|
49
|
+
col = nil
|
50
|
+
min_ratio = 0
|
51
|
+
|
52
|
+
# try to make the marker variable basic, if it isn't already
|
53
|
+
marker = marker_vars.delete(cn)
|
54
|
+
unless rows.has_key? marker
|
55
|
+
# choose which variable to move out of the basis. only consider restricted basic vars
|
56
|
+
col = columns[marker]
|
57
|
+
col.each do |v|
|
58
|
+
if v.restricted?
|
59
|
+
expr = rows[v]
|
60
|
+
coeff = expr.coefficient_for(marker)
|
61
|
+
# only consider negative coefficients
|
62
|
+
if coeff < 0.0
|
63
|
+
r = 0.0 - expr.constant / coeff
|
64
|
+
if exit_var.nil? or r < min_ratio
|
65
|
+
min_ratio = r
|
66
|
+
exit_var = v
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# If exitVar is still nil at this point, then either the
|
73
|
+
# marker variable has a positive coefficient in all equations,
|
74
|
+
# or it only occurs in equations for unrestricted variables.
|
75
|
+
# If it does occur in an equation for a restricted variable,
|
76
|
+
# pick the equation that gives the smallest ratio. (The row
|
77
|
+
# with the marker variable will become infeasible, but all the
|
78
|
+
# other rows will still be feasible; and we will be dropping
|
79
|
+
# the row with the marker variable. In effect we are removing
|
80
|
+
# the non-negativity restriction on the marker variable.)
|
81
|
+
if exit_var.nil?
|
82
|
+
col.each do |v|
|
83
|
+
if v.restricted?
|
84
|
+
expr = rows[v]
|
85
|
+
coeff = expr.coefficient_for(marker)
|
86
|
+
r = expr.constant / coeff
|
87
|
+
if exit_var.nil? or r < min_ratio
|
88
|
+
min_ratio = r
|
89
|
+
exit_var = v
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# If exitVar is still nil, and col is empty, then exitVar
|
96
|
+
# doesn't occur in any equations, so just remove it.
|
97
|
+
# Otherwise pick an exit var from among the unrestricted
|
98
|
+
# variables whose equation involves the marker var
|
99
|
+
if exit_var.nil?
|
100
|
+
if col.empty?
|
101
|
+
remove_parametric_var(marker)
|
102
|
+
else
|
103
|
+
exit_var = col.to_a.first
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
if exit_var
|
108
|
+
pivot(marker, exit_var)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Now delete any error variables. If cn is an inequality, it
|
113
|
+
# also contains a slack variable; but we use that as the
|
114
|
+
# marker variable and so it has been deleted when we removed
|
115
|
+
# its row
|
116
|
+
if rows.has_key?(marker)
|
117
|
+
remove_row(marker)
|
118
|
+
end
|
119
|
+
evars.each do |v|
|
120
|
+
remove_parametric_var(v) unless v == marker
|
121
|
+
end
|
122
|
+
|
123
|
+
if cn.stay_constraint?
|
124
|
+
self.stay_plus_error_vars = stay_plus_error_vars.reject do |v| evars.include? v end
|
125
|
+
self.stay_minus_error_vars = stay_minus_error_vars.reject do |v| evars.include? v end
|
126
|
+
end
|
127
|
+
|
128
|
+
if cn.edit_constraint?
|
129
|
+
# find the index in editPlusErrorVars of the error variable for this constraint
|
130
|
+
index = find_edit_error_index(evars)
|
131
|
+
|
132
|
+
# remove the error variables from editPlusErrorVars and editMinusErrorVars
|
133
|
+
edit_plus_error_vars.delete_at(index)
|
134
|
+
edit_minus_error_vars.delete_at(index)
|
135
|
+
|
136
|
+
# remove the constants from prevEditConstants
|
137
|
+
prev_edit_constants.delete_at(index)
|
138
|
+
end
|
139
|
+
|
140
|
+
if auto_solve
|
141
|
+
optimize(zrow)
|
142
|
+
set_external_variables
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def resolve(cs = nil)
|
147
|
+
if cs
|
148
|
+
self.new_edit_constants = cs
|
149
|
+
end
|
150
|
+
|
151
|
+
# Re-solve the current collection of constraints for the new values in newEditConstants.
|
152
|
+
self.infeasible_rows = []
|
153
|
+
reset_stay_constants
|
154
|
+
reset_edit_constants
|
155
|
+
dual_optimize
|
156
|
+
set_external_variables
|
157
|
+
end
|
158
|
+
|
159
|
+
def solve
|
160
|
+
optimize objective
|
161
|
+
set_external_variables
|
162
|
+
end
|
163
|
+
|
164
|
+
def suggest_value(var, val)
|
165
|
+
edit_vars.each_with_index do |v, idx|
|
166
|
+
if v == var
|
167
|
+
new_edit_constants[idx] = val
|
168
|
+
end
|
169
|
+
return self
|
170
|
+
end
|
171
|
+
raise InternalError, "variable not currently being edited"
|
172
|
+
end
|
173
|
+
|
174
|
+
def add_edit_var(variable, strength)
|
175
|
+
add_constraint(EditConstraint.new variable: variable, strength: strength)
|
176
|
+
end
|
177
|
+
|
178
|
+
def add_stay(variable, strength = Strength::WeakStrength)
|
179
|
+
add_constraint(StayConstraint.new variable: variable, strength: strength)
|
180
|
+
end
|
181
|
+
|
182
|
+
def begin_edit
|
183
|
+
self.new_edit_constants = [nil] * edit_vars.size
|
184
|
+
end
|
185
|
+
|
186
|
+
def end_edit
|
187
|
+
edit_constraints.each do |cn|
|
188
|
+
remove_constraint(cn)
|
189
|
+
end
|
190
|
+
self.edit_vars = []
|
191
|
+
self.edit_constraints = []
|
192
|
+
end
|
193
|
+
|
194
|
+
def note_added_variable(var, subject)
|
195
|
+
if subject
|
196
|
+
columns[var] ||= Set.new
|
197
|
+
columns[var] << subject
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def note_removed_variable(var, subject)
|
202
|
+
if subject
|
203
|
+
columns[var].delete(subject)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
private
|
208
|
+
def add_row(var, expr)
|
209
|
+
rows[var] = expr
|
210
|
+
expr.each_variable_and_coefficient do |v, c|
|
211
|
+
columns[v] ||= Set.new
|
212
|
+
columns[v] << var
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def add_with_artificial_variable(expr)
|
217
|
+
av = SlackVariable.new
|
218
|
+
az = ObjectiveVariable.new
|
219
|
+
azrow = LinearExpression.new
|
220
|
+
|
221
|
+
# the artificial objective is av, which we know is equal to expr
|
222
|
+
# (which contains only parametric variables)
|
223
|
+
azrow.constant = expr.constant
|
224
|
+
expr.each_variable_and_coefficient do |v, c|
|
225
|
+
azrow.terms[v] = c
|
226
|
+
end
|
227
|
+
|
228
|
+
add_row(az, azrow)
|
229
|
+
add_row(av, expr)
|
230
|
+
|
231
|
+
# try to optimize av to 0
|
232
|
+
optimize az
|
233
|
+
|
234
|
+
# Check that we were able to make the objective value 0. If
|
235
|
+
# not, the original constraint was unsatisfiable.
|
236
|
+
raise RequiredFailure unless azrow.constant.cl_approx_zero
|
237
|
+
|
238
|
+
if e = rows[av]
|
239
|
+
# Find another variable in this row and pivot, so that av
|
240
|
+
# becomes parametric. If there isn't another variable in the
|
241
|
+
# row then the tableau contains the equation av=0 -- just
|
242
|
+
# delete av's row.
|
243
|
+
if e.constant?
|
244
|
+
remove_row(av)
|
245
|
+
return nil
|
246
|
+
else
|
247
|
+
pivot(e.any_variable, av)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# av should be parametric at this point
|
252
|
+
remove_parametric_var av
|
253
|
+
|
254
|
+
# remove the temporary objective function
|
255
|
+
remove_row az
|
256
|
+
end
|
257
|
+
|
258
|
+
def choose_subject(expr)
|
259
|
+
# We are trying to add the constraint expr=0 to the tableaux.
|
260
|
+
# Try to choose a subject (a variable to become basic) from
|
261
|
+
# among the current variables in expr. If expr contains any
|
262
|
+
# unrestricted variables, then we must choose an unrestricted
|
263
|
+
# variable as the subject. Also, if the subject is new to the
|
264
|
+
# solver we won't have to do any substitutions, so we prefer new
|
265
|
+
# variables to ones that are currently noted as parametric. If
|
266
|
+
# expr contains only restricted variables, if there is a
|
267
|
+
# restricted variable with a negative coefficient that is new to
|
268
|
+
# the solver we can make that the subject. Otherwise we can't
|
269
|
+
# find a subject, so return nil. (In this last case we have to
|
270
|
+
# add an artificial variable and use that variable as the
|
271
|
+
# subject -- this is done outside this method though.)
|
272
|
+
#
|
273
|
+
# Note: in checking for variables that are new to the solver, we
|
274
|
+
# ignore whether a variable occurs in the objective function, since
|
275
|
+
# new slack variables are added to the objective function by
|
276
|
+
# 'makeExpression:', which is called before this method.
|
277
|
+
found_unrestricted = false
|
278
|
+
found_new_restricted = false
|
279
|
+
subject = nil
|
280
|
+
coeff = nil
|
281
|
+
|
282
|
+
expr.each_variable_and_coefficient do |v, c|
|
283
|
+
if found_unrestricted
|
284
|
+
# We have already found an unrestricted variable. The only
|
285
|
+
# time we will want to use v instead of the current choice
|
286
|
+
# 'subject' is if v is unrestricted and new to the solver
|
287
|
+
# and 'subject' isn't new. If this is the case just pick v
|
288
|
+
# immediately and return.
|
289
|
+
unless v.restricted?
|
290
|
+
return v unless columns.has_key? v
|
291
|
+
end
|
292
|
+
else
|
293
|
+
if v.restricted?
|
294
|
+
# v is restricted. If we have already found a suitable
|
295
|
+
# restricted variable just stick with that. Otherwise, if
|
296
|
+
# v is new to the solver and has a negative coefficient
|
297
|
+
# pick it. Regarding being new to the solver -- if the
|
298
|
+
# variable occurs only in the objective function we regard
|
299
|
+
# it as being new to the solver, since error variables are
|
300
|
+
# added to the objective function when we make the
|
301
|
+
# expression. We also never pick a dummy variable here.
|
302
|
+
if !found_new_restricted and !v.dummy? and c < 0.0
|
303
|
+
col = columns[v]
|
304
|
+
if col.nil? or (col.size == 1 and col.include? objective)
|
305
|
+
subject = v
|
306
|
+
found_new_restricted = true
|
307
|
+
end
|
308
|
+
end
|
309
|
+
else
|
310
|
+
# v is unrestricted. If v is also new to the solver just
|
311
|
+
# pick it now
|
312
|
+
return v unless columns.has_key? v
|
313
|
+
subject = v
|
314
|
+
found_unrestricted = true
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# subject is nil. Make one last check -- if all of the
|
320
|
+
# variables in expr are dummy variables, then we can pick a
|
321
|
+
# dummy variable as the subject.
|
322
|
+
return subject unless subject.nil?
|
323
|
+
expr.each_variable_and_coefficient do |v, c|
|
324
|
+
return nil unless v.dummy?
|
325
|
+
# if v is new to the solver tentatively make it the subject
|
326
|
+
unless columns.has_key? v
|
327
|
+
subject = v
|
328
|
+
coeff = c
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
# If we get this far, all of the variables in the expression
|
333
|
+
# should be dummy variables. If the constant is nonzero we are
|
334
|
+
# trying to add an unsatisfiable required constraint. (Remember
|
335
|
+
# that dummy variables must take on a value of 0.) Otherwise,
|
336
|
+
# if the constant is zero, multiply by -1 if necessary to make
|
337
|
+
# the coefficient for the subject negative.
|
338
|
+
raise RequiredFailure unless expr.constant.cl_approx_zero
|
339
|
+
if coeff > 0
|
340
|
+
expr.each_variable_and_coefficient do |v, c|
|
341
|
+
expr.terms[v] = 0.0 - c
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
subject
|
346
|
+
end
|
347
|
+
|
348
|
+
def delta_edit_constant(delta, plus_error_var, minus_error_var)
|
349
|
+
if expr = rows[plus_error_var]
|
350
|
+
expr.increment_constant delta
|
351
|
+
# error variables are always restricted -- so the row is
|
352
|
+
# infeasible if the constant is negative
|
353
|
+
(infeasible_rows << plus_error_var) if expr.constant < 0.0
|
354
|
+
return nil
|
355
|
+
end
|
356
|
+
|
357
|
+
if expr = rows[minus_error_var]
|
358
|
+
expr.increment_constant -delta
|
359
|
+
(infeasible_rows << plus_error_var) if expr.constant < 0.0
|
360
|
+
return nil
|
361
|
+
end
|
362
|
+
|
363
|
+
# Neither minusErrorVar nor plusErrorVar is basic. So they must
|
364
|
+
# both be nonbasic, and will both occur in exactly the same
|
365
|
+
# expressions. Find all the expressions in which they occur by
|
366
|
+
# finding the column for the minusErrorVar (it doesn't matter
|
367
|
+
# whether we look for that one or for plusErrorVar). Fix the
|
368
|
+
# constants in these expressions.
|
369
|
+
columns[minus_error_var].each do |basic_var|
|
370
|
+
expr = rows[basic_var]
|
371
|
+
c = expr.coefficient_for(minus_error_var)
|
372
|
+
expr.increment_constant c * delta
|
373
|
+
if basic_var.restricted? and expr.constant < 0.0
|
374
|
+
infeasible_rows << basic_var
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def dual_optimize
|
380
|
+
# We have set new values for the constants in the edit
|
381
|
+
# constraints. Re-optimize using the dual simplex algorithm.
|
382
|
+
entry_var = nil
|
383
|
+
zrow = rows[objective]
|
384
|
+
until infeasible_rows.empty?
|
385
|
+
exit_var = infeasible_rows.shift
|
386
|
+
if expr = rows[exit_var]
|
387
|
+
if expr.constant < 0.0
|
388
|
+
ratio = nil
|
389
|
+
expr.each_variable_and_coefficient do |v, c|
|
390
|
+
if c > 0.0 and v.pivotable?
|
391
|
+
zc = zrow.terms[v]
|
392
|
+
r = zc ? zc / c : SymbolicWeight::Zero
|
393
|
+
if ratio.nil? or r < ratio or (r == ratio and v.hash < entry_var.hash)
|
394
|
+
entry_var = v
|
395
|
+
ratio = r
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
raise InternalError if ratio.nil?
|
400
|
+
pivot entry_var, exit_var
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def find_edit_error_index(evars)
|
407
|
+
evars.each do |v|
|
408
|
+
if index = edit_plus_error_vars.index(v)
|
409
|
+
return index
|
410
|
+
end
|
411
|
+
end
|
412
|
+
raise InternalError, "didn't find a variable"
|
413
|
+
end
|
414
|
+
|
415
|
+
def initialize
|
416
|
+
self.objective = ObjectiveVariable.new
|
417
|
+
self.rows = {objective => LinearExpression.new_with_symbolic_weight}
|
418
|
+
self.columns = {}
|
419
|
+
self.infeasible_rows = []
|
420
|
+
self.prev_edit_constants = []
|
421
|
+
self.stay_plus_error_vars = []
|
422
|
+
self.stay_minus_error_vars = []
|
423
|
+
self.edit_vars = []
|
424
|
+
self.edit_constraints = []
|
425
|
+
self.edit_plus_error_vars = []
|
426
|
+
self.edit_minus_error_vars = []
|
427
|
+
self.marker_vars = {}
|
428
|
+
self.error_vars = {}
|
429
|
+
self.auto_solve = true
|
430
|
+
end
|
431
|
+
|
432
|
+
def make_expression(cn)
|
433
|
+
# Make a new linear expression representing the constraint cn,
|
434
|
+
# replacing any basic variables with their defining expressions.
|
435
|
+
# Normalize if necessary so that the constant is non-negative.
|
436
|
+
# If the constraint is non-required give its error variables an
|
437
|
+
# appropriate weight in the objective function.
|
438
|
+
expr = LinearExpression.new
|
439
|
+
cnexpr = cn.expression
|
440
|
+
expr.constant = cnexpr.constant
|
441
|
+
cnexpr.each_variable_and_coefficient do |v, c|
|
442
|
+
e = rows[v]
|
443
|
+
if e.nil?
|
444
|
+
expr.add_variable(v, c)
|
445
|
+
else
|
446
|
+
expr.add_expression(e, c)
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
# add slack and error variables as needed
|
451
|
+
if cn.inequality?
|
452
|
+
# cn is an inequality, so add a slack variable. The original
|
453
|
+
# constraint is expr>=0, so that the resulting equality is
|
454
|
+
# expr-slackVar=0. If cn is also non-required add a negative
|
455
|
+
# error variable, giving expr-slackVar = -errorVar, in other
|
456
|
+
# words expr-slackVar+errorVar=0. Since both of these
|
457
|
+
# variables are newly created we can just add them to the
|
458
|
+
# expression (they can't be basic).
|
459
|
+
slackvar = SlackVariable.new
|
460
|
+
expr.terms[slackvar] = -1.0
|
461
|
+
marker_vars[cn] = slackvar
|
462
|
+
unless cn.required?
|
463
|
+
eminus = SlackVariable.new
|
464
|
+
expr.terms[eminus] = 1.0
|
465
|
+
|
466
|
+
zrow = rows[objective]
|
467
|
+
zrow.terms[eminus] = cn.strength.symbolic_weight * cn.weight
|
468
|
+
error_vars[cb] = [eminus]
|
469
|
+
note_added_variable(eminus, objective)
|
470
|
+
end
|
471
|
+
else
|
472
|
+
if cn.required?
|
473
|
+
# Add a dummy variable to the expression to serve as a
|
474
|
+
# marker for this constraint. The dummy variable is never
|
475
|
+
# allowed to enter the basis when pivoting.
|
476
|
+
dummyvar = DummyVariable.new
|
477
|
+
expr.terms[dummyvar] = 1.0
|
478
|
+
marker_vars[cn] = dummyvar
|
479
|
+
else
|
480
|
+
# cn is a non-required equality. Add a positive and a
|
481
|
+
# negative error variable, making the resulting constraint
|
482
|
+
# expr = eplus - eminus, in other words expr-eplus+eminus=0
|
483
|
+
eplus = SlackVariable.new
|
484
|
+
eminus = SlackVariable.new
|
485
|
+
expr.terms[eplus] = -1.0
|
486
|
+
expr.terms[eminus] = 1.0
|
487
|
+
|
488
|
+
# index the constraint under one of the error variables
|
489
|
+
marker_vars[cn] = eplus
|
490
|
+
zrow = rows[objective]
|
491
|
+
zrow.terms[eplus] = cn.strength.symbolic_weight * cn.weight
|
492
|
+
note_added_variable(eplus, objective)
|
493
|
+
zrow.terms[eminus] = cn.strength.symbolic_weight * cn.weight
|
494
|
+
error_vars[cn] = [eplus, eminus]
|
495
|
+
note_added_variable(eminus, objective)
|
496
|
+
|
497
|
+
if cn.stay_constraint?
|
498
|
+
stay_plus_error_vars << eplus
|
499
|
+
stay_minus_error_vars << eminus
|
500
|
+
end
|
501
|
+
|
502
|
+
if cn.edit_constraint?
|
503
|
+
edit_vars << cn.variable
|
504
|
+
edit_constraints << cn
|
505
|
+
edit_plus_error_vars << eplus
|
506
|
+
edit_minus_error_vars << eminus
|
507
|
+
prev_edit_constants << cnexpr.constant
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
# The constant in the expression should be non-negative. If
|
513
|
+
# necessary normalize the expression by multiplying by -1.
|
514
|
+
if expr.constant < 0
|
515
|
+
expr.constant = 0.0 - expr.constant
|
516
|
+
expr.each_variable_and_coefficient do |v, c|
|
517
|
+
expr.terms[v] = 0.0 - c
|
518
|
+
end
|
519
|
+
end
|
520
|
+
expr
|
521
|
+
end
|
522
|
+
|
523
|
+
def optimize(zvar)
|
524
|
+
# Minimize the value of the objective. (The tableau should
|
525
|
+
# already be feasible.)
|
526
|
+
zrow = rows[zvar]
|
527
|
+
exitvar = nil
|
528
|
+
while true do
|
529
|
+
# Find a variable in the objective function with a negative
|
530
|
+
# coefficient (ignoring dummy variables). If all coefficients
|
531
|
+
# are positive we're done. To implement Bland's anticycling
|
532
|
+
# rule, if there is more than one variable with a negative
|
533
|
+
# coefficient, pick the one with the smaller id (implemented
|
534
|
+
# as hash).
|
535
|
+
entryvar = nil
|
536
|
+
zrow.each_variable_and_coefficient do |v, c|
|
537
|
+
if v.pivotable? and c.definitely_negative and (entryvar.nil? or v.hash < entryvar.hash)
|
538
|
+
entryvar = v
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
# if all coefficients were positive (or if the objective
|
543
|
+
# function has no pivotable variables) we are at optimum
|
544
|
+
return nil if entryvar.nil?
|
545
|
+
|
546
|
+
# Choose which variable to move out of the basis. Only
|
547
|
+
# consider pivotable basic variables (that is, restricted,
|
548
|
+
# non-dummy variables).
|
549
|
+
minratio = nil
|
550
|
+
columns[entryvar].each do |v|
|
551
|
+
if v.pivotable?
|
552
|
+
expr = rows[v]
|
553
|
+
coeff = expr.coefficient_for(entryvar)
|
554
|
+
|
555
|
+
if coeff < 0.0
|
556
|
+
r = -(expr.constant / coeff)
|
557
|
+
# Decide whether to make v be the best choice for exit
|
558
|
+
# variable so far by comparing the ratios. In case of a
|
559
|
+
# tie, choose the variable with the smaller id (to
|
560
|
+
# implement Bland's anticycling rule).
|
561
|
+
if minratio.nil? or r < minratio or (r == minratio and v.hash < exitvar.hash)
|
562
|
+
minratio = r
|
563
|
+
exitvar = v
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
# If minRatio is still nil at this point, it means that the
|
570
|
+
# objective function is unbounded, i.e. it can become
|
571
|
+
# arbitrarily negative. This should never happen in this
|
572
|
+
# application.
|
573
|
+
raise InternalError if minratio.nil?
|
574
|
+
pivot entryvar, exitvar
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
def pivot(entryvar, exitvar)
|
579
|
+
# Do a pivot. Move entryVar into the basis (i.e. make it a
|
580
|
+
# basic variable), and move exitVar out of the basis (i.e. make
|
581
|
+
# it a parametric variable). expr is the expression for the
|
582
|
+
# exit variable (about to leave the basis) -- so that the old
|
583
|
+
# tableau includes the equation exitVar = expr
|
584
|
+
expr = remove_row(exitvar)
|
585
|
+
|
586
|
+
# Compute an expression for the entry variable. Since expr has
|
587
|
+
# been deleted from the tableau we can destructively modify it
|
588
|
+
# to build this expression.
|
589
|
+
expr.change_subject exitvar, entryvar
|
590
|
+
substitute_out(entryvar, expr)
|
591
|
+
add_row(entryvar, expr)
|
592
|
+
end
|
593
|
+
|
594
|
+
def remove_parametric_var(var)
|
595
|
+
set = columns.delete(var)
|
596
|
+
set.each do |v|
|
597
|
+
rows[v].terms.delete(var)
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
def remove_row(var)
|
602
|
+
expr = rows.delete(var)
|
603
|
+
expr.each_variable_and_coefficient do |v, c|
|
604
|
+
columns[v].delete var
|
605
|
+
end
|
606
|
+
infeasible_rows.delete(var)
|
607
|
+
expr
|
608
|
+
end
|
609
|
+
|
610
|
+
def reset_edit_constants
|
611
|
+
# Each of the non-required edits will be represented by an
|
612
|
+
# equation of the form
|
613
|
+
#
|
614
|
+
# v = c + eplus - eminus
|
615
|
+
#
|
616
|
+
# where v is the variable with the edit, c is the previous edit
|
617
|
+
# value, and eplus and eminus are slack variables that hold the
|
618
|
+
# error in satisfying the edit constraint. We are about to
|
619
|
+
# change something, and we want to fix the constants in the
|
620
|
+
# equations representing the edit constraints. If one of eplus
|
621
|
+
# and eminus is basic, the other must occur only in the
|
622
|
+
# expression for that basic error variable. (They can't both be
|
623
|
+
# basic.) Fix the constant in this expression. Otherwise they
|
624
|
+
# are both nonbasic. Find all of the expressions in which they
|
625
|
+
# occur, and fix the constants in those. See the UIST paper for
|
626
|
+
# details.
|
627
|
+
|
628
|
+
raise InternalError if new_edit_constants.size != edit_plus_error_vars.size
|
629
|
+
new_edit_constants.each_with_index do |ec, idx|
|
630
|
+
delta = ec - prev_edit_constants[idx]
|
631
|
+
prev_edit_constants[idx] = ec
|
632
|
+
delta_edit_constant(delta, edit_plus_error_vars[idx], edit_minus_error_vars[idx])
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
def reset_stay_constants
|
637
|
+
# Each of the non-required stays will be represented by an
|
638
|
+
# equation of the form
|
639
|
+
#
|
640
|
+
# v = c + eplus - eminus
|
641
|
+
#
|
642
|
+
# where v is the variable with the stay, c is the previous value
|
643
|
+
# of v, and eplus and eminus are slack variables that hold the
|
644
|
+
# error in satisfying the stay constraint. We are about to
|
645
|
+
# change something, and we want to fix the constants in the
|
646
|
+
# equations representing the stays. If both eplus and eminus
|
647
|
+
# are nonbasic they have value 0 in the current solution,
|
648
|
+
# meaning the previous stay was exactly satisfied. In this case
|
649
|
+
# nothing needs to be changed. Otherwise one of them is basic,
|
650
|
+
# and the other must occur only in the expression for that basic
|
651
|
+
# error variable. Reset the constant in this expression to 0.
|
652
|
+
|
653
|
+
stay_plus_error_vars.each_with_index do |ev, idx|
|
654
|
+
expr = rows[ev] || rows[stay_minus_error_vars[idx]]
|
655
|
+
expr.constant = 0.0 if expr
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
def set_external_variables
|
660
|
+
# Set each external basic variable to its value, and set each
|
661
|
+
# external parametric variable to 0. (It isn't clear that we
|
662
|
+
# will ever have external parametric variables -- every external
|
663
|
+
# variable should either have a stay on it, or have an equation
|
664
|
+
# that defines it in terms of other external variables that do
|
665
|
+
# have stays. For the moment I'll put this in though.)
|
666
|
+
# Variables that are internal to the solver don't actually store
|
667
|
+
# values -- their values are just implicit in the tableu -- so
|
668
|
+
# we don't need to set them.
|
669
|
+
rows.each_pair do |var, expr|
|
670
|
+
var.value = expr.constant if var.external?
|
671
|
+
end
|
672
|
+
|
673
|
+
columns.keys.each do |var|
|
674
|
+
var.value = 0.0 if var.external?
|
675
|
+
end
|
676
|
+
end
|
677
|
+
|
678
|
+
def substitute_out(old_var, expr)
|
679
|
+
col = columns.delete(old_var)
|
680
|
+
col.each do |v|
|
681
|
+
row = rows[v]
|
682
|
+
row.substitute_variable(old_var, expr, v, self)
|
683
|
+
if v.restricted? and row.constant < 0.0
|
684
|
+
infeasible_rows << v
|
685
|
+
end
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
def try_adding_directly(expr)
|
690
|
+
# If possible choose a subject for expr (a variable to become
|
691
|
+
# basic) from among the current variables in expr. If this
|
692
|
+
# isn't possible, add an artificial variable and use that
|
693
|
+
# variable as the subject.
|
694
|
+
subject = choose_subject(expr)
|
695
|
+
return false if subject.nil?
|
696
|
+
expr.new_subject subject
|
697
|
+
if columns.has_key? subject
|
698
|
+
substitute_out subject, expr
|
699
|
+
end
|
700
|
+
add_row subject, expr
|
701
|
+
true
|
702
|
+
end
|
703
|
+
end
|
704
|
+
end
|