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
data/lib/rcas/steps.rb ADDED
@@ -0,0 +1,835 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # One line of a worked solution: how deep it sits, what it says, and the
5
+ # expression it is about (for typesetting).
6
+ Step = Struct.new(:depth, :text, :expr)
7
+
8
+ # A worked solution: the problem as it was asked, the lines of working,
9
+ # and the answer. It prints as a student would write it out.
10
+ #
11
+ # steps { diff(x**2*sin(x), x) }
12
+ # steps { integrate(x*exp(x), x) }
13
+ # steps(x**2 - 5*x + 6, :solve)
14
+ class Derivation
15
+ attr_reader :problem, :lines, :result
16
+
17
+ def initialize(problem, lines, result)
18
+ @problem = problem
19
+ @lines = lines.freeze
20
+ @result = result
21
+ freeze
22
+ end
23
+
24
+ def to_s
25
+ body = lines.flat_map do |step|
26
+ indent = " " * (step.depth + 1)
27
+ step.text.to_s.lines.map { |l| "#{indent}#{l.chomp}" }
28
+ end
29
+ ([problem.to_s] + body + ["= #{result}"]).join("\n")
30
+ end
31
+ alias inspect to_s
32
+
33
+ def to_latex(wrap: nil)
34
+ rows = lines.map do |step|
35
+ indent = "\\quad " * step.depth
36
+ text = "#{indent}\\text{#{LaTeX.escape(step.text)}}"
37
+ step.expr ? "#{text} \\quad #{LaTeX.of(step.expr)}" : text
38
+ end
39
+ rows = [LaTeX.of(problem)] + rows + ["= #{LaTeX.of(result)}"]
40
+ "\\begin{aligned} #{rows.join(' \\\\ ')} \\end{aligned}"
41
+ end
42
+
43
+ # How many lines of working there are.
44
+ def size = lines.size
45
+ end
46
+
47
+ # Worked solutions: the working, not only the answer.
48
+ #
49
+ # steps { diff(x**2*sin(x), x) } the rules, named, as they are used
50
+ # steps { integrate(x*exp(x), x) } substitution and parts spelled out
51
+ # steps(x**2 - 5*x + 6, :solve) the quadratic formula with its numbers
52
+ # steps(x**3 - 5*x + 6, :factor) common factors, then the roots
53
+ # steps(1/(x**2 - 1), :apart) the partial-fraction ansatz
54
+ # steps(m, :rref) the row operations, one at a time
55
+ # steps(1071, 462, :gcd) Euclid's algorithm
56
+ # steps(f, x, :discuss) a whole curve discussion, question by question
57
+ #
58
+ # Each narrator decides which rule applies and then asks the library for
59
+ # the piece it names, so the working can never disagree with the answer
60
+ # `diff`, `integrate` or `solve` would give on its own. Where no rule is
61
+ # recognised, the line says so instead of inventing a derivation.
62
+ #
63
+ # Sources (keys: MANUAL.md, Sources): the rules are the textbook ones
64
+ # [Spi08, ch. 10, 18, 19]; Euclid's algorithm [Knu98, §4.5.2].
65
+ module Steps
66
+ module_function
67
+
68
+ # steps(target, ..., how) or steps { ... }
69
+ def of(*args, &block)
70
+ return of(Hold.hold(block)) if block
71
+ how = args.last.is_a?(Symbol) ? args.pop : nil
72
+ target = args.first
73
+ raise ArgumentError, "steps: nothing to work through" if target.nil?
74
+ how ||= default_for(target, args)
75
+ case how
76
+ when :diff then derivative(target)
77
+ when :integrate then antiderivative(target)
78
+ when :solve then solution(target, args[1])
79
+ when :apart then fractions(target, args[1])
80
+ when :rref then elimination(target)
81
+ when :gcd then euclid(target, args[1])
82
+ when :factor then factorization(target, args[1])
83
+ when :discuss then discussion(target, args[1])
84
+ else raise ArgumentError, "steps: don't know how to work through #{how || target.class}"
85
+ end
86
+ end
87
+
88
+ def default_for(target, rest = [])
89
+ case target
90
+ when Derivative then :diff
91
+ when Integral then :integrate
92
+ when Equation then :solve
93
+ when Matrix then :rref
94
+ when Fn then target.name == :discuss ? :discuss : :solve
95
+ when Expression then :solve
96
+ when Integer then rest.size > 1 ? :gcd : :factor
97
+ end
98
+ end
99
+
100
+ # ---- building blocks -----------------------------------------------------
101
+
102
+ def line(out, depth, text, expr = nil)
103
+ out << Step.new(depth, text, expr)
104
+ out
105
+ end
106
+
107
+ def constant?(expr, var) = !expr.variables.include?(var.name)
108
+
109
+ # ---- curve discussion ----------------------------------------------------
110
+
111
+ # The ritual, question by question. Every answer comes from the report
112
+ # discuss builds, so the working and the summary cannot disagree; what
113
+ # rcas cannot decide is said out loud rather than left out.
114
+ def discussion(target, var)
115
+ f = Expression.lift(target)
116
+ if f.is_a?(Fn) && f.name == :discuss
117
+ # steps { discuss(f, x) }: hold kept the call, so unpack it.
118
+ var ||= f.args[1]
119
+ f = f.args.first
120
+ end
121
+ x = Expression.lift(var || Solve.variable(f, nil))
122
+ report = Discussion.discuss(f, x)
123
+ out = []
124
+ @section = 0
125
+ discussion_domain(out, report, f, x)
126
+ discussion_symmetry(out, report, f, x)
127
+ discussion_zeros(out, report, x)
128
+ discussion_gaps(out, report, x)
129
+ discussion_infinity(out, report, x)
130
+ discussion_derivatives(out, report)
131
+ discussion_extrema(out, report, x)
132
+ discussion_chart(out, report, report.monotonicity, "f'", "the monotonicity: the sign of f' between its zeros")
133
+ discussion_inflections(out, report, x)
134
+ discussion_chart(out, report, report.curvature, "f''", "the curvature: the sign of f''")
135
+ Derivation.new(Fn.new(:discuss, [f, x]), out, report)
136
+ end
137
+
138
+ # "1. the domain: ...", with the working under it.
139
+ def section(out, text) = line(out, 0, "#{@section += 1}. #{text}")
140
+
141
+ def discussion_domain(out, report, f, x)
142
+ conditions = Analysis.domain_conditions(f, x)
143
+ if conditions.empty?
144
+ section(out, "the domain: nothing to exclude, so every real #{x}")
145
+ else
146
+ section(out, "the domain: what must not happen")
147
+ conditions.each { |condition| line(out, 1, condition.to_s) }
148
+ end
149
+ line(out, 1, "D = #{report.domain || 'not determined'}")
150
+ end
151
+
152
+ def discussion_symmetry(out, report, f, x)
153
+ section(out, "symmetry: put -#{x} in for #{x}")
154
+ line(out, 1, "f(-#{x}) = #{Discussion.reflect(f, x)}")
155
+ line(out, 1, case report.symmetry
156
+ when :even then "that is f(#{x}): the graph is symmetric about the vertical axis"
157
+ when :odd then "that is -f(#{x}): the graph is symmetric about the origin"
158
+ else "neither f(#{x}) nor -f(#{x}): no symmetry of either kind"
159
+ end)
160
+ line(out, 1, "f(#{x} + #{report.period}) = f(#{x}): the period is #{report.period}") if report.period
161
+ end
162
+
163
+ def discussion_zeros(out, report, x)
164
+ section(out, "the zeros: solve f(#{x}) = 0")
165
+ line(out, 1, if report.zeros.nil? then "rcas cannot solve f(#{x}) = 0"
166
+ elsif report.zeros.empty? then "no real zero"
167
+ else "#{x} = #{report.zeros.join(', ')}"
168
+ end)
169
+ line(out, 1, "and every #{report.period} on from each of them") if report.period && report.zeros&.any?
170
+ line(out, 1, "f(0) = #{report.intercept}, where the graph crosses the vertical axis") if report.intercept
171
+ end
172
+
173
+ def discussion_gaps(out, report, x)
174
+ return if report.gaps.empty?
175
+ section(out, "the gaps: where a denominator vanishes")
176
+ report.gaps.each do |point, kind, left, right|
177
+ line(out, 1, "#{x} = #{point}: f -> #{left || '?'} from the left, f -> #{right || '?'} from the right")
178
+ line(out, 1, case kind
179
+ when :pole then "the graph runs away there: a pole"
180
+ when :removable then "the same value from both sides: the gap can be filled"
181
+ else "rcas cannot tell what happens there"
182
+ end)
183
+ end
184
+ end
185
+
186
+ def discussion_infinity(out, report, x)
187
+ section(out, "at infinity: the limits, and the lines the graph approaches")
188
+ report.limits.each do |point, value|
189
+ line(out, 1, if value then "limit(f, #{x}, #{point}) = #{value}"
190
+ elsif report.period then "f repeats every #{report.period}, so it has no limit as #{x} -> #{point}"
191
+ else "limit(f, #{x}, #{point}): rcas cannot decide it"
192
+ end)
193
+ end
194
+ lines = report.asymptotes
195
+ (lines[:vertical] || []).each { |p| line(out, 1, "the pole at #{p} makes #{x} = #{p} a vertical asymptote") }
196
+ (lines[:horizontal] || []).each { |c| line(out, 1, "the limit #{c} makes y = #{c} a horizontal asymptote") }
197
+ (lines[:oblique] || []).each { |l| line(out, 1, "f - (#{l}) -> 0, so y = #{l} is an oblique asymptote") }
198
+ lines.each { |kind, found| line(out, 1, "rcas cannot decide the #{kind} asymptotes") if found.nil? }
199
+ end
200
+
201
+ def discussion_derivatives(out, report)
202
+ section(out, "the derivatives")
203
+ line(out, 1, "rcas cannot differentiate this one") if report.derivatives.first.nil?
204
+ ["f'", "f''", "f'''"].zip(report.derivatives).each do |name, value|
205
+ line(out, 1, "#{name}(#{report.var}) = #{value}", value) if value
206
+ end
207
+ end
208
+
209
+ def discussion_extrema(out, report, x)
210
+ section(out, "the extrema: solve f'(#{x}) = 0, then the second derivative decides")
211
+ if report.extrema.nil?
212
+ line(out, 1, "rcas cannot solve f'(#{x}) = 0, so the extrema stay open")
213
+ return
214
+ end
215
+ points = Discussion.solutions(report.derivatives[0], x)
216
+ line(out, 1, "f'(#{x}) = 0 at #{x} = #{Analysis.sort_points(points).join(', ')}") if points&.any?
217
+ line(out, 1, "no #{x} with f'(#{x}) = 0: no extremum") if report.extrema.empty?
218
+ second = report.derivatives[1]
219
+ report.extrema.each do |point, value, kind|
220
+ curvature = second.subs(x => point).simplify
221
+ sign = Analysis.numeric(curvature)
222
+ test = if Scalar.zero?(curvature) || sign&.zero?
223
+ "f''(#{point}) = 0, so the sign of f' on either side decides"
224
+ elsif sign
225
+ "f''(#{point}) = #{curvature} #{sign.positive? ? '>' : '<'} 0"
226
+ else
227
+ "f''(#{point}) = #{curvature}"
228
+ end
229
+ line(out, 1, "#{test}: a #{kind} at (#{point}, #{value})")
230
+ end
231
+ end
232
+
233
+ def discussion_inflections(out, report, x)
234
+ section(out, "the inflections: solve f''(#{x}) = 0")
235
+ if report.inflections.nil?
236
+ line(out, 1, "rcas cannot solve f''(#{x}) = 0, so the inflections stay open")
237
+ return
238
+ end
239
+ points = Discussion.solutions(report.derivatives[1], x)
240
+ line(out, 1, "f''(#{x}) = 0 at #{x} = #{Analysis.sort_points(points).join(', ')}") if points&.any?
241
+ line(out, 1, "no #{x} with f''(#{x}) = 0: the curvature never turns") if report.inflections.empty?
242
+ third = report.derivatives[2]
243
+ report.inflections.each do |point, value|
244
+ rate = third.subs(x => point).simplify
245
+ test = if Scalar.zero?(rate)
246
+ "f'''(#{point}) = 0, so the sign of f'' on either side decides"
247
+ else
248
+ "f'''(#{point}) = #{rate}, not 0"
249
+ end
250
+ line(out, 1, "#{test}: an inflection at (#{point}, #{value})")
251
+ end
252
+ end
253
+
254
+ def discussion_chart(out, report, chart, name, headline)
255
+ section(out, headline)
256
+ if chart.nil?
257
+ line(out, 1, "the sign of #{name} is not decided on every piece")
258
+ return
259
+ end
260
+ line(out, 1, "#{name} is zero throughout: nothing to report") if chart.empty?
261
+ chart.each do |interval, kind|
262
+ sign = %i[increasing convex].include?(kind) ? ">" : "<"
263
+ line(out, 1, "#{name} #{sign} 0 on #{interval}: #{kind}")
264
+ end
265
+ line(out, 1, "one period, and it repeats every #{report.period}") if report.period && chart.any?
266
+ end
267
+
268
+ # ---- derivatives ---------------------------------------------------------
269
+
270
+ def derivative(target)
271
+ expr, var = target.is_a?(Derivative) ? [target.expr, target.var] : [Expression.lift(target), nil]
272
+ var = Expression.lift(var || Solve.variable(expr, nil))
273
+ order = target.is_a?(Derivative) ? target.order : 1
274
+ out = []
275
+ result = expr
276
+ order.times do |i|
277
+ line(out, 0, "the derivative number #{i + 1}:", result) if order > 1
278
+ result = differentiate(result.simplify, var, 0, out)
279
+ end
280
+ Derivation.new(Derivative.new(expr, var, order), out, result)
281
+ end
282
+
283
+ # The rule that applies at the top, then the pieces it asks for.
284
+ def differentiate(expr, var, depth, out)
285
+ value = Differentiate.diff(expr, var).simplify
286
+ case expr
287
+ when Num, Const
288
+ line(out, depth, "#{expr} is a constant, so its derivative is 0")
289
+ when Var
290
+ line(out, depth, expr == var ? "d/d#{var} #{var} = 1" : "#{expr} does not depend on #{var}: its derivative is 0")
291
+ when Neg
292
+ line(out, depth, "a minus sign comes along:")
293
+ differentiate(expr.arg, var, depth + 1, out)
294
+ when Add, Sub
295
+ line(out, depth, "sum rule: differentiate each term of #{expr}")
296
+ [expr.left, expr.right].each { |part| differentiate(part, var, depth + 1, out) unless constant?(part, var) }
297
+ when Mul
298
+ product_rule(expr, var, depth, out)
299
+ when Div
300
+ quotient_rule(expr, var, depth, out)
301
+ when Pow
302
+ power_rule(expr, var, depth, out)
303
+ when Fn
304
+ chain_rule(expr, var, depth, out)
305
+ else
306
+ line(out, depth, "the derivative of #{expr}:")
307
+ end
308
+ line(out, depth, "d/d#{var} (#{expr}) = #{value}", value) if depth.positive? && !atom?(expr)
309
+ value
310
+ end
311
+
312
+ def atom?(expr) = expr.is_a?(Var) || expr.is_a?(Num) || expr.is_a?(Const)
313
+
314
+ def product_rule(expr, var, depth, out)
315
+ u, v = expr.left, expr.right
316
+ if constant?(u, var) || constant?(v, var)
317
+ constant, rest = constant?(u, var) ? [u, v] : [v, u]
318
+ line(out, depth, "#{constant} is a constant factor, so it stays where it is:")
319
+ return differentiate(rest, var, depth + 1, out)
320
+ end
321
+ line(out, depth, "product rule (u*v)' = u'*v + u*v', with u = #{u} and v = #{v}")
322
+ differentiate(u, var, depth + 1, out)
323
+ differentiate(v, var, depth + 1, out)
324
+ end
325
+
326
+ def quotient_rule(expr, var, depth, out)
327
+ u, v = expr.left, expr.right
328
+ if constant?(v, var)
329
+ line(out, depth, "dividing by the constant #{v} does not change the rule:")
330
+ return differentiate(u, var, depth + 1, out)
331
+ end
332
+ line(out, depth, "quotient rule (u/v)' = (u'*v - u*v')/v**2, with u = #{u} and v = #{v}")
333
+ differentiate(u, var, depth + 1, out) unless constant?(u, var)
334
+ differentiate(v, var, depth + 1, out)
335
+ end
336
+
337
+ def power_rule(expr, var, depth, out)
338
+ base, exponent = expr.base, expr.exponent
339
+ if constant?(exponent, var)
340
+ line(out, depth, "power rule (u**n)' = n*u**(n - 1)*u', with u = #{base} and n = #{exponent}")
341
+ differentiate(base, var, depth + 1, out) unless base == var
342
+ elsif constant?(base, var)
343
+ line(out, depth, "an exponential: (a**u)' = a**u*log(a)*u', with a = #{base}")
344
+ differentiate(exponent, var, depth + 1, out) unless exponent == var
345
+ else
346
+ line(out, depth, "both base and exponent depend on #{var}, so write #{expr} as exp(#{exponent}*log(#{base}))")
347
+ end
348
+ end
349
+
350
+ # The derivatives a table lists, and the chain rule around them.
351
+ TABLE = {
352
+ sin: "cos(u)", cos: "-sin(u)", tan: "1 + tan(u)**2", exp: "exp(u)", log: "1/u",
353
+ atan: "1/(1 + u**2)", asin: "1/sqrt(1 - u**2)", acos: "-1/sqrt(1 - u**2)",
354
+ sinh: "cosh(u)", cosh: "sinh(u)", abs: "sign(u)", erf: "2*exp(-u**2)/sqrt(pi)",
355
+ Si: "sin(u)/u", Ci: "cos(u)/u", Ei: "exp(u)/u", li: "1/log(u)"
356
+ }.freeze
357
+
358
+ def chain_rule(expr, var, depth, out)
359
+ u = expr.args.first
360
+ rule = TABLE[expr.name]
361
+ known = rule ? "d/du #{expr.name}(u) = #{rule}" : "the derivative of #{expr.name}"
362
+ if u == var
363
+ line(out, depth, "#{known}, from the table")
364
+ else
365
+ line(out, depth, "chain rule: #{known} with u = #{u}, times u'")
366
+ differentiate(u, var, depth + 1, out)
367
+ end
368
+ end
369
+
370
+ # ---- antiderivatives -----------------------------------------------------
371
+
372
+ def antiderivative(target)
373
+ unless target.is_a?(Integral)
374
+ raise ArgumentError, "steps: integrate(...) inside a block, or an integral node"
375
+ end
376
+ var = target.var
377
+ out = []
378
+ result = integrate_part(target.integrand.simplify, var, 0, out)
379
+ result = definite(target, var, result, out) if target.definite?
380
+ Derivation.new(target, out, result.simplify)
381
+ end
382
+
383
+ def definite(target, var, antiderivative, out)
384
+ line(out, 0, "put in the two ends of #{target.from} .. #{target.to}:", antiderivative)
385
+ value = Integrate.definite(target.integrand, var, target.from, target.to)
386
+ line(out, 0, "F(#{target.to}) - F(#{target.from})")
387
+ value
388
+ end
389
+
390
+ # The integrals a table lists, for a linear argument.
391
+ INTEGRALS = {
392
+ sin: "-cos(u)", cos: "sin(u)", exp: "exp(u)", sinh: "cosh(u)", cosh: "sinh(u)",
393
+ tan: "-log(cos(u))"
394
+ }.freeze
395
+
396
+ def integrate_part(expr, var, depth, out)
397
+ value = Integrate.integrate(expr, var)
398
+ if expr.is_a?(Add) || expr.is_a?(Sub)
399
+ line(out, depth, "integrate term by term:")
400
+ [expr.left, expr.right].each { |part| integrate_part(part, var, depth + 1, out) }
401
+ return value
402
+ end
403
+ coefficient, rest = Integrate.split_constant(expr, var)
404
+ unless Scalar.one?(coefficient)
405
+ line(out, depth, "the constant factor #{coefficient} comes out of the integral:")
406
+ integrate_part(rest, var, depth + 1, out)
407
+ line(out, depth, "integral(#{expr}, #{var}) = #{value}", value) if depth.positive?
408
+ return value
409
+ end
410
+ power_integral(expr, var, depth, out) || table_integral(expr, var, depth, out) ||
411
+ substitution(expr, var, depth, out) || by_parts(expr, var, depth, out) ||
412
+ line(out, depth, "rcas integrates #{expr} directly (no textbook rule is being followed here)")
413
+ line(out, depth, "integral(#{expr}, #{var}) = #{value}", value) if depth.positive?
414
+ value
415
+ end
416
+
417
+ # x**n, and 1/x with its logarithm. The factor table is what says which
418
+ # power this is: 1/x is a Div, sqrt(x) a Pow with a rational exponent.
419
+ def power_integral(expr, var, depth, out)
420
+ coefficient, factors = Simplify.factorize(expr)
421
+ return nil unless coefficient == 1 && factors.size == 1
422
+ base, exponent = factors.first
423
+ return nil unless base == var && exponent.is_a?(Numeric) && exponent.real?
424
+ if exponent == -1
425
+ line(out, depth, "integral(1/#{var}) = log(#{var}), the one power the rule below misses")
426
+ else
427
+ line(out, depth, "power rule: integral(u**n) = u**(n + 1)/(n + 1) with n = #{Simplify.normalize_number(exponent)}")
428
+ end
429
+ true
430
+ end
431
+
432
+ # sin(a*x + b) and friends: the table entry, divided by a.
433
+ def table_integral(expr, var, depth, out)
434
+ name, u = expr.is_a?(Fn) && expr.args.size == 1 ? [expr.name, expr.args.first] : [nil, nil]
435
+ name = :exp if expr.is_a?(Pow) && expr.base == Simplify.exp_base
436
+ u = Expression.lift(expr.exponent) if name == :exp && expr.is_a?(Pow)
437
+ entry = INTEGRALS[name]
438
+ return nil if entry.nil?
439
+ a, = Integrate.linear(u, var)
440
+ return nil if a.nil?
441
+ if Scalar.one?(a)
442
+ line(out, depth, "from the table: integral(#{name}(u)) = #{entry}")
443
+ else
444
+ line(out, depth, "from the table: integral(#{name}(u)) = #{entry}, and u = #{u} is linear, so divide by #{a}")
445
+ end
446
+ true
447
+ end
448
+
449
+ # g(u)*u': the substitution that turns the integral into one in u.
450
+ def substitution(expr, var, depth, out)
451
+ candidates(expr, var).each do |u|
452
+ derivative = begin
453
+ u.diff(var).simplify
454
+ rescue StandardError => rescued
455
+ RCAS.guard!(rescued)
456
+ next
457
+ end
458
+ next if Scalar.zero?(derivative)
459
+ # Substitute before cancelling: cancelling first can dissolve the
460
+ # very subexpression that is about to become u (x/(1 + x**2)
461
+ # becomes x/(2*x + 2*x**3) and u is no longer there to be seen).
462
+ t = Var.new(:_u)
463
+ replaced = begin
464
+ Div.new(expr, derivative).subs(u => t).cancel.simplify
465
+ rescue StandardError => rescued
466
+ RCAS.guard!(rescued)
467
+ next
468
+ end
469
+ next if replaced.variables.include?(var.name) || !replaced.variables.include?(t.name)
470
+ line(out, depth, "substitute u = #{u}, so du = #{derivative} d#{var}")
471
+ line(out, depth, "the integral becomes integral(#{replaced.subs(t => Var.new(:u))}, u)")
472
+ return true
473
+ end
474
+ nil
475
+ end
476
+
477
+ # What is worth trying as a substitution: first the inside of a
478
+ # function or a power (u = x**2 in exp(x**2)), then the function itself
479
+ # (u = sin(x) in sin(x)*cos(x)).
480
+ def candidates(expr, var)
481
+ inner = expr.each_node.filter_map do |node|
482
+ argument = node.is_a?(Fn) ? node.args.first : (node.is_a?(Pow) ? node.base : nil)
483
+ next if argument.nil? || atom?(argument) || !argument.variables.include?(var.name)
484
+ argument
485
+ end
486
+ whole = expr.each_node.select { |node| node.is_a?(Fn) && node.args.size == 1 && !constant?(node, var) }
487
+ (inner + whole).uniq
488
+ end
489
+
490
+ # A polynomial times exp/sin/cos, or a lone log/atan/asin.
491
+ BY_PARTS = %i[log atan asin acos].freeze
492
+
493
+ def by_parts(expr, var, depth, out)
494
+ if expr.is_a?(Fn) && BY_PARTS.include?(expr.name) && expr.args.first == var
495
+ line(out, depth, "by parts with u = #{expr} and dv = d#{var}, so v = #{var}")
496
+ line(out, depth, "u*v - integral(v*du) = #{var}*#{expr} - integral(#{var}*#{expr.diff(var).simplify}, #{var})")
497
+ return true
498
+ end
499
+ coefficient, factors = Simplify.factorize(expr)
500
+ polynomial = factors.find { |base, exp| base == var && exp.is_a?(Integer) && exp.positive? }
501
+ return nil if polynomial.nil? || factors.size != 2
502
+ other = Simplify.rebuild_product(1, factors.reject { |base, _| base == polynomial.first })
503
+ u = Simplify.rebuild_product(coefficient, { polynomial.first => polynomial.last })
504
+ v = Integrate.integrate(other, var)
505
+ return nil unless Integrate.complete?(v)
506
+ line(out, depth, "by parts with u = #{u} and dv = #{other} d#{var}")
507
+ line(out, depth, "du = #{u.diff(var).simplify} d#{var} and v = #{v}")
508
+ line(out, depth, "u*v - integral(v*du) leaves integral(#{(v * u.diff(var)).simplify}, #{var})")
509
+ true
510
+ end
511
+
512
+ # ---- solving -------------------------------------------------------------
513
+
514
+ def solution(target, var = nil)
515
+ equation = target.is_a?(Equation) ? target : Equation.new(Expression.lift(target), Num.new(0))
516
+ f = (equation.lhs - equation.rhs).simplify
517
+ var = Expression.lift(var || Solve.variable(f, nil))
518
+ out = []
519
+ line(out, 0, "everything on one side: #{f} = 0", f) unless Scalar.zero?(equation.rhs)
520
+ coefficients = Solve.polynomial_coefficients(f, var)
521
+ if coefficients.nil?
522
+ line(out, 0, "#{f} is not a polynomial in #{var}, so rcas solves it its own way")
523
+ else
524
+ polynomial(coefficients, var, out)
525
+ end
526
+ Derivation.new(Equation.new(equation.lhs, equation.rhs), out, Solve.solve(equation, var.name, principal: true))
527
+ end
528
+
529
+ def polynomial(coefficients, var, out)
530
+ degree = coefficients.size - 1
531
+ case degree
532
+ when 0 then line(out, 0, "there is no #{var} left: #{coefficients.first} = 0 has no solution unless it is 0")
533
+ when 1 then linear(coefficients, var, out)
534
+ when 2 then quadratic(coefficients, var, out)
535
+ else higher(coefficients, degree, var, out)
536
+ end
537
+ end
538
+
539
+ def linear(coefficients, var, out)
540
+ b, a = coefficients
541
+ line(out, 0, "a linear equation #{a}*#{var} + #{b} = 0, so #{var} = -#{b}/#{a} = #{(-b / a).simplify}")
542
+ end
543
+
544
+ def quadratic(coefficients, var, out)
545
+ c, b, a = coefficients
546
+ line(out, 0, "a quadratic a*#{var}**2 + b*#{var} + c = 0 with a = #{a}, b = #{b}, c = #{c}")
547
+ discriminant = (b**2 - 4 * a * c).simplify
548
+ line(out, 0, "the discriminant b**2 - 4*a*c = #{discriminant}", discriminant)
549
+ if Scalar.zero?(discriminant)
550
+ line(out, 0, "it is zero, so the two roots fall together at -b/(2*a)")
551
+ elsif discriminant.variables.empty? && discriminant.evalf.negative?
552
+ line(out, 0, "it is negative, so the two roots are complex conjugates")
553
+ else
554
+ root = Pow.new(discriminant, Num.new(Rational(1, 2))).simplify
555
+ line(out, 0, "#{var} = (-b +- sqrt(b**2 - 4*a*c))/(2*a) = (#{(-b).simplify} +- #{root})/#{(2 * a).simplify}")
556
+ end
557
+ end
558
+
559
+ def higher(coefficients, degree, var, out)
560
+ line(out, 0, "a polynomial of degree #{degree}: look for factors first")
561
+ expression = coefficients.each_with_index.map { |c, k| c * var**k }.reduce(:+).simplify
562
+ factored = expression.factor
563
+ if factored.to_s == expression.to_s
564
+ line(out, 0, "it does not factor over the rationals, so the roots are algebraic numbers (RootOf)")
565
+ else
566
+ line(out, 0, "#{expression} = #{factored}, and a product is zero when one of its factors is", factored)
567
+ end
568
+ end
569
+
570
+ # ---- partial fractions ---------------------------------------------------
571
+
572
+ def fractions(target, var = nil)
573
+ f = Expression.lift(target).cancel
574
+ var = Expression.lift(var || Solve.variable(f, nil))
575
+ out = []
576
+ numerator = RationalFunction.numer(f)
577
+ denominator = RationalFunction.denom(f)
578
+ line(out, 0, "numerator #{numerator} over denominator #{denominator}")
579
+ factored = denominator.factor
580
+ line(out, 0, "factor the denominator: #{denominator} = #{factored}", factored)
581
+ ansatz(numerator, denominator, var, out)
582
+ Derivation.new(Fn.new(:apart, [f]), out, RationalFunction.apart(f, var))
583
+ end
584
+
585
+ # One term per factor and per power of it, with an unknown numerator of
586
+ # one degree less; multiplying up and comparing coefficients in x gives
587
+ # a linear system for the unknowns. This is the lesson, so it is worked
588
+ # out rather than asserted.
589
+ def ansatz(numerator, denominator, var, out)
590
+ pieces = []
591
+ unknowns = []
592
+ names = ("A".."Z").to_a
593
+ denominator.to_poly.factor.factors.each do |factor, multiplicity|
594
+ base = factor.to_expr
595
+ degree = Coefficients.degree(base, var)
596
+ (1..multiplicity).each do |power|
597
+ top = (0...degree).map do |k|
598
+ name = Var.new(names[unknowns.size].to_sym)
599
+ unknowns << name
600
+ name * var**k
601
+ end.reduce(:+).simplify
602
+ pieces << Div.new(top, Simplify.power_node(base, power))
603
+ end
604
+ end
605
+ return line(out, 0, "one term per factor, with an unknown on top") if pieces.empty? || unknowns.size > names.size
606
+ line(out, 0, "the ansatz: (#{numerator})/(#{denominator}) = #{pieces.map(&:to_s).join(' + ')}")
607
+ identity = pieces.map { |piece| (piece * denominator).cancel.expand }.reduce(:+)
608
+ equations = Coefficients.coeffs((identity - numerator).expand, var)
609
+ solution = begin
610
+ Solve.linear_system(equations.map(&:simplify), unknowns).first
611
+ rescue StandardError => rescued
612
+ RCAS.guard!(rescued)
613
+ nil
614
+ end
615
+ if solution
616
+ line(out, 0, "comparing the coefficients of #{var}: #{unknowns.map { |u| "#{u} = #{solution[u] || 0}" }.join(', ')}")
617
+ else
618
+ line(out, 0, "comparing the coefficients of #{var} gives a linear system for #{unknowns.join(', ')}")
619
+ end
620
+ end
621
+
622
+ # ---- Gaussian elimination ------------------------------------------------
623
+
624
+ def elimination(matrix)
625
+ rows = matrix.to_a
626
+ out = []
627
+ pivot_row = 0
628
+ (0...matrix.cols).each do |column|
629
+ break if pivot_row >= matrix.rows
630
+ pivot = (pivot_row...matrix.rows).find { |i| !Scalar.zero?(rows[i][column]) }
631
+ next if pivot.nil?
632
+ if pivot != pivot_row
633
+ rows[pivot_row], rows[pivot] = rows[pivot], rows[pivot_row]
634
+ show(out, rows, matrix, "swap R#{pivot_row + 1} and R#{pivot + 1}")
635
+ end
636
+ unless Scalar.one?(rows[pivot_row][column])
637
+ divisor = rows[pivot_row][column]
638
+ rows[pivot_row] = rows[pivot_row].map { |e| Scalar.div(e, divisor).simplify }
639
+ show(out, rows, matrix, "R#{pivot_row + 1} := R#{pivot_row + 1}/(#{divisor})")
640
+ end
641
+ (0...matrix.rows).each do |i|
642
+ next if i == pivot_row || Scalar.zero?(rows[i][column])
643
+ factor = rows[i][column]
644
+ rows[i] = rows[i].each_with_index.map { |e, j| Scalar.sub(e, Scalar.mul(factor, rows[pivot_row][j])).simplify }
645
+ show(out, rows, matrix, "R#{i + 1} := R#{i + 1} - (#{factor})*R#{pivot_row + 1}")
646
+ end
647
+ pivot_row += 1
648
+ end
649
+ Derivation.new(matrix, out, matrix.rref)
650
+ end
651
+
652
+ def show(out, rows, matrix, text)
653
+ space = MatrixSpace.new(matrix.base.fraction_field, matrix.rows, matrix.cols)
654
+ line(out, 0, "#{text}\n#{space.unchecked(rows.map(&:dup))}")
655
+ end
656
+
657
+ # ---- Euclid --------------------------------------------------------------
658
+
659
+ def euclid(a, b)
660
+ raise ArgumentError, "steps: gcd needs two arguments" if b.nil?
661
+ out = []
662
+ integers = a.is_a?(Integer) && b.is_a?(Integer)
663
+ integers ? integer_euclid(a, b, out) : polynomial_euclid(a, b, out)
664
+ result = integers ? a.gcd(b) : RationalFunction.gcd(a, b)
665
+ Derivation.new(Fn.new(:gcd, [Expression.lift(a), Expression.lift(b)]), out, result)
666
+ end
667
+
668
+ def integer_euclid(a, b, out)
669
+ a, b = a.abs, b.abs
670
+ a, b = b, a if b > a
671
+ while b.positive?
672
+ quotient, remainder = a.divmod(b)
673
+ line(out, 0, "#{a} = #{quotient}*#{b} + #{remainder}")
674
+ a, b = b, remainder
675
+ end
676
+ line(out, 0, "the last remainder that is not zero is the gcd")
677
+ end
678
+
679
+ def polynomial_euclid(a, b, out)
680
+ a = Expression.lift(a)
681
+ b = Expression.lift(b)
682
+ var = Solve.variable(a + b, nil)
683
+ while !Scalar.zero?(b.simplify)
684
+ quotient = RationalFunction.quo(a, b, var)
685
+ remainder = RationalFunction.rem(a, b, var).simplify
686
+ line(out, 0, "#{a} = (#{quotient})*(#{b}) + (#{remainder})")
687
+ a, b = b, remainder
688
+ break if Coefficients.degree(b, var).zero?
689
+ end
690
+ line(out, 0, "the last remainder that is not zero is the gcd, up to a constant factor")
691
+ end
692
+
693
+ # ---- factorization -------------------------------------------------------
694
+
695
+ def factorization(target, var = nil)
696
+ value = target.is_a?(Num) ? target.value : target
697
+ return integer_factors(value) if value.is_a?(Integer)
698
+ polynomial_factors(Expression.lift(value), var)
699
+ end
700
+
701
+ TRIAL_LIMIT = 10_000
702
+
703
+ # Trial division by the primes, one at a time, which is how it is first
704
+ # taught and what rcas itself does until the numbers get large.
705
+ def integer_factors(n)
706
+ out = []
707
+ rest = n.abs
708
+ line(out, 0, "a minus sign comes out in front") if n.negative?
709
+ if rest < 2
710
+ line(out, 0, "#{n} has no prime factorization of its own")
711
+ return Derivation.new(Fn.new(:factor, [Num.new(n)]), out, NumberTheory.factor(n))
712
+ end
713
+ prime = 2
714
+ while rest > 1 && prime <= TRIAL_LIMIT && prime * prime <= rest
715
+ break if NumberTheory.prime?(rest)
716
+ if (rest % prime).zero?
717
+ line(out, 0, "#{rest} = #{prime}*#{rest / prime}")
718
+ rest /= prime
719
+ else
720
+ prime = NumberTheory.nextprime(prime)
721
+ end
722
+ end
723
+ if rest > 1 && NumberTheory.prime?(rest)
724
+ line(out, 0, "#{rest} is prime, and the trial division stops there")
725
+ elsif rest > 1
726
+ line(out, 0, "#{rest} is left, too big to divide through by hand; rcas finishes it with Pollard's rho")
727
+ end
728
+ Derivation.new(Fn.new(:factor, [Num.new(n)]), out, NumberTheory.factor(n))
729
+ end
730
+
731
+ def polynomial_factors(f, var)
732
+ out = []
733
+ result = f.factor
734
+ var = Expression.lift(var) if var
735
+ var ||= f.variables.size == 1 ? Var.new(f.variables.first) : nil
736
+ coefficients = var && Solve.polynomial_coefficients(f.expand, var)
737
+ if coefficients.nil?
738
+ line(out, 0, "several variables, so rcas factors this its own way: squarefree decomposition, " \
739
+ "factoring modulo a prime, Hensel lifting and recombination")
740
+ return Derivation.new(Fn.new(:factor, [f]), out, result)
741
+ end
742
+ rest = common_factor(coefficients, var, out)
743
+ roots(rest, var, out)
744
+ Derivation.new(Fn.new(:factor, [f]), out, result)
745
+ end
746
+
747
+ # The content and the lowest power of the variable.
748
+ def common_factor(coefficients, var, out)
749
+ numbers = coefficients.map { |c| c.is_a?(Num) ? c.value : nil }
750
+ content = numbers.all? { |c| c.is_a?(Integer) } ? numbers.reduce(0) { |a, b| a.gcd(b) } : 1
751
+ low = coefficients.index { |c| !Scalar.zero?(c) }
752
+ polynomial = rebuild(coefficients, var)
753
+ if content > 1 || low.positive?
754
+ factor = Simplify.rebuild_product(content, low.positive? ? { var => low } : {})
755
+ polynomial = rebuild(coefficients.drop(low).map { |c| (c / content).simplify }, var)
756
+ line(out, 0, "every term has #{factor} in it, so it comes out: #{Mul.new(factor, polynomial)}")
757
+ end
758
+ polynomial
759
+ end
760
+
761
+ def rebuild(coefficients, var)
762
+ coefficients.each_with_index.map { |c, k| c * var**k }.reduce(:+).simplify
763
+ end
764
+
765
+ # The rational root theorem, then division, until a quadratic is left.
766
+ def roots(f, var, out, depth = 0)
767
+ coefficients = Solve.polynomial_coefficients(f.expand, var)
768
+ degree = coefficients ? coefficients.size - 1 : 0
769
+ return line(out, depth, "#{f} is linear, so there is nothing left to do") if degree <= 1
770
+ return difference_of_squares(coefficients, var, out, depth) if squares?(coefficients)
771
+ return quadratic_factor(coefficients, f, var, out, depth) if degree == 2
772
+
773
+ root = rational_root(coefficients, var, out, depth)
774
+ unless root
775
+ line(out, depth, "no rational root, so #{f} needs the modular algorithm rcas uses for the general case")
776
+ return
777
+ end
778
+ quotient = RationalFunction.quo(f, var - root, var).simplify
779
+ line(out, depth, "#{f} = (#{(var - root).simplify})*(#{quotient})")
780
+ roots(quotient, var, out, depth)
781
+ end
782
+
783
+ # p/q with p dividing the constant term and q the leading coefficient.
784
+ def rational_root(coefficients, var, out, depth)
785
+ constant = coefficients.first
786
+ leading = coefficients.last
787
+ return nil unless constant.is_a?(Num) && leading.is_a?(Num) &&
788
+ constant.value.is_a?(Integer) && leading.value.is_a?(Integer) && !constant.value.zero?
789
+ tops = NumberTheory.divisors(constant.value)
790
+ bottoms = NumberTheory.divisors(leading.value)
791
+ candidates = tops.product(bottoms).flat_map { |a, b| [Rational(a, b), Rational(-a, b)] }.uniq.sort
792
+ line(out, depth, "a rational root p/q has p dividing #{constant} and q dividing #{leading}: " \
793
+ "try #{candidates.first(8).map { |c| Num.new(Simplify.normalize_number(c)) }.join(', ')}#{' and so on' if candidates.size > 8}")
794
+ f = rebuild(coefficients, var)
795
+ found = candidates.find { |c| Scalar.zero?(f.subs(var => Num.new(Simplify.normalize_number(c))).simplify) }
796
+ return nil if found.nil?
797
+ value = Num.new(Simplify.normalize_number(found))
798
+ line(out, depth, "f(#{value}) = 0, so #{(var - value).simplify} divides it")
799
+ value
800
+ end
801
+
802
+ def quadratic_factor(coefficients, f, var, out, depth)
803
+ c, b, a = coefficients
804
+ discriminant = (b**2 - 4 * a * c).simplify
805
+ line(out, depth, "the quadratic #{f}: its discriminant is #{discriminant}", discriminant)
806
+ if Scalar.zero?(discriminant)
807
+ line(out, depth, "zero, so it is a square: (#{(var + b / (2 * a)).simplify})**2 times #{a}")
808
+ elsif square_number?(discriminant)
809
+ root = Pow.new(discriminant, Num.new(Rational(1, 2))).simplify
810
+ line(out, depth, "#{root}**2, a square, so the roots (#{(-b).simplify} +- #{root})/#{(2 * a).simplify} are rational and it factors")
811
+ else
812
+ line(out, depth, "not a square, so it does not factor over the rationals")
813
+ end
814
+ end
815
+
816
+ # a*x**2 - c with both a and c square numbers.
817
+ def squares?(coefficients)
818
+ return false unless coefficients.size == 3 && Scalar.zero?(coefficients[1])
819
+ a = coefficients[2]
820
+ c = coefficients[0]
821
+ square_number?(a) && a.is_a?(Num) && c.is_a?(Num) && c.value.negative? && square_number?(Num.new(-c.value))
822
+ end
823
+
824
+ def difference_of_squares(coefficients, var, out, depth)
825
+ a = Pow.new(coefficients[2], Num.new(Rational(1, 2))).simplify
826
+ b = Pow.new(Num.new(-coefficients[0].value), Num.new(Rational(1, 2))).simplify
827
+ line(out, depth, "a difference of squares: u**2 - v**2 = (u - v)*(u + v) with u = #{(a * var).simplify} and v = #{b}")
828
+ end
829
+
830
+ def square_number?(value)
831
+ value.is_a?(Num) && value.value.is_a?(Integer) && !value.value.negative? &&
832
+ Integer.sqrt(value.value)**2 == value.value
833
+ end
834
+ end
835
+ end