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