rcas 0.2.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 +7 -0
- data/CITATION.cff +17 -0
- data/DESIGN.md +783 -0
- data/LICENSE +21 -0
- data/MANUAL.md +6265 -0
- data/README.md +267 -0
- data/bin/rcas +9 -0
- data/bin/rcas-app +9 -0
- data/bin/rcas-chat +9 -0
- data/lib/rcas/algebraic.rb +481 -0
- data/lib/rcas/analysis.rb +966 -0
- data/lib/rcas/app/launcher.rb +203 -0
- data/lib/rcas/app/public/app.css +402 -0
- data/lib/rcas/app/public/app.js +449 -0
- data/lib/rcas/app/public/index.html +46 -0
- data/lib/rcas/app/server.rb +220 -0
- data/lib/rcas/app/window.rb +94 -0
- data/lib/rcas/app/worksheet.rb +290 -0
- data/lib/rcas/app.rb +168 -0
- data/lib/rcas/background.rb +758 -0
- data/lib/rcas/chat/assistant.rb +199 -0
- data/lib/rcas/chat/picker.rb +164 -0
- data/lib/rcas/chat/repl.rb +583 -0
- data/lib/rcas/chat/session.rb +137 -0
- data/lib/rcas/chat/settings.rb +71 -0
- data/lib/rcas/chat/style.rb +30 -0
- data/lib/rcas/chat/tool.rb +53 -0
- data/lib/rcas/chat/ui.rb +316 -0
- data/lib/rcas/chat/usage.rb +62 -0
- data/lib/rcas/chat/workspace.rb +132 -0
- data/lib/rcas/chat.rb +54 -0
- data/lib/rcas/coefficients.rb +170 -0
- data/lib/rcas/combinatorics.rb +274 -0
- data/lib/rcas/complex_parts.rb +160 -0
- data/lib/rcas/constants.rb +129 -0
- data/lib/rcas/core_ext.rb +35 -0
- data/lib/rcas/decide.rb +501 -0
- data/lib/rcas/decompositions.rb +241 -0
- data/lib/rcas/differentiate.rb +144 -0
- data/lib/rcas/discussion.rb +558 -0
- data/lib/rcas/distributions.rb +980 -0
- data/lib/rcas/dixon.rb +95 -0
- data/lib/rcas/docs.rb +321 -0
- data/lib/rcas/domains.rb +728 -0
- data/lib/rcas/expand.rb +174 -0
- data/lib/rcas/expression.rb +613 -0
- data/lib/rcas/factor.rb +605 -0
- data/lib/rcas/finite_field.rb +577 -0
- data/lib/rcas/fourier.rb +118 -0
- data/lib/rcas/fps.rb +678 -0
- data/lib/rcas/fraction.rb +126 -0
- data/lib/rcas/functions.rb +1136 -0
- data/lib/rcas/gcd.rb +112 -0
- data/lib/rcas/geometry.rb +266 -0
- data/lib/rcas/groebner.rb +162 -0
- data/lib/rcas/hold.rb +277 -0
- data/lib/rcas/hypothesis.rb +364 -0
- data/lib/rcas/inequalities.rb +689 -0
- data/lib/rcas/integral_functions.rb +260 -0
- data/lib/rcas/integrate.rb +1589 -0
- data/lib/rcas/integrate_substitutions.rb +434 -0
- data/lib/rcas/interpolate.rb +40 -0
- data/lib/rcas/irb.rb +146 -0
- data/lib/rcas/laplace.rb +159 -0
- data/lib/rcas/latex.rb +556 -0
- data/lib/rcas/lattice.rb +172 -0
- data/lib/rcas/linear_algebra.rb +117 -0
- data/lib/rcas/linear_program.rb +416 -0
- data/lib/rcas/lint.rb +79 -0
- data/lib/rcas/matrix.rb +531 -0
- data/lib/rcas/matrix_multiply.rb +202 -0
- data/lib/rcas/multimodular.rb +286 -0
- data/lib/rcas/named_polynomials.rb +274 -0
- data/lib/rcas/number_theory.rb +443 -0
- data/lib/rcas/numerics.rb +825 -0
- data/lib/rcas/ode.rb +488 -0
- data/lib/rcas/openmath/objects.rb +364 -0
- data/lib/rcas/openmath/phrasebook.rb +551 -0
- data/lib/rcas/openmath/popcorn.rb +518 -0
- data/lib/rcas/openmath/xml.rb +309 -0
- data/lib/rcas/openmath.rb +49 -0
- data/lib/rcas/petkovsek.rb +165 -0
- data/lib/rcas/piecewise.rb +488 -0
- data/lib/rcas/plot.rb +763 -0
- data/lib/rcas/plot3d.rb +419 -0
- data/lib/rcas/poly_matrix.rb +318 -0
- data/lib/rcas/poly_recurrence.rb +117 -0
- data/lib/rcas/polynomial.rb +466 -0
- data/lib/rcas/precision.rb +925 -0
- data/lib/rcas/printer.rb +150 -0
- data/lib/rcas/product.rb +155 -0
- data/lib/rcas/q_difference.rb +296 -0
- data/lib/rcas/q_functions.rb +158 -0
- data/lib/rcas/q_summation.rb +308 -0
- data/lib/rcas/q_zeilberger.rb +199 -0
- data/lib/rcas/random.rb +506 -0
- data/lib/rcas/rational_function.rb +186 -0
- data/lib/rcas/recurrence.rb +323 -0
- data/lib/rcas/render.rb +431 -0
- data/lib/rcas/results.rb +192 -0
- data/lib/rcas/scalar.rb +219 -0
- data/lib/rcas/series.rb +726 -0
- data/lib/rcas/simplify.rb +649 -0
- data/lib/rcas/solve.rb +2002 -0
- data/lib/rcas/special.rb +163 -0
- data/lib/rcas/statistics.rb +175 -0
- data/lib/rcas/steps.rb +835 -0
- data/lib/rcas/summation.rb +532 -0
- data/lib/rcas/trig.rb +264 -0
- data/lib/rcas/van_hoeij.rb +241 -0
- data/lib/rcas/vector.rb +175 -0
- data/lib/rcas/vector_calculus.rb +411 -0
- data/lib/rcas/version.rb +5 -0
- data/lib/rcas/zeilberger.rb +358 -0
- data/lib/rcas.rb +91 -0
- data/package.json +8 -0
- metadata +206 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Linear optimization: a linear objective, maximized or minimized over the
|
|
5
|
+
# points that satisfy linear inequalities and equations, exactly.
|
|
6
|
+
#
|
|
7
|
+
# maximize(3*x + 2*y, [x + y <= 4, x + 3*y <= 6], nonnegative: true)
|
|
8
|
+
# # => maximum 12 at x = 4, y = 0
|
|
9
|
+
# minimize(x + y, [x + 2*y >= 3, 3*x + y >= 4], nonnegative: true)
|
|
10
|
+
# maximize(5*x + 4*y, [6*x + 4*y <= 24, x + 2*y <= 6], nonnegative: true, integer: true)
|
|
11
|
+
#
|
|
12
|
+
# The simplex method in two phases: the first finds a vertex of the
|
|
13
|
+
# feasible region by minimizing the sum of artificial variables (a
|
|
14
|
+
# positive minimum means there is no feasible point at all), the second
|
|
15
|
+
# walks from vertex to vertex along edges that improve the objective until
|
|
16
|
+
# none does, or until an edge runs off to infinity. Every pivot is chosen
|
|
17
|
+
# by Bland's rule - the entering and the leaving variable are the ones of
|
|
18
|
+
# smallest index among the candidates - which is what guarantees the walk
|
|
19
|
+
# ends: with the textbook largest-coefficient rule a degenerate problem can
|
|
20
|
+
# cycle for ever. The arithmetic is exact, so "optimal", "infeasible" and
|
|
21
|
+
# "unbounded" are decisions, not tolerances.
|
|
22
|
+
#
|
|
23
|
+
# Variables are free unless the constraints bound them. A variable with a
|
|
24
|
+
# lower bound x >= c is written c + x' with x' >= 0; one without is the
|
|
25
|
+
# difference of two non-negative ones. `nonnegative: true` adds x >= 0 for
|
|
26
|
+
# every variable, the textbook convention (Maple's NONNEGATIVE).
|
|
27
|
+
#
|
|
28
|
+
# `integer:` (true, or a list of variables) asks for whole-number values
|
|
29
|
+
# and is answered by branch and bound: the linear program without the
|
|
30
|
+
# integrality is solved, and a variable that comes out fractional splits
|
|
31
|
+
# the problem into x <= floor and x >= ceil, each solved in turn; a branch
|
|
32
|
+
# whose relaxation is no better than the best whole-number point found so
|
|
33
|
+
# far is cut off.
|
|
34
|
+
#
|
|
35
|
+
# Sources (keys: MANUAL.md, Sources): the simplex method [Dan63]; the two
|
|
36
|
+
# phases, Bland's rule and cycling, and duality as in [Chv83, ch. 2-5];
|
|
37
|
+
# Bland's rule itself [Bla77]; branch and bound [LD60]; [Sch86] for the
|
|
38
|
+
# theory of both.
|
|
39
|
+
module LinearProgram
|
|
40
|
+
# Branch and bound gives up after this many linear programs: "no
|
|
41
|
+
# whole-number optimum" and "unlucky" look the same from in here, and a
|
|
42
|
+
# CAS that spins for ever is worse than one that says so.
|
|
43
|
+
MAX_NODES = 2000
|
|
44
|
+
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
def maximize(objective, constraints, vars = nil, nonnegative: false, integer: nil)
|
|
48
|
+
optimize(:max, objective, constraints, vars, nonnegative: nonnegative, integer: integer)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def minimize(objective, constraints, vars = nil, nonnegative: false, integer: nil)
|
|
52
|
+
optimize(:min, objective, constraints, vars, nonnegative: nonnegative, integer: integer)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def optimize(sense, objective, constraints, vars, nonnegative:, integer:)
|
|
56
|
+
objective = Expression.lift(objective)
|
|
57
|
+
constraints = Array(constraints).flatten
|
|
58
|
+
vars = variables(objective, constraints, vars)
|
|
59
|
+
floats = ([objective] + constraints.flat_map { |c| [c.lhs, c.rhs] }).any? { |e| float?(e) }
|
|
60
|
+
rows = constraints.map { |c| row(c, vars) }
|
|
61
|
+
rows += vars.map { |v| [unit(vars, v), :>=, 0r] } if nonnegative
|
|
62
|
+
cost, constant = linear(objective, vars, "the objective")
|
|
63
|
+
cost = cost.map(&:-@) if sense == :min
|
|
64
|
+
problem = Problem.new(vars, rows, cost)
|
|
65
|
+
whole = integer_variables(integer, vars)
|
|
66
|
+
found = whole.empty? ? problem.solve : branch_and_bound(problem, whole)
|
|
67
|
+
result(sense, found, objective, constant, vars, floats, integer: !whole.empty?)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# ---- reading the problem ------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def variables(objective, constraints, given)
|
|
73
|
+
constraints.each do |c|
|
|
74
|
+
next if c.is_a?(Equation) || (c.is_a?(Inequality) && !c.strict?)
|
|
75
|
+
if c.is_a?(Inequality)
|
|
76
|
+
raise ArgumentError, "#{c}: a strict inequality has no optimum on its boundary, where a linear optimum lies; write it with <= or >=" unless c.op == :!=
|
|
77
|
+
raise ArgumentError, "#{c}: != cuts the feasible region in two, which is no longer a linear program"
|
|
78
|
+
end
|
|
79
|
+
raise ArgumentError, "a constraint must be an inequality (x + y <= 4) or an equation (eq(x + y, 3)), got #{c.inspect}"
|
|
80
|
+
end
|
|
81
|
+
names = given ? Array(given).map { |v| Expression.lift(v) } : nil
|
|
82
|
+
names ||= (objective.variables | constraints.flat_map { |c| c.lhs.variables | c.rhs.variables }).sort.map { |n| Var.new(n) }
|
|
83
|
+
raise ArgumentError, "the variables must be symbols, got #{names.inspect}" unless names.all?(Var)
|
|
84
|
+
names
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def integer_variables(integer, vars)
|
|
88
|
+
return [] if integer.nil? || integer == false
|
|
89
|
+
return (0...vars.size).to_a if integer == true
|
|
90
|
+
Array(integer).map do |v|
|
|
91
|
+
i = vars.index(Expression.lift(v))
|
|
92
|
+
raise ArgumentError, "integer: #{v} is not one of the variables #{vars.join(', ')}" if i.nil?
|
|
93
|
+
i
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# [coefficients, :<= | :>= | :==, right-hand side]
|
|
98
|
+
def row(constraint, vars)
|
|
99
|
+
coeffs, constant = linear(constraint.lhs - constraint.rhs, vars, constraint.to_s)
|
|
100
|
+
op = constraint.is_a?(Equation) ? :== : { :<= => :<=, :>= => :>= }.fetch(constraint.op)
|
|
101
|
+
[coeffs, op, -constant]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def unit(vars, v) = vars.map { |w| w == v ? 1r : 0r }
|
|
105
|
+
|
|
106
|
+
# The coefficient of each variable and the constant term of a linear
|
|
107
|
+
# expression, as Rationals.
|
|
108
|
+
def linear(expr, vars, what)
|
|
109
|
+
constant, table = Expand.table(Expression.lift(expr))
|
|
110
|
+
coeffs = Array.new(vars.size, 0r)
|
|
111
|
+
table.each do |factors, coeff|
|
|
112
|
+
var = factors.size == 1 && factors.first[1] == 1 ? vars.index(factors.first[0]) : nil
|
|
113
|
+
unless var
|
|
114
|
+
term = Simplify.rebuild_product(coeff, factors)
|
|
115
|
+
raise ArgumentError, "#{what} is not linear in #{vars.join(', ')}: #{term}" if term.variables.intersect?(vars.map(&:name))
|
|
116
|
+
raise ArgumentError, "#{what} has a parameter, #{term.variables.join(', ')}; the coefficients must be numbers"
|
|
117
|
+
end
|
|
118
|
+
coeffs[var] += number(coeff, what)
|
|
119
|
+
end
|
|
120
|
+
[coeffs, number(constant, what)]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# A Float is taken as the decimal it was written as (0.1 as 1/10), and
|
|
124
|
+
# the answer is then reported in Floats again.
|
|
125
|
+
def number(value, what)
|
|
126
|
+
value = value.value if value.is_a?(Num)
|
|
127
|
+
return value.to_r if value.is_a?(Integer) || value.is_a?(Rational)
|
|
128
|
+
return Rational(value.to_s) if value.is_a?(Float) && value.finite?
|
|
129
|
+
raise ArgumentError, "#{what}: the coefficients must be rational numbers, got #{value}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def float?(e) = Expression.lift(e).each_node.any? { |n| n.is_a?(Num) && n.value.is_a?(Float) }
|
|
133
|
+
|
|
134
|
+
# ---- the problem in standard form ---------------------------------------------
|
|
135
|
+
|
|
136
|
+
# max cost*x subject to rows, x free unless a row bounds it from below.
|
|
137
|
+
# Solved by `solve`, which answers [:optimal, x, unique] | [:infeasible]
|
|
138
|
+
# | [:unbounded].
|
|
139
|
+
class Problem
|
|
140
|
+
attr_reader :vars, :rows, :cost
|
|
141
|
+
|
|
142
|
+
def initialize(vars, rows, cost)
|
|
143
|
+
@vars = vars
|
|
144
|
+
@rows = rows
|
|
145
|
+
@cost = cost
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def with(extra_rows) = Problem.new(vars, rows + extra_rows, cost)
|
|
149
|
+
|
|
150
|
+
def solve
|
|
151
|
+
shift, split = substitution
|
|
152
|
+
tableau, basis, columns = standard_form(shift, split)
|
|
153
|
+
artificial = columns[:artificial]
|
|
154
|
+
# phase 1: a feasible vertex, or the proof that there is none
|
|
155
|
+
phase_one = Array.new(columns[:count], 0r)
|
|
156
|
+
artificial.each { |j| phase_one[j] = -1r }
|
|
157
|
+
LinearProgram.simplex(tableau, basis, phase_one, (0...columns[:count]).to_a)
|
|
158
|
+
return [:infeasible] unless basis.each_with_index.all? { |j, i| !artificial.include?(j) || tableau[i][-1].zero? }
|
|
159
|
+
LinearProgram.drive_out(tableau, basis, artificial)
|
|
160
|
+
# phase 2: the objective, over the columns that are not artificial
|
|
161
|
+
allowed = (0...columns[:count]).reject { |j| artificial.include?(j) }
|
|
162
|
+
objective = Array.new(columns[:count], 0r)
|
|
163
|
+
columns[:structure].each { |j, var, sign| objective[j] += sign * cost[var] }
|
|
164
|
+
status, = LinearProgram.simplex(tableau, basis, objective, allowed)
|
|
165
|
+
return [:unbounded] if status == :unbounded
|
|
166
|
+
values = Array.new(columns[:count], 0r)
|
|
167
|
+
basis.each_with_index { |j, i| values[j] = tableau[i][-1] }
|
|
168
|
+
x = point(values, columns, shift)
|
|
169
|
+
[:optimal, x, LinearProgram.unique(tableau, basis, objective, allowed, columns)]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# x_i = shift_i + x'_i where the rows give a lower bound, and
|
|
173
|
+
# x_i = p_i - m_i where they give none (split).
|
|
174
|
+
def substitution
|
|
175
|
+
shift = Array.new(vars.size)
|
|
176
|
+
rows.each do |coeffs, op, rhs|
|
|
177
|
+
nonzero = coeffs.each_index.reject { |j| coeffs[j].zero? }
|
|
178
|
+
next unless nonzero.size == 1
|
|
179
|
+
j = nonzero.first
|
|
180
|
+
a = coeffs[j]
|
|
181
|
+
next unless (op == :>= && a.positive?) || (op == :<= && a.negative?) || op == :==
|
|
182
|
+
bound = rhs / a
|
|
183
|
+
shift[j] = shift[j] ? [shift[j], bound].max : bound
|
|
184
|
+
end
|
|
185
|
+
[shift, shift.each_index.select { |j| shift[j].nil? }]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The tableau [A | b] with b >= 0, a slack for every <=, a surplus
|
|
189
|
+
# and an artificial for every >=, an artificial for every equation;
|
|
190
|
+
# the starting basis is the slacks and the artificials.
|
|
191
|
+
def standard_form(shift, split)
|
|
192
|
+
structure = [] # [column, variable, sign]
|
|
193
|
+
vars.each_index do |i|
|
|
194
|
+
structure << [structure.size, i, 1r]
|
|
195
|
+
structure << [structure.size, i, -1r] if split.include?(i)
|
|
196
|
+
end
|
|
197
|
+
n = structure.size
|
|
198
|
+
m = rows.size
|
|
199
|
+
slack_count = rows.count { |_, op, _| op != :== }
|
|
200
|
+
# at most one artificial per row; which rows need one is known only
|
|
201
|
+
# once b is made non-negative, so the unused columns are cut at the end
|
|
202
|
+
count = n + slack_count + m
|
|
203
|
+
tableau = []
|
|
204
|
+
basis = []
|
|
205
|
+
artificial = []
|
|
206
|
+
slack = n
|
|
207
|
+
extra = n + slack_count
|
|
208
|
+
rows.each do |coeffs, op, rhs|
|
|
209
|
+
line = Array.new(count + 1, 0r)
|
|
210
|
+
structure.each { |j, var, sign| line[j] = sign * coeffs[var] }
|
|
211
|
+
b = rhs - coeffs.each_index.sum { |var| shift[var] ? coeffs[var] * shift[var] : 0r }
|
|
212
|
+
if b.negative?
|
|
213
|
+
line.map!(&:-@)
|
|
214
|
+
b = -b
|
|
215
|
+
op = { :<= => :>=, :>= => :<=, :== => :== }[op]
|
|
216
|
+
end
|
|
217
|
+
line[-1] = b
|
|
218
|
+
if op == :<=
|
|
219
|
+
line[slack] = 1r
|
|
220
|
+
basis << slack
|
|
221
|
+
slack += 1
|
|
222
|
+
else
|
|
223
|
+
if op == :>=
|
|
224
|
+
line[slack] = -1r
|
|
225
|
+
slack += 1
|
|
226
|
+
end
|
|
227
|
+
line[extra] = 1r
|
|
228
|
+
basis << extra
|
|
229
|
+
artificial << extra
|
|
230
|
+
extra += 1
|
|
231
|
+
end
|
|
232
|
+
tableau << line
|
|
233
|
+
end
|
|
234
|
+
count = extra
|
|
235
|
+
tableau.map! { |line| line.first(count) + [line[-1]] }
|
|
236
|
+
[tableau, basis, { structure: structure, artificial: artificial, count: count, split: split }]
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def point(values, columns, shift)
|
|
240
|
+
x = shift.map { |s| s || 0r }
|
|
241
|
+
columns[:structure].each { |j, var, sign| x[var] += sign * values[j] }
|
|
242
|
+
x
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Pivot until no allowed column improves the objective (maximized),
|
|
247
|
+
# choosing by Bland's rule. [:optimal] or [:unbounded, column].
|
|
248
|
+
def simplex(tableau, basis, cost, allowed)
|
|
249
|
+
loop do
|
|
250
|
+
reduced = reduced_costs(tableau, basis, cost)
|
|
251
|
+
entering = allowed.find { |j| !basis.include?(j) && reduced[j].positive? }
|
|
252
|
+
return [:optimal] unless entering
|
|
253
|
+
leaving = ratio_row(tableau, basis, entering)
|
|
254
|
+
return [:unbounded, entering] unless leaving
|
|
255
|
+
pivot(tableau, basis, leaving, entering)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# c_j - c_B . column j: how much the objective gains per unit of x_j
|
|
260
|
+
def reduced_costs(tableau, basis, cost)
|
|
261
|
+
(0...cost.size).map do |j|
|
|
262
|
+
cost[j] - basis.each_with_index.sum { |b, i| cost[b] * tableau[i][j] }
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# The row that limits the entering column first; a tie goes to the
|
|
267
|
+
# basic variable of smallest index (Bland). nil when nothing limits it.
|
|
268
|
+
def ratio_row(tableau, basis, entering)
|
|
269
|
+
candidates = tableau.each_index.select { |i| tableau[i][entering].positive? }
|
|
270
|
+
return nil if candidates.empty?
|
|
271
|
+
candidates.min_by { |i| [tableau[i][-1] / tableau[i][entering], basis[i]] }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def pivot(tableau, basis, row, column)
|
|
275
|
+
p = tableau[row][column]
|
|
276
|
+
tableau[row] = tableau[row].map { |v| v / p }
|
|
277
|
+
tableau.each_index do |i|
|
|
278
|
+
next if i == row || tableau[i][column].zero?
|
|
279
|
+
f = tableau[i][column]
|
|
280
|
+
tableau[i] = tableau[i].zip(tableau[row]).map { |a, b| a - f * b }
|
|
281
|
+
end
|
|
282
|
+
basis[row] = column
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# After phase 1 an artificial can still be basic, at level 0. It is
|
|
286
|
+
# pivoted out on any other column of its row; a row with no other entry
|
|
287
|
+
# says an equation was implied by the others, and it goes.
|
|
288
|
+
def drive_out(tableau, basis, artificial)
|
|
289
|
+
tableau.each_index.to_a.reverse_each do |i|
|
|
290
|
+
next unless artificial.include?(basis[i])
|
|
291
|
+
j = (0...tableau[i].size - 1).find { |c| !artificial.include?(c) && !tableau[i][c].zero? }
|
|
292
|
+
if j
|
|
293
|
+
pivot(tableau, basis, i, j)
|
|
294
|
+
else
|
|
295
|
+
tableau.delete_at(i)
|
|
296
|
+
basis.delete_at(i)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Whether the optimal point is the only one. A non-basic column with
|
|
302
|
+
# reduced cost 0 is an edge along which the objective stays put; the
|
|
303
|
+
# point is unique when every such edge leaves x unchanged (a split
|
|
304
|
+
# variable's partner does), and not when one moves x a positive
|
|
305
|
+
# distance. An edge that moves x but cannot be followed at all (a
|
|
306
|
+
# degenerate vertex) decides nothing: nil.
|
|
307
|
+
def unique(tableau, basis, cost, allowed, columns)
|
|
308
|
+
reduced = reduced_costs(tableau, basis, cost)
|
|
309
|
+
verdict = true
|
|
310
|
+
allowed.each do |j|
|
|
311
|
+
next if basis.include?(j) || !reduced[j].zero?
|
|
312
|
+
direction = Hash.new(0r)
|
|
313
|
+
columns[:structure].each do |col, var, sign|
|
|
314
|
+
change = col == j ? 1r : -(basis.index(col) ? tableau[basis.index(col)][j] : 0r)
|
|
315
|
+
direction[var] += sign * change
|
|
316
|
+
end
|
|
317
|
+
next if direction.values.all?(&:zero?)
|
|
318
|
+
row = ratio_row(tableau, basis, j)
|
|
319
|
+
return false if row.nil? || tableau[row][-1].positive?
|
|
320
|
+
verdict = nil
|
|
321
|
+
end
|
|
322
|
+
verdict
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# ---- whole numbers --------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
def branch_and_bound(problem, whole)
|
|
328
|
+
best = nil
|
|
329
|
+
stack = [[]]
|
|
330
|
+
nodes = 0
|
|
331
|
+
until stack.empty?
|
|
332
|
+
extra = stack.pop
|
|
333
|
+
nodes += 1
|
|
334
|
+
raise RCAS::Unsupported, "integer: no answer after #{MAX_NODES} linear programs; the branch and bound gives up" if nodes > MAX_NODES
|
|
335
|
+
status, x, = problem.with(extra).solve
|
|
336
|
+
next if status == :infeasible
|
|
337
|
+
if status == :unbounded
|
|
338
|
+
raise RCAS::Unsupported, "integer: the problem without integrality is unbounded, and rcas does not decide whether a whole-number point runs off with it"
|
|
339
|
+
end
|
|
340
|
+
value = problem.cost.each_index.sum { |i| problem.cost[i] * x[i] }
|
|
341
|
+
next if best && value <= best[1]
|
|
342
|
+
i = whole.find { |k| x[k].denominator != 1 }
|
|
343
|
+
if i.nil?
|
|
344
|
+
best = [x, value]
|
|
345
|
+
next
|
|
346
|
+
end
|
|
347
|
+
coeffs = LinearProgram.unit(problem.vars, problem.vars[i])
|
|
348
|
+
stack << (extra + [[coeffs, :>=, x[i].ceil.to_r]])
|
|
349
|
+
stack << (extra + [[coeffs, :<=, x[i].floor.to_r]])
|
|
350
|
+
end
|
|
351
|
+
best ? [:optimal, best[0], nil] : [:infeasible]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# ---- the answer -----------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
def result(sense, found, objective, constant, vars, floats, integer:)
|
|
357
|
+
status, x, unique = found
|
|
358
|
+
return Result.new(sense, status, nil, nil, objective, nil) if status == :infeasible
|
|
359
|
+
return Result.new(sense, status, sense == :max ? OO : Neg.new(OO).simplify, nil, objective, nil) if status == :unbounded
|
|
360
|
+
value = objective.subs(vars.zip(x).to_h { |v, e| [v, Num.new(e)] }).simplify
|
|
361
|
+
value = Num.new(Simplify.normalize_number(number(value, "the objective") )) if value.is_a?(Num)
|
|
362
|
+
point = Assignment[vars.zip(x).to_h { |v, e| [v, Num.new(floats ? e.to_f : Simplify.normalize_number(e))] }]
|
|
363
|
+
value = Num.new(value.value.to_f) if floats && value.is_a?(Num)
|
|
364
|
+
Result.new(sense, :optimal, value, point, objective, integer ? nil : unique)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# What maximize and minimize answer: the status, the optimal value and
|
|
368
|
+
# the point where it is taken. `value, point = maximize(...)` works,
|
|
369
|
+
# because the result spreads like an array.
|
|
370
|
+
class Result
|
|
371
|
+
attr_reader :sense, :status, :value, :point, :objective, :unique
|
|
372
|
+
|
|
373
|
+
def initialize(sense, status, value, point, objective, unique)
|
|
374
|
+
@sense = sense
|
|
375
|
+
@status = status
|
|
376
|
+
@value = value
|
|
377
|
+
@point = point
|
|
378
|
+
@objective = objective
|
|
379
|
+
@unique = unique
|
|
380
|
+
freeze
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def optimal? = status == :optimal
|
|
384
|
+
def infeasible? = status == :infeasible
|
|
385
|
+
def unbounded? = status == :unbounded
|
|
386
|
+
def unique? = unique
|
|
387
|
+
def to_a = [value, point]
|
|
388
|
+
def to_ary = to_a
|
|
389
|
+
def ==(other) = other.is_a?(Result) && [sense, status, value, point] == [other.sense, other.status, other.value, other.point]
|
|
390
|
+
|
|
391
|
+
def to_s
|
|
392
|
+
word = sense == :max ? "maximum" : "minimum"
|
|
393
|
+
case status
|
|
394
|
+
when :infeasible then "infeasible: no point satisfies the constraints"
|
|
395
|
+
when :unbounded then "unbounded: #{objective} has no #{word} on the feasible region"
|
|
396
|
+
else
|
|
397
|
+
at = point.empty? ? "" : " at #{point.map { |v, e| "#{v} = #{e}" }.join(', ')}"
|
|
398
|
+
"#{word} #{value}#{at}#{unique == false ? ' (one of infinitely many optimal points)' : ''}"
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def inspect = to_s
|
|
403
|
+
|
|
404
|
+
def to_latex(wrap: nil)
|
|
405
|
+
word = sense == :max ? "\\max" : "\\min"
|
|
406
|
+
case status
|
|
407
|
+
when :infeasible then "\\text{infeasible}"
|
|
408
|
+
when :unbounded then "#{word} = #{sense == :max ? '' : '-'}\\infty"
|
|
409
|
+
else
|
|
410
|
+
at = point.map { |v, e| "#{LaTeX.of(v)} = #{LaTeX.of(e)}" }.join(",\\ ")
|
|
411
|
+
"#{word} = #{LaTeX.of(value)}#{at.empty? ? '' : "\\ \\text{at}\\ #{at}"}"
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
data/lib/rcas/lint.rb
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Warnings about input that Ruby evaluates before rcas sees it. The one
|
|
5
|
+
# that matters is integer division: `x + 1/3` is `x + 0`, because Ruby
|
|
6
|
+
# folds `1/3` to 0 first, and `2**(1/2)` is `2**0`. Nothing downstream
|
|
7
|
+
# can tell, so the front ends read the line's syntax tree before running
|
|
8
|
+
# it and say so at the moment it happens.
|
|
9
|
+
#
|
|
10
|
+
# Lint.warnings("x + 1/3") # => ["1/3 is 0: Ruby divides two Integers as integers, rounding down; write 1/3r for the fraction"]
|
|
11
|
+
#
|
|
12
|
+
# A division that comes out whole (`6/3`) is left alone, and so is the
|
|
13
|
+
# inside of `hold { }` and `steps { }`, which keep the division as typed.
|
|
14
|
+
# `RCAS.lint = false` (or `RCAS_LINT=0`) turns the warnings off.
|
|
15
|
+
module Lint
|
|
16
|
+
KEEPS_INPUT = %i[hold steps].freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def enabled? = @enabled.nil? ? ENV["RCAS_LINT"] != "0" : @enabled
|
|
21
|
+
|
|
22
|
+
def enabled=(value)
|
|
23
|
+
@enabled = value.nil? ? nil : !!value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The messages for one line of input, [] when there is nothing to say
|
|
27
|
+
# (or the line does not parse: Ruby reports that itself).
|
|
28
|
+
def warnings(source)
|
|
29
|
+
return [] unless enabled?
|
|
30
|
+
tree = Hold.parse(source)
|
|
31
|
+
found = []
|
|
32
|
+
walk(tree, false, found)
|
|
33
|
+
found.uniq
|
|
34
|
+
rescue SyntaxError, ArgumentError
|
|
35
|
+
[]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# +exponent+: the node is (a parenthesized part of) the right operand
|
|
39
|
+
# of `**`, as the 1/2 in `2**(1/2)` is.
|
|
40
|
+
def walk(node, exponent, found)
|
|
41
|
+
return unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
|
42
|
+
return if node.type == :ITER && KEEPS_INPUT.include?(called(node.children[0]))
|
|
43
|
+
message = integer_division(node, exponent) and found << message
|
|
44
|
+
power = node.type == :OPCALL && node.children[1] == :**
|
|
45
|
+
node.children.each_with_index do |child, i|
|
|
46
|
+
walk(child, power ? i == 2 : exponent && %i[LIST BLOCK].include?(node.type), found)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def integer_division(node, exponent)
|
|
51
|
+
return nil unless node.type == :OPCALL && node.children[1] == :/
|
|
52
|
+
a = integer(node.children[0])
|
|
53
|
+
b = integer(node.children[2]&.children&.first)
|
|
54
|
+
return nil if a.nil? || b.nil? || b.zero? || (a % b).zero?
|
|
55
|
+
text = "#{a}/#{b} is #{a / b}: Ruby divides two Integers as integers, rounding down"
|
|
56
|
+
text += " (so the exponent is #{a / b})" if exponent
|
|
57
|
+
"#{text}; write #{a}/#{b}r for the fraction"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Ruby 3.3 writes an integer literal as LIT, 3.4 and later as INTEGER.
|
|
61
|
+
def integer(node)
|
|
62
|
+
return nil unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
|
63
|
+
return nil unless %i[LIT INTEGER].include?(node.type)
|
|
64
|
+
value = node.children[0]
|
|
65
|
+
value if value.is_a?(Integer)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def called(call)
|
|
69
|
+
return nil unless call.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
|
70
|
+
call.children.find { |c| c.is_a?(Symbol) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.lint? = Lint.enabled?
|
|
75
|
+
|
|
76
|
+
def self.lint=(value)
|
|
77
|
+
Lint.enabled = value
|
|
78
|
+
end
|
|
79
|
+
end
|