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
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
|