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,411 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # Integrals along a curve and over a surface, and the three theorems that
5
+ # turn one of them into another.
6
+ #
7
+ # line_integral(x*y, [cos(t), sin(t)], t: 0..pi/2) # scalar: f ds
8
+ # line_integral([-y, x], [cos(t), sin(t)], t: 0..2*pi) # vector: F . dr
9
+ # surface_integral(1, [u, v, u + v], u: 0..1, v: 0..1) # f dS
10
+ # flux([x, y, z], sphere, u: 0..2*pi, v: 0..pi) # F . n dS
11
+ # green([-y, x], x: 0..1, y: 0..1) # the double integral
12
+ # stokes([-y, x, 0], disc, u: 0..1, v: 0..2*pi)
13
+ # divergence_theorem([x, y, z], x: 0..1, y: 0..1, z: 0..1)
14
+ #
15
+ # Everything here is a parametrization followed by an ordinary integral:
16
+ # a curve is [x(t), y(t)] or [x(t), y(t), z(t)], a surface is
17
+ # [x(u, v), y(u, v), z(u, v)], and the field is written in x, y, z (name
18
+ # other coordinates with vars:). The integrals are the ones integrate
19
+ # already does, so an integrand it cannot do stays an integral(...) node.
20
+ #
21
+ # Sources (keys: MANUAL.md, Sources): the elements ds = |r'| dt and
22
+ # dS = |r_u x r_v| du dv and the three theorems are the textbook ones
23
+ # [MT12, ch. 7-8]; the general statement behind all three is Stokes's
24
+ # theorem for differential forms [Spi65, ch. 4-5].
25
+ module VectorCalculus
26
+ module_function
27
+
28
+ # The coordinates a field is written in, when nobody says: x, y, z if
29
+ # that is all it uses, otherwise its own variables if there are exactly
30
+ # as many as the parametrization has components.
31
+ COORDINATES = %i[x y z].freeze
32
+
33
+ def coordinates(integrand, vars, dim)
34
+ return Array(vars).map { |v| Expression.lift(v) } if vars && !Array(vars).empty?
35
+ default = COORDINATES.first(dim).map { |n| Var.new(n) }
36
+ names = list(integrand).flat_map { |f| Expression.lift(f).variables }.uniq
37
+ # a field that mentions x, y or z is read in x, y, z, and its other
38
+ # names are parameters: (a*y, -x, 0) with a as a coordinate did no
39
+ # work around the circle (third review, L8)
40
+ return default if (names - COORDINATES).empty? || names.size != dim || names.any? { |n| COORDINATES.include?(n) }
41
+ names.sort.map { |n| Var.new(n) }
42
+ end
43
+
44
+ def vector?(obj) = obj.is_a?(Array) || obj.is_a?(Vector)
45
+
46
+ # A field, a curve or a surface as a plain Array; a scalar field is one
47
+ # component of its own, which is what tells the two kinds apart.
48
+ def list(obj)
49
+ return obj.to_a if obj.is_a?(Vector)
50
+ obj.is_a?(Array) ? obj : [obj]
51
+ end
52
+
53
+ # The components of a curve, a surface or a field, as Expressions.
54
+ def components(obj, name, sizes)
55
+ parts = list(obj)
56
+ unless vector?(obj) && sizes.include?(parts.size)
57
+ raise ArgumentError, "#{name}: expected #{sizes.map(&:to_s).join(' or ')} components, got #{obj.inspect}"
58
+ end
59
+ parts.map { |c| Expression.lift(c) }
60
+ end
61
+
62
+ # f(r(t)): the field along the parametrization.
63
+ def along(expr, coords, parametrization)
64
+ Expression.lift(expr).subs(coords.zip(parametrization).to_h)
65
+ end
66
+
67
+ def dot(a, b) = a.zip(b).map { |u, v| u * v }.reduce(:+).simplify
68
+
69
+ def cross(a, b)
70
+ [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]].map(&:simplify)
71
+ end
72
+
73
+ # ---- the length element ---------------------------------------------------
74
+
75
+ # |v|, with the perfect squares taken out of the root: sqrt(16*sin(v)**2)
76
+ # is 4*sin(v) on 0..pi. The sign of such a factor is decided on the
77
+ # parameter range (ranges is [[var, from, to], ...]) and abs(...) is the
78
+ # answer when it changes there, so nothing is assumed silently.
79
+ def norm(parts, ranges = [])
80
+ radicand = parts.map { |c| Expression.lift(c)**2 }.reduce(:+).simplify
81
+ radicand = shorter(radicand, Trigonometry.trigsimp(radicand.expand)) if trigonometric?(radicand)
82
+ coeff, factors = Simplify.factorize(radicand)
83
+ outside = Num.new(1)
84
+ inside = {}
85
+ factors.each do |base, exponent|
86
+ half = exponent.is_a?(Integer) && exponent.even? ? exponent / 2 : nil
87
+ if half.nil?
88
+ inside[base] = exponent
89
+ else
90
+ outside *= (half.even? ? base : root_factor(base, ranges))**half
91
+ end
92
+ end
93
+ (outside * Analysis.root(Simplify.rebuild_product(coeff, inside))).simplify
94
+ end
95
+
96
+ TRIGONOMETRIC = (Trigonometry::SQUARES.keys + %i[tan tanh]).freeze
97
+
98
+ def trigonometric?(expr) = expr.each_node.any? { |n| n.is_a?(Fn) && TRIGONOMETRIC.include?(n.name) }
99
+
100
+ def shorter(a, b) = [a, b].min_by { |e| [e.each_node.count, e.to_s.size] }
101
+
102
+ # sqrt(u**2) is u, -u or abs(u), by the sign of u on the parameter
103
+ # ranges - a sign that is proved, never sampled.
104
+ def root_factor(base, ranges)
105
+ case proven_sign(base, ranges) || real_sign(base, ranges)
106
+ when :positive, :nonnegative then base
107
+ when :negative, :nonpositive then Neg.new(base).simplify
108
+ else Fn.new(:abs, [base])
109
+ end
110
+ end
111
+
112
+ # The sign that needs no range: 4*u**2 + 4*v**2 is not negative for the
113
+ # real parameters of any range (the abs kept it formal: fourth review),
114
+ # and on a box the enclosure of the base may be one-signed.
115
+ def real_sign(base, ranges)
116
+ names = ranges.map { |var, _, _| Expression.lift(var).name }
117
+ sign = RCAS.assume(**names.to_h { |n| [n, RR] }) { RCAS.sign_of(base) }
118
+ return sign if sign
119
+ ranges.empty? ? nil : Analysis.sign_on_box(base, ranges)
120
+ end
121
+
122
+ # In one variable the zeros decide (Analysis.sign_on_interval). On a box
123
+ # of several ranges the sign is proved factor by factor: a product whose
124
+ # every factor moves with one range at most has the sign of the product
125
+ # of theirs. Anything else keeps its abs - sampling five points of the
126
+ # box said x + y - 1/10 was positive on the unit square, and it is not
127
+ # in the corner (third review, T2).
128
+ def proven_sign(base, ranges)
129
+ base = Expression.lift(base)
130
+ one = single_range(base, ranges)
131
+ return Analysis.sign_on_interval(base, one[0], one[1], one[2]) if one
132
+ boxed = ranges.empty? ? nil : Analysis.sign_on_box(base, ranges)
133
+ return boxed if %i[positive negative].include?(boxed)
134
+ coeff, factors = Simplify.factorize(base)
135
+ return nil unless coeff.is_a?(Numeric) && coeff.real? && !coeff.zero?
136
+ negative = coeff.negative?
137
+ factors.each do |factor, exponent|
138
+ sign = proven_sign_of_factor(factor, ranges)
139
+ return nil if sign.nil?
140
+ next if exponent.is_a?(Integer) && exponent.even?
141
+ return nil unless exponent.is_a?(Integer) || sign == :positive
142
+ negative = !negative if sign == :negative
143
+ end
144
+ negative ? :negative : :positive
145
+ end
146
+
147
+ def proven_sign_of_factor(factor, ranges)
148
+ return Analysis.constant_sign(factor) if factor.variables.empty?
149
+ one = single_range(factor, ranges)
150
+ one ? Analysis.sign_on_interval(factor, one[0], one[1], one[2]) : nil
151
+ end
152
+
153
+ # The one range the expression actually moves with, when there is one.
154
+ def single_range(base, ranges)
155
+ wanted = ranges.select { |var, _, _| Expression.lift(base).variables.include?(Expression.lift(var).name) }
156
+ wanted.size == 1 && Expression.lift(base).variables.size == 1 ? wanted.first : nil
157
+ end
158
+
159
+ # ---- line integrals -------------------------------------------------------
160
+
161
+ # A scalar field integrated against ds = |r'(t)| dt, a vector field
162
+ # against dr = r'(t) dt (the work it does along the curve).
163
+ def line_integral(integrand, curve, var, from, to, vars: nil)
164
+ r = components(curve, "line_integral", [2, 3])
165
+ t = Expression.lift(var)
166
+ dr = r.map { |c| c.diff(t) }
167
+ coords = coordinates(integrand, vars, r.size)
168
+ integrand =
169
+ if vector?(integrand)
170
+ f = components(integrand, "line_integral", [r.size])
171
+ dot(f.map { |c| along(c, coords, r) }, dr)
172
+ else
173
+ (along(integrand, coords, r) * norm(dr, [[t, from, to]])).simplify
174
+ end
175
+ Integrate.definite(integrand, t, from, to)
176
+ end
177
+
178
+ # The flux of a plane field across a curve: the integral of F . n ds with
179
+ # n = (y', -x')/|r'|, which points to the right of the direction of
180
+ # travel (outwards for a curve run anticlockwise).
181
+ def curve_flux(field, curve, var, from, to, vars: nil)
182
+ r = components(curve, "flux", [2])
183
+ f = components(field, "flux", [2])
184
+ t = Expression.lift(var)
185
+ coords = coordinates(field, vars, 2)
186
+ p, q = f.map { |c| along(c, coords, r) }
187
+ Integrate.definite((p * r[1].diff(t) - q * r[0].diff(t)).simplify, t, from, to)
188
+ end
189
+
190
+ # The area a closed plane curve encloses, by Green's theorem:
191
+ # 1/2 * integral(x*y' - y*x'), positive when the curve runs anticlockwise.
192
+ def enclosed_area(curve, var, from, to)
193
+ r = components(curve, "enclosed_area", [2])
194
+ t = Expression.lift(var)
195
+ Integrate.definite(((r[0] * r[1].diff(t) - r[1] * r[0].diff(t)) / 2).simplify, t, from, to)
196
+ end
197
+
198
+ # ---- surface integrals ----------------------------------------------------
199
+
200
+ # A scalar field against dS = |r_u x r_v| du dv, a vector field against
201
+ # the vector element (r_u x r_v) du dv, whose direction is the normal
202
+ # the parametrization orients. `ranges` is [[u, from, to], [v, from, to]]
203
+ # and they are integrated in that order, the first one innermost.
204
+ def surface_integral(integrand, surface, ranges, vars: nil)
205
+ r = components(surface, "surface_integral", [3])
206
+ normal = surface_normal(r, ranges)
207
+ coords = coordinates(integrand, vars, 3)
208
+ integrand =
209
+ if vector?(integrand)
210
+ f = components(integrand, "surface_integral", [3])
211
+ dot(f.map { |c| along(c, coords, r) }, normal)
212
+ else
213
+ (along(integrand, coords, r) * norm(normal, ranges)).simplify
214
+ end
215
+ integrate_over(integrand, ranges)
216
+ end
217
+
218
+ # r_u x r_v: the vector surface element, normal to the surface and as
219
+ # long as the area the parameters sweep out.
220
+ def surface_normal(r, ranges)
221
+ u, v = ranges.first(2).map { |range| Expression.lift(range.first) }
222
+ cross(r.map { |c| c.diff(u) }, r.map { |c| c.diff(v) })
223
+ end
224
+
225
+ # The ranges innermost first: an inner range may depend on the outer
226
+ # variables, never an outer range on an inner one - y: 0..x with x
227
+ # innermost is no region, and the answer kept an integration variable
228
+ # (third review, L14).
229
+ def integrate_over(integrand, ranges)
230
+ check_nesting!(ranges)
231
+ ranges.reduce(Expression.lift(integrand)) do |acc, (var, from, to)|
232
+ Integrate.definite(acc, Expression.lift(var), from, to)
233
+ end
234
+ end
235
+
236
+ def check_nesting!(ranges)
237
+ names = ranges.map { |var, _, _| Expression.lift(var).name }
238
+ ranges.each_with_index do |(var, from, to), i|
239
+ inner = names.first(i)
240
+ used = [from, to].flat_map { |b| Expression.lift(b).variables } & inner
241
+ next if used.empty?
242
+ raise ArgumentError, "the range of #{var} depends on #{used.join(', ')}, which is integrated first; " \
243
+ "list the ranges innermost first, each depending only on the ones after it"
244
+ end
245
+ end
246
+
247
+ # A field with a singularity inside the region is outside the
248
+ # theorems: the vortex (-y, x)/(x**2 + y**2) has zero curl everywhere it
249
+ # is defined and circulates 2*pi around the origin (third review, L13).
250
+ # Refused unless every denominator of the field is shown not to vanish
251
+ # on the region - a sign under real coordinates, or no zero in the one
252
+ # range it moves with.
253
+ def regular_on!(field, xs, ranges, name)
254
+ list(field).each do |component|
255
+ Analysis.denominators(Expression.lift(component), xs.first).concat(
256
+ xs.drop(1).flat_map { |x| Analysis.denominators(Expression.lift(component), x) }
257
+ ).uniq.each do |d|
258
+ next if nonvanishing?(d, xs, ranges)
259
+ raise RCAS::Unsupported, "#{name}: the field is singular where #{d} = 0, which may meet the region; the theorem needs a field without singularities there"
260
+ end
261
+ end
262
+ end
263
+
264
+ def nonvanishing?(d, xs, ranges)
265
+ sign = RCAS.assume(**xs.to_h { |x| [x.name, RR] }) { RCAS.sign_of(d) }
266
+ return true if %i[positive negative].include?(sign)
267
+ # x**2 + y**2 >= 2 on [1, 2]x[1, 2]: the enclosure proves it (the
268
+ # vortex on that square was refused: fourth review)
269
+ !ranges.empty? && (%i[positive negative].include?(proven_sign(d, ranges)) || %i[positive negative].include?(Analysis.sign_on_box(d, ranges)))
270
+ rescue StandardError => rescued
271
+ RCAS.guard!(rescued)
272
+ false
273
+ end
274
+
275
+ # ---- the three theorems ---------------------------------------------------
276
+
277
+ # Green: the circulation of [P, Q] around the boundary of a plane region
278
+ # is the double integral of Q_x - P_y over the region. The region is the
279
+ # two ranges, innermost first, so the inner bounds may depend on the
280
+ # outer variable.
281
+ def green(field, ranges, vars: nil)
282
+ f = components(field, "green", [2])
283
+ xs = region_coordinates(ranges, vars, 2, "green")
284
+ check_nesting!(ranges)
285
+ regular_on!(f, xs, ranges, "green")
286
+ integrate_over((f[1].diff(xs[0]) - f[0].diff(xs[1])).simplify, ranges)
287
+ end
288
+
289
+ # Stokes: the circulation of a field around the edge of a surface is the
290
+ # flux of its curl through the surface.
291
+ def stokes(field, surface, ranges, vars: nil)
292
+ f = components(field, "stokes", [3])
293
+ xs = coordinates(field, vars, 3)
294
+ surface_integral(Analysis.curl(f, xs).to_a, surface, ranges, vars: xs)
295
+ end
296
+
297
+ # Gauss: the flux of a field out of the boundary of a solid is the
298
+ # triple integral of its divergence over the solid, the three ranges
299
+ # again innermost first.
300
+ def divergence_theorem(field, ranges, vars: nil)
301
+ f = components(field, "divergence_theorem", [3])
302
+ xs = region_coordinates(ranges, vars, 3, "divergence_theorem")
303
+ check_nesting!(ranges)
304
+ regular_on!(f, xs, ranges, "divergence_theorem")
305
+ integrate_over(Analysis.divergence(f, xs), ranges)
306
+ end
307
+
308
+ # For a region the coordinates are the ones the ranges name, in
309
+ # alphabetical order, so that [P, Q] belongs to x, y and not to the order
310
+ # of integration.
311
+ def region_coordinates(ranges, vars, dim, name)
312
+ return Array(vars).map { |v| Expression.lift(v) } if vars && !Array(vars).empty?
313
+ names = ranges.map { |var, _, _| Expression.lift(var).name }
314
+ raise ArgumentError, "#{name}: give #{dim} ranges" unless names.size == dim
315
+ names.sort.map { |n| Var.new(n) }
316
+ end
317
+
318
+ # ---- conservative fields --------------------------------------------------
319
+
320
+ # A field is conservative when it is the gradient of a potential; on a
321
+ # region without holes that is exactly the symmetry of the derivatives
322
+ # (curl = 0 in three variables, Q_x = P_y in two).
323
+ #
324
+ # "Without holes" is the part that is easy to forget: a field with a
325
+ # singularity has a hole in its domain, and its symmetric derivatives
326
+ # prove nothing there, so such a field is refused rather than
327
+ # certified.
328
+ def conservative?(field, vars = nil)
329
+ f = components(field, "conservative?", [2, 3])
330
+ xs = coordinates(field, vars, f.size)
331
+ return symmetric?(f, xs) if regular?(f, xs)
332
+ # a field with a singularity is conservative on its domain when it has
333
+ # a potential there: grad(1/r) is, the vortex is not
334
+ return true if symmetric?(f, xs) && global_potential(f, xs)
335
+ regular_on!(f, xs, [], "conservative?")
336
+ end
337
+
338
+ def symmetric?(f, xs)
339
+ (0...f.size).to_a.combination(2).all? { |i, j| Scalar.zero?((f[i].diff(xs[j]) - f[j].diff(xs[i])).simplify) }
340
+ end
341
+
342
+ def regular?(f, xs)
343
+ regular_on!(f, xs, [], "conservative?")
344
+ true
345
+ rescue NotImplementedError, RCAS::Unsupported
346
+ false
347
+ end
348
+
349
+ # The potential f with gradient f = field, up to a constant: integrate
350
+ # the first component, then correct it with what the next components
351
+ # still miss. Raises when the field has no potential. A potential found
352
+ # is its own proof, whatever the topology of the domain - checked by
353
+ # differentiating, and by asking that it be singular only where the
354
+ # field is: (2x, 2y)/(x**2 + y**2) has log(x**2 + y**2) (refused as
355
+ # singular before: fourth review), while the vortex's -atan(x/y) breaks
356
+ # on the line y = 0, where the field is smooth.
357
+ def potential(field, vars = nil)
358
+ f = components(field, "potential", [2, 3])
359
+ xs = coordinates(field, vars, f.size)
360
+ raise ArgumentError, "potential: (#{f.join(', ')}) is not conservative" unless symmetric?(f, xs)
361
+ return construct_potential(f, xs) if regular?(f, xs)
362
+ global_potential(f, xs) or regular_on!(f, xs, [], "potential")
363
+ end
364
+
365
+ def construct_potential(f, xs)
366
+ f.each_with_index.reduce(Num.new(0)) do |found, (component, i)|
367
+ missing = (component - found.diff(xs[i])).simplify
368
+ antiderivative = Integrate.integrate(missing, xs[i])
369
+ raise ArgumentError, "potential: cannot integrate #{missing} with respect to #{xs[i]}" if antiderivative.is_a?(Integral)
370
+ (found + antiderivative).simplify
371
+ end
372
+ end
373
+
374
+ # A potential of a singular field that is defined wherever the field
375
+ # is, with the field as its gradient; nil otherwise.
376
+ def global_potential(f, xs)
377
+ phi = construct_potential(f, xs)
378
+ gradient = xs.each_with_index.all? { |x, i| Scalar.zero?((phi.diff(x) - f[i]).simplify) }
379
+ gradient && singular_within?(phi, f, xs) ? phi : nil
380
+ rescue ArgumentError
381
+ nil
382
+ end
383
+
384
+ # Every place the potential may break - a denominator, the argument of
385
+ # a log, a radicand - is a zero of the field's denominators: each of its
386
+ # irreducible factors is one of theirs.
387
+ def singular_within?(phi, f, xs)
388
+ field_factors = f.flat_map { |c| xs.flat_map { |x| Analysis.denominators(Expression.lift(c), x) } }
389
+ .flat_map { |d| irreducible_factors(d, xs) }.uniq
390
+ breaks = xs.flat_map { |x| Analysis.denominators(phi, x) } +
391
+ phi.each_node.select { |n| n.is_a?(Fn) && n.name == :log }.map { |n| n.args.first } +
392
+ phi.each_node.select { |n| n.is_a?(Pow) && n.exponent.is_a?(Num) && !n.exponent.value.is_a?(Integer) }.map(&:base) +
393
+ phi.each_node.select { |n| n.is_a?(Fn) && !%i[log exp sin cos].include?(n.name) }.flat_map(&:args)
394
+ breaks.uniq.all? do |b|
395
+ next true if b.variables.empty?
396
+ irreducible_factors(b, xs).all? { |factor| field_factors.any? { |g| Scalar.zero?((factor - g).simplify) || Scalar.zero?((factor + g).simplify) } }
397
+ end
398
+ end
399
+
400
+ def irreducible_factors(d, xs)
401
+ d = Expression.lift(d)
402
+ d = d.base while d.is_a?(Pow) && d.exponent.is_a?(Num) && d.exponent.value.positive? # (x**2 + y**2)**(3/2)
403
+ numerator = Fraction.as_fraction(d, xs.map(&:name))
404
+ return [Expression.lift(d)] if numerator.nil? || !numerator.last.constant?
405
+ Factor.factor(numerator.first).factors.map { |g, _| g.to_expr }.reject { |g| g.variables.empty? }
406
+ rescue StandardError, NotImplementedError, RCAS::Unsupported => rescued
407
+ RCAS.guard!(rescued, refused: true) if rescued.is_a?(StandardError)
408
+ [Expression.lift(d)]
409
+ end
410
+ end
411
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ VERSION = "0.2.0"
5
+ end