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,649 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module RCAS
5
+ # The text of a term, built only if the ordering of a sum comes down to
6
+ # it - which is after the degree and the exponents have tied, and rarely.
7
+ # Printing every term of a long sum to sort it cost more than the sorting.
8
+ class LazyText
9
+ include Comparable
10
+
11
+ def initialize(&block) = @block = block
12
+ def text = (@text ||= @block.call)
13
+ def <=>(other) = text <=> other.text
14
+ end
15
+
16
+ # Algebraic simplification.
17
+ #
18
+ # Sums are flattened into (constant, {factors => coefficient}) and products
19
+ # into (coefficient, {base => exponent}); numbers fold exactly using
20
+ # Integer/Rational arithmetic. The result is rebuilt into a tree with a
21
+ # canonical ordering so structurally equal inputs print identically.
22
+ module Simplify
23
+ module_function
24
+
25
+ # Sentinel base for exponential factors: exp(u) is stored as EXP**u so
26
+ # that exp(x)*exp(y) merges into exp(x + y) and exp(x)**2 into exp(2*x).
27
+ def exp_base = (@exp_base ||= Fn.new(:exp, [Num.new(1)]))
28
+
29
+ def simplify(expr)
30
+ case expr
31
+ when Num, Var then expr
32
+ when Add, Sub, Neg then rebuild_sum(*termize(expr, simplify: true))
33
+ when Mul, Div, Pow then rebuild_product(*factorize(expr, simplify: true))
34
+ when Fn then Functions.fold(expr.map_children { |c| simplify(c) })
35
+ when Piecewise then Piecewises.simplify(expr)
36
+ else expr
37
+ end
38
+ end
39
+
40
+ # Rational with denominator 1 becomes Integer, a Complex with zero
41
+ # imaginary part becomes real; everything else is kept.
42
+ def normalize_number(value)
43
+ case value
44
+ when Rational then value.denominator == 1 ? value.numerator : value
45
+ when Complex
46
+ re = normalize_number(value.real)
47
+ im = normalize_number(value.imaginary)
48
+ im.is_a?(Integer) && im.zero? ? re : Complex(re, im)
49
+ else value
50
+ end
51
+ end
52
+
53
+ # Sign test that tolerates Complex (never "negative").
54
+ def negative?(value)
55
+ value.is_a?(Numeric) && value.real? && value.negative?
56
+ end
57
+
58
+ def imaginary_unit?(base) = base.is_a?(Num) && base.value.is_a?(Complex)
59
+
60
+ # For choosing + or - in a sum: -i*x is written with a minus.
61
+ def sign_negative?(value)
62
+ return value.imaginary.negative? if value.is_a?(Complex) && value.real.zero? && value.imaginary.real?
63
+ negative?(value)
64
+ end
65
+
66
+ # ---- sums -------------------------------------------------------------
67
+
68
+ # => [constant, { factors_hash => coefficient }]
69
+ # Iterative so that long left-leaning sums don't exhaust the stack.
70
+ # With simplify: true the non-sum leaves are simplified on the way (a
71
+ # single pass over the whole sum, instead of once per nesting level).
72
+ def termize(expr, sign = 1, constant = 0, terms = {}, simplify: false)
73
+ stack = [[expr, sign, !simplify]]
74
+ until stack.empty?
75
+ e, s, done = stack.pop
76
+ case e
77
+ when Add then stack.push([e.right, s, done], [e.left, s, done])
78
+ when Sub then stack.push([e.right, -s, done], [e.left, s, done])
79
+ when Neg then stack.push([e.arg, -s, done])
80
+ when Num then constant += s * e.value
81
+ else
82
+ unless done
83
+ e = simplify(e)
84
+ if e.is_a?(Add) || e.is_a?(Sub) || e.is_a?(Neg) || e.is_a?(Num)
85
+ stack.push([e, s, true])
86
+ next
87
+ end
88
+ end
89
+ coeff, factors = factorize(e)
90
+ if factors.empty?
91
+ constant += s * coeff
92
+ else
93
+ terms[factors] = (terms[factors] || 0) + s * coeff
94
+ end
95
+ end
96
+ end
97
+ [constant, terms]
98
+ end
99
+
100
+ def rebuild_sum(constant, terms)
101
+ # oo - oo is not 0. Infinity is an atom like any other in the term
102
+ # table, so a cancelling infinite term - or one already undefined -
103
+ # has to be caught here, before the table is rebuilt into a tree.
104
+ return UNDEFINED if terms.any? { |factors, coeff| undefined_term?(factors, coeff) }
105
+ # A finite quantity added to an infinite one changes nothing: oo - 2
106
+ # is oo. Two different infinite terms are left alone (oo*x - oo says
107
+ # nothing until x does).
108
+ infinite = terms.reject { |_, c| c.zero? }.select { |factors, _| factors.key?(OO) }
109
+ return rebuild_product(infinite.values.first, infinite.keys.first) if infinite.size == 1
110
+ parts = terms.reject { |_, c| c.zero? }.map do |factors, coeff|
111
+ negative = sign_negative?(coeff)
112
+ [rebuild_product(negative ? -coeff : coeff, factors), negative, term_key(factors)]
113
+ end
114
+ parts.sort_by! { |_, _, key| key }
115
+ unless constant.zero?
116
+ entry = [Num.new(sign_negative?(constant) ? -constant : constant), sign_negative?(constant)]
117
+ index = parts.index { |_, _, key| key.first >= 0 } || parts.size
118
+ parts.insert(index, entry)
119
+ end
120
+ return Num.new(0) if parts.empty?
121
+ sum_tree(parts.map { |e, neg| [e, neg] })
122
+ end
123
+
124
+ # Longest left-leaning chain we build; longer sums become balanced trees
125
+ # of such chains so that tree depth stays logarithmic.
126
+ CHAIN = 32
127
+
128
+ # parts: [[expr, negative?], ...]
129
+ def sum_tree(parts)
130
+ if parts.size <= CHAIN
131
+ (first, negative), *rest = parts
132
+ acc = negative ? negate(first) : first
133
+ return rest.reduce(acc) { |sum, (e, neg)| neg ? Sub.new(sum, e) : Add.new(sum, e) }
134
+ end
135
+ left, right = parts.each_slice((parts.size + 1) / 2).to_a
136
+ if right.first[1]
137
+ Sub.new(sum_tree(left), sum_tree(right.map { |e, neg| [e, !neg] }))
138
+ else
139
+ Add.new(sum_tree(left), sum_tree(right))
140
+ end
141
+ end
142
+
143
+ # ---- products ---------------------------------------------------------
144
+
145
+ # => [coefficient, { base => exponent }]
146
+ # Exponents are Ruby numbers when numeric, otherwise Expressions.
147
+ # Iterative over Mul/Div chains for the same reason as termize.
148
+ def factorize(expr, power = 1, coeff = 1, factors = {}, simplify: false)
149
+ stack = [[expr, power, !simplify]]
150
+ until stack.empty?
151
+ e, pw, done = stack.pop
152
+ case e
153
+ when Num
154
+ coeff *= pow_number(e.value, pw)
155
+ when Neg
156
+ coeff *= pow_number(-1, pw)
157
+ stack.push([e.arg, pw, done])
158
+ when Mul
159
+ stack.push([e.right, pw, done], [e.left, pw, done])
160
+ when Div
161
+ raise ZeroDivisionError, "division by zero" if e.right.is_a?(Num) && e.right.zero?
162
+ stack.push([e.right, -pw, done], [e.left, pw, done])
163
+ when Pow
164
+ e = Pow.new(simplify(e.base), simplify(e.exponent)) unless done
165
+ exp = exponent_value(e.exponent)
166
+ if e.base.is_a?(Num) && e.base.value == 1
167
+ next # 1**anything
168
+ elsif e.base.is_a?(Num) && exp.is_a?(Expression) && (e.base.value.is_a?(Integer) || e.base.value.is_a?(Rational))
169
+ # 2**(k + 1) => 2 * 2**k so that both spellings share one canonical form
170
+ constant, rest = termize(exp)
171
+ if constant.is_a?(Integer) && !constant.zero?
172
+ coeff *= pow_number(e.base.value, constant * pw) if pw.is_a?(Integer)
173
+ exp = exponent_value(rebuild_sum(0, rest))
174
+ end
175
+ add_factor(factors, e.base, multiply_exponents(exp, pw))
176
+ elsif e.base.is_a?(Fn) && e.base.name == :exp && e.base.args.size == 1 &&
177
+ exp_power_mergeable?(e.base.args.first, multiply_exponents(exp, pw))
178
+ # e**x, exp(u)**v => exp(u*v)
179
+ add_factor(factors, exp_base, multiply_exponents(multiply_exponents(exponent_value(e.base.args.first), exp), pw))
180
+ elsif e.base.is_a?(Fn) && e.base.name == :exp && e.base.args.size == 1
181
+ # The branch would move: sqrt(exp(2*pi*i)) is 1 and exp(pi*i) is
182
+ # -1. The power stays as it stands (see exp_power_mergeable?).
183
+ add_factor(factors, power_node(e.base, exp), pw)
184
+ elsif exp == 0 && infinity?(e.base)
185
+ next # oo**0 is 1, as t**0 is for every t; oo/oo still cancels to undefined
186
+ elsif exp.is_a?(Integer)
187
+ stack.push([e.base, pw * exp, true])
188
+ elsif exp.is_a?(Rational) && e.base.is_a?(Pow) && e.base.exponent.is_a?(Num) &&
189
+ e.base.exponent.value.is_a?(Integer) && RCAS.nonnegative?(e.base.base)
190
+ # (x**2)**(1/2) is x when x cannot be negative
191
+ stack.push([e.base.base, multiply_exponents(e.base.exponent.value, multiply_exponents(exp, pw)), true])
192
+ elsif exp.is_a?(Rational) && e.base.is_a?(Pow) && e.base.exponent.is_a?(Num) &&
193
+ e.base.exponent.value.is_a?(Rational) && RCAS.nonnegative?(e.base.base)
194
+ # (2**(1/2))**(1/2) is 2**(1/4): (b**r)**s = b**(r*s) for real r, s
195
+ # when b cannot be negative - b**r is then a real power of a
196
+ # positive number and no branch moves
197
+ stack.push([Pow.new(e.base.base, Num.new(e.base.exponent.value * exp)), pw, true])
198
+ elsif exp.is_a?(Rational) && exp.denominator > 1 && (split = root_of_product(e.base, exp))
199
+ # (a**2*x**2)**(1/2) is a*(x**2)**(1/2) for a nonnegative a: a
200
+ # factor that cannot be negative comes out of the root, and the
201
+ # rest stays inside, where its sign is still nobody's business.
202
+ outside, inside = split
203
+ outside.each { |base, power| stack.push([base, multiply_exponents(power, pw), true]) }
204
+ stack.push([Pow.new(inside, Num.new(exp)), pw, true])
205
+ elsif exp.is_a?(Numeric) && e.base.is_a?(Num) && (root = exact_power(e.base.value, exp))
206
+ coeff *= pow_number(root, pw)
207
+ elsif e.base.is_a?(Num) && exp.is_a?(Numeric) && (exp.is_a?(Float) || e.base.value.is_a?(Float))
208
+ coeff *= pow_number(e.base.value**exp, pw)
209
+ elsif e.base.is_a?(Num) && e.base.value.is_a?(Integer) && e.base.value.negative? && exp.is_a?(Rational) && exp.denominator == 2
210
+ # sqrt(-n) = i*sqrt(n)
211
+ coeff *= Complex(0, 1)**(exp.numerator * pw) if pw.is_a?(Integer)
212
+ return factorize(expr, power, coeff, factors) unless pw.is_a?(Integer) # give up merging; rare
213
+ stack.push([Pow.new(Num.new(-e.base.value), Num.new(exp)), pw, true])
214
+ elsif e.base.is_a?(Num) && exp.is_a?(Rational) && (parts = extract_root(e.base.value, exp.denominator))
215
+ root, rest = parts
216
+ coeff *= pow_number(root, exp.numerator * pw)
217
+ add_factor(factors, Num.new(rest), multiply_exponents(exp, pw)) unless rest == 1
218
+ elsif exp.is_a?(Rational) && (e.base.is_a?(Mul) || e.base.is_a?(Div)) && (split = positive_coefficient_split(e.base))
219
+ # (4*a)**(1/2) => 4**(1/2) * a**(1/2): a positive number may always leave the root
220
+ c, rest = split
221
+ stack.push([Pow.new(Num.new(c), e.exponent), pw, true], [Pow.new(rest, e.exponent), pw, true])
222
+ elsif exp.is_a?(Rational) && (e.base.is_a?(Add) || e.base.is_a?(Sub)) && (split = sum_content_split(e.base, exp.denominator))
223
+ # (4 - 4*y**2)**(1/2) => 2*(1 - y**2)**(1/2)
224
+ c, rest = split
225
+ stack.push([Pow.new(Num.new(c), e.exponent), pw, true], [Pow.new(rest, e.exponent), pw, true])
226
+ else
227
+ add_factor(factors, e.base, multiply_exponents(exp, pw))
228
+ end
229
+ when Fn
230
+ if e.name == :exp && e.args.size == 1
231
+ if exp_power_mergeable?(e.args.first, pw)
232
+ add_factor(factors, exp_base, multiply_exponents(exponent_value(e.args.first), pw))
233
+ else
234
+ add_factor(factors, e, pw)
235
+ end
236
+ else
237
+ unless done
238
+ e = simplify(e)
239
+ if e.is_a?(Num) || e.is_a?(Neg) || e.is_a?(Mul) || e.is_a?(Div) || e.is_a?(Pow) || (e.is_a?(Fn) && e.name == :exp)
240
+ stack.push([e, pw, true])
241
+ next
242
+ end
243
+ end
244
+ add_factor(factors, e, pw)
245
+ end
246
+ else
247
+ unless done
248
+ e = simplify(e)
249
+ if e.is_a?(Num) || e.is_a?(Neg) || e.is_a?(Mul) || e.is_a?(Div) || e.is_a?(Pow)
250
+ stack.push([e, pw, true])
251
+ next
252
+ end
253
+ end
254
+ add_factor(factors, e, pw)
255
+ end
256
+ end
257
+ Combinatorics.merge_factorials(factors) if factors.count { |b, _| b.is_a?(Fn) && b.name == :factorial } > 1
258
+ [coeff, factors]
259
+ end
260
+
261
+ # sign(u)*|u|**e is u**e for an odd e, since |u| = u*sign(u) and
262
+ # sign(u)**2 = 1 wherever the product has a value: sign(x)/abs(x) is
263
+ # 1/x, which an antiderivative valid on both sides of a point writes.
264
+ def sign_times_abs(factors)
265
+ factors.each do |base, exp|
266
+ next unless base.is_a?(Fn) && base.name == :sign && exp == 1
267
+ modulus = Fn.new(:abs, base.args)
268
+ power = factors[modulus]
269
+ next unless power.is_a?(Integer) && power.odd?
270
+ rest = factors.reject { |b, _| b == base || b == modulus }
271
+ add_factor(rest, base.args.first, power)
272
+ return rest
273
+ end
274
+ factors
275
+ end
276
+
277
+ def rebuild_product(coeff, factors)
278
+ coeff = normalize_number(coeff)
279
+ # 0*oo and oo/oo have no value either (the second reaches here as an
280
+ # infinite factor whose exponents have cancelled to zero).
281
+ return UNDEFINED if undefined_term?(factors, coeff)
282
+ return Num.new(0) if coeff.zero?
283
+ coeff, factors = absorb_infinity(coeff, factors) if factors.key?(OO)
284
+ return Num.new(0) if factors.nil?
285
+ return Num.new(coeff) if factors.all? { |_, exp| exp.is_a?(Numeric) && exp.zero? }
286
+ factors = sign_times_abs(factors) if factors.keys.any? { |b| b.is_a?(Fn) && b.name == :sign }
287
+ # (-1)**(k/2) is i**k: sqrt(-1/4) came out as (-1)**(1/2)/2 while
288
+ # sqrt(-4) was 2*i (third review, 3.3)
289
+ minus_one = Num.new(-1)
290
+ half = factors[minus_one]
291
+ if half.is_a?(Rational) && half.denominator == 2
292
+ factors = factors.reject { |base, _| base == minus_one }
293
+ coeff = normalize_number(coeff * Complex(0, 1)**(2 * half).to_i)
294
+ end
295
+ if coeff.is_a?(Complex) && coeff.real.zero?
296
+ # The unit joins the powers of i already there: i*i**(1/2) is
297
+ # i**(3/2), and writing i => 1 over the 1/2 would lose the root.
298
+ unit = Num.new(Complex(0, 1))
299
+ exp = (factors[unit] || 0) + 1
300
+ coeff = coeff.imaginary
301
+ factors = factors.reject { |base, _| base == unit }
302
+ if exp.is_a?(Integer) || (exp.is_a?(Rational) && exp.denominator == 1)
303
+ coeff = normalize_number(coeff * Complex(0, 1)**exp.to_i)
304
+ if coeff.is_a?(Complex) && coeff.real.zero?
305
+ factors[unit] = 1
306
+ coeff = coeff.imaginary
307
+ end
308
+ else
309
+ factors[unit] = exp
310
+ end
311
+ end
312
+
313
+ # 2**(-1/2) => 2**(1/2)/2, 2**(3/2) => 2*2**(1/2): a positive integer base keeps a
314
+ # fractional exponent in (0, 1); the integer part moves into the coefficient.
315
+ if factors.any? { |base, exp| base.is_a?(Num) && base.value.is_a?(Rational) && base.value.positive? && exp.is_a?(Rational) }
316
+ split = {}
317
+ factors.each do |base, exp|
318
+ if base.is_a?(Num) && base.value.is_a?(Rational) && base.value.positive? && exp.is_a?(Rational)
319
+ add_factor(split, Num.new(base.value.numerator), exp) unless base.value.numerator == 1
320
+ add_factor(split, Num.new(base.value.denominator), -exp)
321
+ else
322
+ add_factor(split, base, exp)
323
+ end
324
+ end
325
+ factors = split
326
+ end
327
+ factors = factors.to_h do |base, exp|
328
+ next [base, exp] unless base.is_a?(Num) && base.value.is_a?(Integer) && base.value.positive? && exp.is_a?(Rational) && exp.floor != 0
329
+ m = exp.floor
330
+ coeff = normalize_number(coeff * Rational(base.value)**m)
331
+ [base, exp - m]
332
+ end
333
+ numerator, denominator = [], []
334
+ factors.sort_by { |base, _| factor_key(base) }.each do |base, exp|
335
+ next if exp.is_a?(Numeric) && exp.zero?
336
+ if exp.is_a?(Numeric) && negative?(exp)
337
+ node = power_node(base, -exp)
338
+ node.is_a?(Num) && !imaginary_unit?(base) ? coeff = normalize_number(coeff.quo(node.value)) : denominator << node
339
+ else
340
+ node = power_node(base, exp)
341
+ node.is_a?(Num) && !imaginary_unit?(base) ? coeff = normalize_number(coeff * node.value) : numerator << node
342
+ end
343
+ end
344
+ return Num.new(0) if coeff.zero?
345
+ # every factor folded into the number: (2**(1/2)/2)**2 is the number
346
+ # 1/2, not Div(1, 2)
347
+ return Num.new(coeff) if numerator.empty? && denominator.empty?
348
+
349
+ if numerator.size == 1 && denominator.empty? && coeff != 1 &&
350
+ (numerator.first.is_a?(Add) || numerator.first.is_a?(Sub))
351
+ constant, terms = termize(numerator.first)
352
+ if (constant.zero? ? 0 : 1) + terms.size >= 2
353
+ return rebuild_sum(constant * coeff, terms.transform_values { |c| c * coeff })
354
+ end
355
+ end
356
+
357
+ if coeff.is_a?(Rational)
358
+ denominator.unshift(Num.new(coeff.denominator))
359
+ coeff = coeff.numerator
360
+ end
361
+
362
+ negative = negative?(coeff)
363
+ coeff = negative ? -coeff : coeff
364
+ numerator.unshift(Num.new(coeff)) unless coeff == 1 && !numerator.empty?
365
+
366
+ result = product_node(numerator)
367
+ result = Div.new(result, product_node(denominator)) unless denominator.empty?
368
+ negative ? negate(result) : result
369
+ end
370
+
371
+ # [what every term of a sum has in common, what is left], or nil:
372
+ # -2*log(x) + x*log(x) is log(x) times (x - 2). `factor` refuses this
373
+ # one, because log(x) is not a polynomial variable, but the term table
374
+ # has the factor in plain sight. Both `solve` and `discuss` look for a
375
+ # product this way before giving up on an equation.
376
+ def common_factor(expr)
377
+ constant, terms = Expand.table(expr)
378
+ return nil unless constant.zero?
379
+ return nil if terms.size < 2
380
+ shared = terms.keys.first.select { |base, exponent| terms.keys.all? { |factors| factors[base] == exponent } }
381
+ return nil if shared.empty?
382
+ common = shared.reduce(Num.new(1)) { |product, (base, exponent)| (product * power_node(base, exponent)).simplify }
383
+ [common, Expand.expand(expr / common)]
384
+ end
385
+
386
+ # ---- helpers ----------------------------------------------------------
387
+
388
+ # A factor map that must not be rebuilt into a value: an infinity that
389
+ # cancels (against a zero coefficient in a product, against another term
390
+ # in a sum, or against itself in the exponents), or an undefined factor.
391
+ def undefined_term?(factors, coeff)
392
+ return true if factors.key?(UNDEFINED)
393
+ return false unless factors.key?(OO)
394
+ exp = factors[OO]
395
+ coeff.zero? || (exp.is_a?(Numeric) && exp.zero?)
396
+ end
397
+
398
+ # oo or -oo as written, before the tables merge it with anything.
399
+ def infinity?(e) = e == OO || (e.is_a?(Neg) && infinity?(e.arg))
400
+
401
+ # Infinity swallows what multiplies it: 2*oo and oo/2 are oo, oo**2 is
402
+ # oo, and a number over oo is 0 (nil for the factors says so). Only the
403
+ # sign of a numeric coefficient survives; a symbolic one is kept, since
404
+ # x*oo is not oo. The cancellations are caught before this.
405
+ def absorb_infinity(coeff, factors)
406
+ exp = factors[OO]
407
+ return [coeff, factors] unless exp.is_a?(Numeric) && exp.real? && coeff.is_a?(Numeric) && coeff.real?
408
+ return [coeff, nil] if exp.negative? && factors.size == 1
409
+ return [coeff, factors] if exp.negative?
410
+ [coeff.negative? ? -1 : 1, factors.merge(OO => 1)]
411
+ end
412
+
413
+ # exp(u)**v is exp(u*v) only where the choice of branch cannot change.
414
+ # For an integer v the power is a repeated product, and for a real u the
415
+ # base exp(u) is a positive real whose principal power is the real one;
416
+ # in between the rule changes the value. sqrt(exp(x)) at x = 2*pi*i is
417
+ # sqrt(1) = 1, while exp(x/2) there is exp(pi*i) = -1 (22 Sept 2026,
418
+ # from a review). An undeclared indeterminate is not known to be real,
419
+ # so exp(x)**(1/2) keeps its shape until `assume(x: RR)` says otherwise.
420
+ def exp_power_mergeable?(u, v)
421
+ return true if v.is_a?(Integer)
422
+ ComplexParts.real_valued?(Expression.lift(u))
423
+ end
424
+
425
+ def add_factor(factors, base, exp)
426
+ factors[base] = add_exponents(factors[base] || 0, exp)
427
+ end
428
+
429
+ def exponent_value(exp) = exp.is_a?(Num) ? exp.value : exp
430
+
431
+ def add_exponents(a, b)
432
+ return normalize_number(a + b) if a.is_a?(Numeric) && b.is_a?(Numeric)
433
+ exponent_value(simplify(Expression.lift(a) + Expression.lift(b)))
434
+ end
435
+
436
+ def multiply_exponents(a, b)
437
+ return normalize_number(a * b) if a.is_a?(Numeric) && b.is_a?(Numeric)
438
+ exponent_value(simplify(Expression.lift(a) * Expression.lift(b)))
439
+ end
440
+
441
+ # Exact integer power, going through Rational for negative exponents.
442
+ # The factors of a product that may leave a root: nonnegative ones whose
443
+ # exponent the root divides, since sqrt(c**2*w) is c*sqrt(w) for a real
444
+ # c >= 0 and any w. Everything else stays inside. nil when nothing comes
445
+ # out, which is what stops the rule from firing on its own result.
446
+ def root_of_product(base, exp)
447
+ # sqrt(pi**2) is pi and sqrt(1/pi**2) is 1/pi, as sqrt(4*pi**2) is 2*pi
448
+ return nil unless base.is_a?(Mul) || base.is_a?(Div) || (base.is_a?(Pow) && base.exponent.is_a?(Num) && base.exponent.value.is_a?(Integer))
449
+ coeff, factors = factorize(base)
450
+ outside = {}
451
+ inside = {}
452
+ factors.each do |factor, power|
453
+ if power.is_a?(Integer) && (power * exp).denominator == 1 && !power.zero? && RCAS.nonnegative?(factor)
454
+ outside[factor] = power * exp
455
+ else
456
+ inside[factor] = power
457
+ end
458
+ end
459
+ return nil if outside.empty?
460
+ [outside, rebuild_product(coeff, inside)]
461
+ end
462
+
463
+ def pow_number(value, power)
464
+ return value if power == 1
465
+ return normalize_number(value**power) if value.is_a?(Complex)
466
+ if power.is_a?(Integer) && power.negative?
467
+ raise ZeroDivisionError, "division by zero" if value.zero?
468
+ return normalize_number(Rational(value)**power) if value.is_a?(Integer) || value.is_a?(Rational)
469
+ end
470
+ normalize_number(value**power)
471
+ end
472
+
473
+ # 4 ** (1/2) => 2, 8 ** (2/3) => 4, 2 ** (1/2) => nil (stays symbolic).
474
+ def exact_power(value, exp)
475
+ return nil unless exp.is_a?(Rational) && value.is_a?(Integer) && value >= 0
476
+ root = integer_root(value, exp.denominator)
477
+ return nil unless root**exp.denominator == value
478
+ pow_number(root, exp.numerator)
479
+ end
480
+
481
+ # The integer n-th root of a non-negative integer, by bisection on the
482
+ # bit length. value**(1.0/n) overflows to Infinity above 10**308, so
483
+ # root(10**400, 3) used to raise FloatDomainError.
484
+ def integer_root(value, n)
485
+ return Integer.sqrt(value) if n == 2
486
+ return value if value < 2
487
+ lo = 1
488
+ hi = 1 << ((value.bit_length + n - 1) / n) # 2**ceil(bits/n) is past the root
489
+ while lo < hi
490
+ mid = (lo + hi + 1) / 2
491
+ mid**n <= value ? lo = mid : hi = mid - 1
492
+ end
493
+ lo
494
+ end
495
+
496
+ def power_node(base, exp)
497
+ return Functions.fold(Fn.new(:exp, [Expression.lift(exp)])) if base == exp_base
498
+ return base if exp == 1
499
+ Pow.new(base, Expression.lift(exp))
500
+ end
501
+
502
+ # 32 = 4**2 * 2 => [4, 2] for q = 2; nil when nothing can be extracted.
503
+ def extract_root(value, q)
504
+ if value.is_a?(Rational)
505
+ num = extract_root(value.numerator, q) || [1, value.numerator]
506
+ den = extract_root(value.denominator, q) || [1, value.denominator]
507
+ return nil if num.first == 1 && den.first == 1
508
+ return [Rational(num.first, den.first), normalize_number(Rational(num.last, den.last))]
509
+ end
510
+ return nil unless value.is_a?(Integer) && value > 1
511
+ division = NumberTheory.prime_division(value, hard: false) or return nil
512
+ root = 1
513
+ rest = 1
514
+ division.each do |prime, e|
515
+ root *= prime**(e / q)
516
+ rest *= prime**(e % q)
517
+ end
518
+ root == 1 ? nil : [root, rest]
519
+ end
520
+
521
+ # [c, rest] for a product c*rest with a positive rational c != 1, else nil.
522
+ def positive_coefficient_split(expr)
523
+ c, factors = factorize(expr)
524
+ return nil unless (c.is_a?(Integer) || c.is_a?(Rational)) && !c.zero? && c.abs != 1 && !factors.empty?
525
+ [c.abs, rebuild_product(c.negative? ? -1 : 1, factors)] # sqrt(-4*a) = 2*sqrt(-a)
526
+ end
527
+
528
+ # [content, sum / content] when the rational content of a sum with
529
+ # rational coefficients has a q-th root to extract, else nil.
530
+ def sum_content_split(expr, q)
531
+ constant, terms = termize(expr)
532
+ values = terms.values + (constant.zero? ? [] : [constant])
533
+ return nil unless values.all? { |v| v.is_a?(Integer) || v.is_a?(Rational) }
534
+ content = values.map(&:abs).reduce { |g, v| Polynomial.rational_gcd(g, v) }
535
+ return nil if content == 1 || extract_root(content, q).nil?
536
+ rest = rebuild_sum(normalize_number(Rational(constant) / content), terms.transform_values { |c| normalize_number(Rational(c) / content) })
537
+ [content, rest]
538
+ end
539
+
540
+ # A sum whose terms are all negative, like -1 - 2*x.
541
+ def negative_sum?(expr)
542
+ return false unless expr.is_a?(Add) || expr.is_a?(Sub)
543
+ constant, terms = termize(expr)
544
+ !negative?(-constant) && terms.values.all? { |c| negative?(c) } && !(constant.zero? && terms.empty?)
545
+ end
546
+
547
+ def product_node(list)
548
+ return Num.new(1) if list.empty?
549
+ return list.reduce { |acc, f| Mul.new(acc, f) } if list.size <= CHAIN
550
+ left, right = list.each_slice((list.size + 1) / 2).to_a
551
+ Mul.new(product_node(left), product_node(right))
552
+ end
553
+
554
+ # Negate a product by flipping the sign of its leading numeric factor when
555
+ # there is one, so "-2*x" rather than "-(2*x)".
556
+ def negate(expr)
557
+ case expr
558
+ when Num then Num.new(-expr.value)
559
+ when Div then Div.new(negate(expr.left), expr.right)
560
+ when Mul
561
+ leading = negate(expr.left)
562
+ leading.is_a?(Neg) ? Neg.new(expr) : Mul.new(leading, expr.right)
563
+ else Neg.new(expr)
564
+ end
565
+ end
566
+
567
+ # Ordering of terms in a sum: ascending total degree ("1 + 2*x + x**2"),
568
+ # then graded lexicographic on variables ("x**3 + 3*x**2*y + ..."), then
569
+ # by text as a tie-break.
570
+ def sort_key(expr)
571
+ [degree(expr), exponent_vector(expr), expr.to_s]
572
+ end
573
+
574
+ # The same key for a term of the table, read off its factor map instead
575
+ # of building rebuild_product(1, factors) to ask it: that second build
576
+ # was a quarter of the time of a 20 000-term simplify. A numeric base
577
+ # keeps the long way round, because rebuilding moves the integer part of
578
+ # its exponent into the coefficient and the map here has not.
579
+ def term_key(factors)
580
+ unless factors.keys.all? { |base| keyable?(base) }
581
+ node = rebuild_product(1, factors)
582
+ return [degree(node), exponent_vector(node), LazyText.new { node.to_s }]
583
+ end
584
+ [factor_degree(factors), factor_exponents(factors), LazyText.new { rebuild_product(1, factors).to_s }]
585
+ end
586
+
587
+ # A base the rebuild hands back unchanged, so that reading the map is
588
+ # the same as re-factorizing the node. A number moves its exponent's
589
+ # integer part into the coefficient, and a power or a quotient is
590
+ # flattened into the map, so those take the long way round.
591
+ def keyable?(base)
592
+ case base
593
+ when Fn then base == exp_base || base.name != :exp # exp(u) is stored as EXP**u
594
+ when Var, Const, Add, Sub then true
595
+ else false
596
+ end
597
+ end
598
+
599
+ def factor_degree(factors)
600
+ factors.sum do |base, exp|
601
+ next 0 if exp.is_a?(Numeric) && exp.zero?
602
+ next degree(base) * exp if exp.is_a?(Integer)
603
+ exp.is_a?(Numeric) && negative?(exp) ? -1 : 1
604
+ end
605
+ end
606
+
607
+ def factor_exponents(factors)
608
+ factors.filter_map do |base, exp|
609
+ next nil if exp.is_a?(Numeric) && exp.zero?
610
+ [base.to_s, exp.is_a?(Numeric) && exp.real? ? -exp : 0]
611
+ end.sort
612
+ end
613
+
614
+ def exponent_vector(expr)
615
+ _, factors = factorize(expr)
616
+ factors.map { |base, exp| [base.to_s, exp.is_a?(Numeric) && exp.real? ? -exp : 0] }.sort
617
+ rescue ZeroDivisionError
618
+ []
619
+ end
620
+
621
+ # Ordering of factors in a product: variables first, then functions, then
622
+ # parenthesized sums.
623
+ def factor_key(base)
624
+ rank =
625
+ case base
626
+ when Num then base.value.is_a?(Complex) ? -1 : 0
627
+ when Const then 1
628
+ when Var then 2
629
+ when Fn then 3
630
+ else 4
631
+ end
632
+ [rank, base.to_s]
633
+ end
634
+
635
+ def degree(expr)
636
+ case expr
637
+ when Num, Const then 0
638
+ when Var then 1
639
+ when Neg then degree(expr.arg)
640
+ when Mul then degree(expr.left) + degree(expr.right)
641
+ when Div then degree(expr.left) - degree(expr.right)
642
+ when Pow then expr.exponent.is_a?(Num) && expr.exponent.value.is_a?(Integer) ? degree(expr.base) * expr.exponent.value : 1
643
+ when Add, Sub then [degree(expr.left), degree(expr.right)].max
644
+ when Fn then expr.name == :O ? 10**9 : 1
645
+ else 1
646
+ end
647
+ end
648
+ end
649
+ end