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.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/CITATION.cff +17 -0
  3. data/DESIGN.md +783 -0
  4. data/LICENSE +21 -0
  5. data/MANUAL.md +6265 -0
  6. data/README.md +267 -0
  7. data/bin/rcas +9 -0
  8. data/bin/rcas-app +9 -0
  9. data/bin/rcas-chat +9 -0
  10. data/lib/rcas/algebraic.rb +481 -0
  11. data/lib/rcas/analysis.rb +966 -0
  12. data/lib/rcas/app/launcher.rb +203 -0
  13. data/lib/rcas/app/public/app.css +402 -0
  14. data/lib/rcas/app/public/app.js +449 -0
  15. data/lib/rcas/app/public/index.html +46 -0
  16. data/lib/rcas/app/server.rb +220 -0
  17. data/lib/rcas/app/window.rb +94 -0
  18. data/lib/rcas/app/worksheet.rb +290 -0
  19. data/lib/rcas/app.rb +168 -0
  20. data/lib/rcas/background.rb +758 -0
  21. data/lib/rcas/chat/assistant.rb +199 -0
  22. data/lib/rcas/chat/picker.rb +164 -0
  23. data/lib/rcas/chat/repl.rb +583 -0
  24. data/lib/rcas/chat/session.rb +137 -0
  25. data/lib/rcas/chat/settings.rb +71 -0
  26. data/lib/rcas/chat/style.rb +30 -0
  27. data/lib/rcas/chat/tool.rb +53 -0
  28. data/lib/rcas/chat/ui.rb +316 -0
  29. data/lib/rcas/chat/usage.rb +62 -0
  30. data/lib/rcas/chat/workspace.rb +132 -0
  31. data/lib/rcas/chat.rb +54 -0
  32. data/lib/rcas/coefficients.rb +170 -0
  33. data/lib/rcas/combinatorics.rb +274 -0
  34. data/lib/rcas/complex_parts.rb +160 -0
  35. data/lib/rcas/constants.rb +129 -0
  36. data/lib/rcas/core_ext.rb +35 -0
  37. data/lib/rcas/decide.rb +501 -0
  38. data/lib/rcas/decompositions.rb +241 -0
  39. data/lib/rcas/differentiate.rb +144 -0
  40. data/lib/rcas/discussion.rb +558 -0
  41. data/lib/rcas/distributions.rb +980 -0
  42. data/lib/rcas/dixon.rb +95 -0
  43. data/lib/rcas/docs.rb +321 -0
  44. data/lib/rcas/domains.rb +728 -0
  45. data/lib/rcas/expand.rb +174 -0
  46. data/lib/rcas/expression.rb +613 -0
  47. data/lib/rcas/factor.rb +605 -0
  48. data/lib/rcas/finite_field.rb +577 -0
  49. data/lib/rcas/fourier.rb +118 -0
  50. data/lib/rcas/fps.rb +678 -0
  51. data/lib/rcas/fraction.rb +126 -0
  52. data/lib/rcas/functions.rb +1136 -0
  53. data/lib/rcas/gcd.rb +112 -0
  54. data/lib/rcas/geometry.rb +266 -0
  55. data/lib/rcas/groebner.rb +162 -0
  56. data/lib/rcas/hold.rb +277 -0
  57. data/lib/rcas/hypothesis.rb +364 -0
  58. data/lib/rcas/inequalities.rb +689 -0
  59. data/lib/rcas/integral_functions.rb +260 -0
  60. data/lib/rcas/integrate.rb +1589 -0
  61. data/lib/rcas/integrate_substitutions.rb +434 -0
  62. data/lib/rcas/interpolate.rb +40 -0
  63. data/lib/rcas/irb.rb +146 -0
  64. data/lib/rcas/laplace.rb +159 -0
  65. data/lib/rcas/latex.rb +556 -0
  66. data/lib/rcas/lattice.rb +172 -0
  67. data/lib/rcas/linear_algebra.rb +117 -0
  68. data/lib/rcas/linear_program.rb +416 -0
  69. data/lib/rcas/lint.rb +79 -0
  70. data/lib/rcas/matrix.rb +531 -0
  71. data/lib/rcas/matrix_multiply.rb +202 -0
  72. data/lib/rcas/multimodular.rb +286 -0
  73. data/lib/rcas/named_polynomials.rb +274 -0
  74. data/lib/rcas/number_theory.rb +443 -0
  75. data/lib/rcas/numerics.rb +825 -0
  76. data/lib/rcas/ode.rb +488 -0
  77. data/lib/rcas/openmath/objects.rb +364 -0
  78. data/lib/rcas/openmath/phrasebook.rb +551 -0
  79. data/lib/rcas/openmath/popcorn.rb +518 -0
  80. data/lib/rcas/openmath/xml.rb +309 -0
  81. data/lib/rcas/openmath.rb +49 -0
  82. data/lib/rcas/petkovsek.rb +165 -0
  83. data/lib/rcas/piecewise.rb +488 -0
  84. data/lib/rcas/plot.rb +763 -0
  85. data/lib/rcas/plot3d.rb +419 -0
  86. data/lib/rcas/poly_matrix.rb +318 -0
  87. data/lib/rcas/poly_recurrence.rb +117 -0
  88. data/lib/rcas/polynomial.rb +466 -0
  89. data/lib/rcas/precision.rb +925 -0
  90. data/lib/rcas/printer.rb +150 -0
  91. data/lib/rcas/product.rb +155 -0
  92. data/lib/rcas/q_difference.rb +296 -0
  93. data/lib/rcas/q_functions.rb +158 -0
  94. data/lib/rcas/q_summation.rb +308 -0
  95. data/lib/rcas/q_zeilberger.rb +199 -0
  96. data/lib/rcas/random.rb +506 -0
  97. data/lib/rcas/rational_function.rb +186 -0
  98. data/lib/rcas/recurrence.rb +323 -0
  99. data/lib/rcas/render.rb +431 -0
  100. data/lib/rcas/results.rb +192 -0
  101. data/lib/rcas/scalar.rb +219 -0
  102. data/lib/rcas/series.rb +726 -0
  103. data/lib/rcas/simplify.rb +649 -0
  104. data/lib/rcas/solve.rb +2002 -0
  105. data/lib/rcas/special.rb +163 -0
  106. data/lib/rcas/statistics.rb +175 -0
  107. data/lib/rcas/steps.rb +835 -0
  108. data/lib/rcas/summation.rb +532 -0
  109. data/lib/rcas/trig.rb +264 -0
  110. data/lib/rcas/van_hoeij.rb +241 -0
  111. data/lib/rcas/vector.rb +175 -0
  112. data/lib/rcas/vector_calculus.rb +411 -0
  113. data/lib/rcas/version.rb +5 -0
  114. data/lib/rcas/zeilberger.rb +358 -0
  115. data/lib/rcas.rb +91 -0
  116. data/package.json +8 -0
  117. 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