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/gcd.rb
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Polynomial gcd.
|
|
5
|
+
#
|
|
6
|
+
# * univariate over a field: Euclid, result monic
|
|
7
|
+
# * anything over ZZ or QQ (any number of variables): primitive
|
|
8
|
+
# pseudo-remainder sequences, recursing on the content in one variable
|
|
9
|
+
# at a time; result primitive with positive leading coefficient over ZZ,
|
|
10
|
+
# monic (leading coefficient 1 in graded lex order) over QQ
|
|
11
|
+
#
|
|
12
|
+
# Sources: primitive PRS [Knu98, §4.6.1, Algorithm E], [GCL92, ch. 7];
|
|
13
|
+
# extended Euclid [Knu98, §4.5.2, Algorithm X]. Keys: MANUAL.md, Sources.
|
|
14
|
+
module PolyGCD
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def gcd(f, g)
|
|
18
|
+
ring = f.ring
|
|
19
|
+
numeric!(f)
|
|
20
|
+
numeric!(g)
|
|
21
|
+
|
|
22
|
+
if ring.univariate? && ring.base.field? && !exact?(ring.base)
|
|
23
|
+
return euclid(f, g)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if ring.base == ZZ
|
|
27
|
+
gcd_zz(f, g)
|
|
28
|
+
elsif ring.base == QQ
|
|
29
|
+
zz = ZZ[*ring.vars]
|
|
30
|
+
result = gcd_zz(f.clear_denominators.to_ring(zz), g.clear_denominators.to_ring(zz))
|
|
31
|
+
result.to_ring(ring).monic
|
|
32
|
+
else
|
|
33
|
+
raise RCAS::Unsupported, "gcd over #{ring.base} needs a univariate ring"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def lcm(f, g)
|
|
38
|
+
return f.ring.zero if f.zero? || g.zero?
|
|
39
|
+
(f * g).exact_div(gcd(f, g))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Extended Euclid over a field: returns [g, s, t] with s*f + t*g = gcd, gcd monic.
|
|
43
|
+
def xgcd(f, g)
|
|
44
|
+
ring = f.ring
|
|
45
|
+
raise RCAS::Unsupported, "xgcd needs a univariate ring over a field" unless ring.univariate? && ring.base.field?
|
|
46
|
+
r0, r1 = f, g
|
|
47
|
+
s0, s1 = ring.one, ring.zero
|
|
48
|
+
t0, t1 = ring.zero, ring.one
|
|
49
|
+
until r1.zero?
|
|
50
|
+
q, r = r0.divmod(r1)
|
|
51
|
+
r0, r1 = r1, r
|
|
52
|
+
s0, s1 = s1, s0 - q * s1
|
|
53
|
+
t0, t1 = t1, t0 - q * t1
|
|
54
|
+
end
|
|
55
|
+
return [ring.zero, ring.zero, ring.zero] if r0.zero?
|
|
56
|
+
inv = Scalar.div(Num.new(1), r0.leading_coefficient)
|
|
57
|
+
[r0 * inv, s0 * inv, t0 * inv]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# ---- internals --------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def exact?(base) = base == ZZ || base == QQ
|
|
63
|
+
|
|
64
|
+
def numeric!(f)
|
|
65
|
+
return if f.terms.values.all? { |c| c.is_a?(Num) && (c.value.is_a?(Integer) || c.value.is_a?(Rational) || !exact?(f.ring.base)) }
|
|
66
|
+
raise DomainError, "gcd needs numeric coefficients, got #{f}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def euclid(a, b)
|
|
70
|
+
a, b = b, a % b until b.zero?
|
|
71
|
+
a.monic
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def gcd_zz(f, g)
|
|
75
|
+
return g.primitive_part if f.zero?
|
|
76
|
+
return f.primitive_part if g.zero?
|
|
77
|
+
|
|
78
|
+
x = (f.ring.vars.find { |v| f.degree(v).positive? || g.degree(v).positive? })
|
|
79
|
+
if x.nil?
|
|
80
|
+
return Polynomial.constant(f.ring, f.constant_term.value.gcd(g.constant_term.value))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
cf = content_in(f, x)
|
|
84
|
+
cg = content_in(g, x)
|
|
85
|
+
c = gcd_zz(cf, cg)
|
|
86
|
+
a = f.exact_div(cf)
|
|
87
|
+
b = g.exact_div(cg)
|
|
88
|
+
until b.zero?
|
|
89
|
+
r = prem(a, b, x)
|
|
90
|
+
a, b = b, (r.zero? ? r : r.exact_div(content_in(r, x)))
|
|
91
|
+
end
|
|
92
|
+
c * a.exact_div(content_in(a, x)).primitive_part
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# gcd of the coefficients of f seen as a polynomial in x.
|
|
96
|
+
def content_in(f, x)
|
|
97
|
+
coeffs = (0..f.degree(x)).map { |k| f.coefficient_in(x, k) }.reject(&:zero?)
|
|
98
|
+
coeffs.reduce { |acc, c| gcd_zz(acc, c) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Pseudo-remainder of a by b with respect to x (no fractions introduced).
|
|
102
|
+
def prem(a, b, x)
|
|
103
|
+
db = b.degree(x)
|
|
104
|
+
lb = b.leading_coefficient_in(x)
|
|
105
|
+
r = a
|
|
106
|
+
while !r.zero? && (dr = r.degree(x)) >= db
|
|
107
|
+
r = lb * r - r.leading_coefficient_in(x) * b.shift(x, dr - db)
|
|
108
|
+
end
|
|
109
|
+
r
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Plane geometry with exact coordinates: points, lines and circles, and the
|
|
5
|
+
# questions a school course asks about them.
|
|
6
|
+
#
|
|
7
|
+
# a = point(0, 0); b = point(4, 0); c = point(0, 3)
|
|
8
|
+
# distance(a, b) # => 4
|
|
9
|
+
# distance(c, line(a, b)) # => 3 (the distance to a line)
|
|
10
|
+
# angle(b, a, c) # => pi/2
|
|
11
|
+
# area(a, b, c) # => 6
|
|
12
|
+
# intersect(line(a, c), circle(a, 2))
|
|
13
|
+
# perpendicular_bisector(a, b)
|
|
14
|
+
#
|
|
15
|
+
# Everything is exact: a distance is a square root, an angle a multiple of
|
|
16
|
+
# pi where the cosine is a known value, and a float only when you ask with
|
|
17
|
+
# evalf. Coordinates may be symbolic.
|
|
18
|
+
#
|
|
19
|
+
# Sources (keys: MANUAL.md, Sources): analytic geometry as in [Spi08,
|
|
20
|
+
# ch. 4]; the shoelace formula for the area [Bra86].
|
|
21
|
+
module Geometry
|
|
22
|
+
class Error < ArgumentError; end
|
|
23
|
+
|
|
24
|
+
# A point of the plane.
|
|
25
|
+
class Point
|
|
26
|
+
attr_reader :x, :y
|
|
27
|
+
|
|
28
|
+
def initialize(x, y)
|
|
29
|
+
@x = Expression.lift(x).simplify
|
|
30
|
+
@y = Expression.lift(y).simplify
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def coordinates = [x, y]
|
|
35
|
+
def +(other) = Point.new(x + other.x, y + other.y)
|
|
36
|
+
def -(other) = Point.new(x - other.x, y - other.y)
|
|
37
|
+
def *(factor) = Point.new(x * factor, y * factor)
|
|
38
|
+
def to_vector = VectorSpace.new(RR, 2).unchecked([x, y])
|
|
39
|
+
def to_s = "(#{x}, #{y})"
|
|
40
|
+
def inspect = to_s
|
|
41
|
+
def to_latex(wrap: nil) = "\\left(#{LaTeX.of(x)},\; #{LaTeX.of(y)}\\right)"
|
|
42
|
+
def ==(other) = other.is_a?(Point) && Scalar.zero?((x - other.x).simplify) && Scalar.zero?((y - other.y).simplify)
|
|
43
|
+
alias eql? ==
|
|
44
|
+
def hash = [Point, x, y].hash
|
|
45
|
+
def evalf = Point.new(Num.new(x.evalf), Num.new(y.evalf))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The line a*x + b*y + c = 0, kept in that form so that vertical lines
|
|
49
|
+
# need no special case.
|
|
50
|
+
class Line
|
|
51
|
+
attr_reader :a, :b, :c
|
|
52
|
+
|
|
53
|
+
def initialize(a, b, c)
|
|
54
|
+
a = Expression.lift(a).simplify
|
|
55
|
+
b = Expression.lift(b).simplify
|
|
56
|
+
c = Expression.lift(c).simplify
|
|
57
|
+
raise Error, "a line needs a direction: a and b cannot both be zero" if Scalar.zero?(a) && Scalar.zero?(b)
|
|
58
|
+
@a, @b, @c = normalize(a, b, c)
|
|
59
|
+
freeze
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Scale so that the first non-zero coefficient is positive and, when the
|
|
63
|
+
# coefficients are rational, integral and coprime.
|
|
64
|
+
def normalize(a, b, c)
|
|
65
|
+
values = [a, b, c]
|
|
66
|
+
if values.all? { |v| v.is_a?(Num) && (v.value.is_a?(Integer) || v.value.is_a?(Rational)) }
|
|
67
|
+
rationals = values.map { |v| Rational(v.value) }
|
|
68
|
+
scale = rationals.map(&:denominator).reduce(1, :lcm)
|
|
69
|
+
integers = rationals.map { |r| (r * scale).to_i }
|
|
70
|
+
divisor = integers.reject(&:zero?).map(&:abs).reduce(0, :gcd)
|
|
71
|
+
integers = integers.map { |i| i / divisor } unless divisor.zero?
|
|
72
|
+
integers = integers.map(&:-@) if (integers.find { |i| !i.zero? } || 0).negative?
|
|
73
|
+
return integers.map { |i| Num.new(i) }
|
|
74
|
+
end
|
|
75
|
+
values
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.through(p, q)
|
|
79
|
+
raise Error, "the two points are the same" if p == q
|
|
80
|
+
a = q.y - p.y
|
|
81
|
+
b = p.x - q.x
|
|
82
|
+
new(a, b, -(a * p.x + b * p.y))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.point_slope(p, slope)
|
|
86
|
+
slope = Expression.lift(slope)
|
|
87
|
+
new(slope, -1, p.y - slope * p.x)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def slope = Scalar.zero?(b) ? nil : (-a / b).simplify # nil: vertical
|
|
91
|
+
def intercept = Scalar.zero?(b) ? nil : (-c / b).simplify
|
|
92
|
+
def direction = [(-b).simplify, a]
|
|
93
|
+
def normal_vector = [a, b]
|
|
94
|
+
def equation(x = :x, y = :y) = Equation.new((a * Expression.lift(x) + b * Expression.lift(y) + c).simplify, 0)
|
|
95
|
+
def contains?(p) = Scalar.zero?((a * p.x + b * p.y + c).simplify)
|
|
96
|
+
def parallel?(other) = Scalar.zero?((a * other.b - b * other.a).simplify)
|
|
97
|
+
def perpendicular?(other) = Scalar.zero?((a * other.a + b * other.b).simplify)
|
|
98
|
+
|
|
99
|
+
# The point of the line closest to p.
|
|
100
|
+
def foot(p)
|
|
101
|
+
t = ((a * p.x + b * p.y + c) / (a**2 + b**2)).simplify
|
|
102
|
+
Point.new(p.x - a * t, p.y - b * t)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def to_s = equation.to_s
|
|
106
|
+
def inspect = to_s
|
|
107
|
+
def to_latex(wrap: nil) = "#{LaTeX.of(equation)}"
|
|
108
|
+
def ==(other) = other.is_a?(Line) && parallel?(other) && Scalar.zero?((a * other.c - c * other.a).simplify) && Scalar.zero?((b * other.c - c * other.b).simplify)
|
|
109
|
+
alias eql? ==
|
|
110
|
+
def hash = [Line, a, b, c].hash
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class Circle
|
|
114
|
+
attr_reader :center, :radius
|
|
115
|
+
|
|
116
|
+
def initialize(center, radius)
|
|
117
|
+
@center = center
|
|
118
|
+
@radius = Expression.lift(radius).simplify
|
|
119
|
+
raise Error, "a circle needs a positive radius" if @radius.is_a?(Num) && !@radius.value.positive?
|
|
120
|
+
freeze
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def equation(x = :x, y = :y)
|
|
124
|
+
x = Expression.lift(x)
|
|
125
|
+
y = Expression.lift(y)
|
|
126
|
+
Equation.new(((x - center.x)**2 + (y - center.y)**2).simplify, (radius**2).simplify)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def area = (PI * radius**2).simplify
|
|
130
|
+
def circumference = (2 * PI * radius).simplify
|
|
131
|
+
def contains?(p) = Scalar.zero?(((p.x - center.x)**2 + (p.y - center.y)**2 - radius**2).expand)
|
|
132
|
+
def to_s = "circle(#{center}, #{radius})"
|
|
133
|
+
def inspect = to_s
|
|
134
|
+
def to_latex(wrap: nil) = LaTeX.of(equation)
|
|
135
|
+
def ==(other) = other.is_a?(Circle) && center == other.center && Scalar.zero?((radius - other.radius).simplify)
|
|
136
|
+
alias eql? ==
|
|
137
|
+
def hash = [Circle, center, radius].hash
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
module_function
|
|
141
|
+
|
|
142
|
+
def point(x, y = nil)
|
|
143
|
+
return x if x.is_a?(Point)
|
|
144
|
+
return Point.new(x[0], x[1]) if x.is_a?(Array)
|
|
145
|
+
Point.new(x, y)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def line(first, second = nil, slope: nil)
|
|
149
|
+
return first if first.is_a?(Line) && second.nil?
|
|
150
|
+
p = point(first)
|
|
151
|
+
return Line.point_slope(p, slope) if slope
|
|
152
|
+
Line.through(p, point(second))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def circle(center, radius) = Circle.new(point(center), radius)
|
|
156
|
+
|
|
157
|
+
def midpoint(p, q) = Point.new(((p.x + q.x) / 2).simplify, ((p.y + q.y) / 2).simplify)
|
|
158
|
+
|
|
159
|
+
# Point to point, point to line (either order), or between parallel lines.
|
|
160
|
+
def distance(a, b)
|
|
161
|
+
return point_distance(a, b) if a.is_a?(Point) && b.is_a?(Point)
|
|
162
|
+
return line_distance(b, a) if a.is_a?(Line) && b.is_a?(Point)
|
|
163
|
+
return line_distance(a, b) if a.is_a?(Point) && b.is_a?(Line)
|
|
164
|
+
if a.is_a?(Line) && b.is_a?(Line)
|
|
165
|
+
raise Error, "the lines meet, so their distance is zero only there" unless a.parallel?(b)
|
|
166
|
+
return line_distance(point_on(b), a)
|
|
167
|
+
end
|
|
168
|
+
raise Error, "distance: give two points, a point and a line, or two parallel lines"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def point_distance(p, q) = RCAS.sqrt(((q.x - p.x)**2 + (q.y - p.y)**2).expand).simplify
|
|
172
|
+
|
|
173
|
+
def line_distance(p, l) = (RCAS.abs(l.a * p.x + l.b * p.y + l.c) / RCAS.sqrt((l.a**2 + l.b**2).expand)).simplify
|
|
174
|
+
|
|
175
|
+
def point_on(l)
|
|
176
|
+
Scalar.zero?(l.b) ? Point.new((-l.c / l.a).simplify, 0) : Point.new(0, (-l.c / l.b).simplify)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The angle at b in the corner a-b-c, exact where the cosine is known.
|
|
180
|
+
def angle(a, b, c)
|
|
181
|
+
u = [a.x - b.x, a.y - b.y]
|
|
182
|
+
v = [c.x - b.x, c.y - b.y]
|
|
183
|
+
dot = (u[0] * v[0] + u[1] * v[1]).expand
|
|
184
|
+
norms = RCAS.sqrt((u[0]**2 + u[1]**2).expand) * RCAS.sqrt((v[0]**2 + v[1]**2).expand)
|
|
185
|
+
raise Error, "angle: the corner point coincides with another" if Scalar.zero?(norms.simplify)
|
|
186
|
+
RCAS.acos((dot / norms).simplify)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# The angle between two lines, in [0, pi/2].
|
|
190
|
+
def line_angle(l, m)
|
|
191
|
+
cosine = (RCAS.abs(l.a * m.a + l.b * m.b) / (RCAS.sqrt((l.a**2 + l.b**2).expand) * RCAS.sqrt((m.a**2 + m.b**2).expand))).simplify
|
|
192
|
+
RCAS.acos(cosine)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Triangle area by the shoelace formula, or the area of a circle.
|
|
196
|
+
def area(a, b = nil, c = nil)
|
|
197
|
+
return a.area if a.is_a?(Circle)
|
|
198
|
+
(RCAS.abs(((b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)).expand) / 2).simplify
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def perimeter(a, b = nil, c = nil)
|
|
202
|
+
return a.circumference if a.is_a?(Circle)
|
|
203
|
+
(point_distance(a, b) + point_distance(b, c) + point_distance(c, a)).simplify
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def collinear?(a, b, c) = Scalar.zero?(((b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)).expand)
|
|
207
|
+
|
|
208
|
+
def centroid(*points)
|
|
209
|
+
points = points.flatten
|
|
210
|
+
Point.new(points.map(&:x).reduce(:+) / points.size, points.map(&:y).reduce(:+) / points.size)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def perpendicular_bisector(p, q)
|
|
214
|
+
m = midpoint(p, q)
|
|
215
|
+
Line.new(q.x - p.x, q.y - p.y, (-((q.x - p.x) * m.x + (q.y - p.y) * m.y)).expand)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def parallel_through(l, p) = Line.new(l.a, l.b, (-(l.a * p.x + l.b * p.y)).expand)
|
|
219
|
+
def perpendicular_through(l, p) = Line.new(l.b, -l.a, (-(l.b * p.x - l.a * p.y)).expand)
|
|
220
|
+
|
|
221
|
+
# The circle through three points that do not lie on a line.
|
|
222
|
+
def circumcircle(a, b, c)
|
|
223
|
+
raise Error, "the three points lie on a line" if collinear?(a, b, c)
|
|
224
|
+
centre = intersect(perpendicular_bisector(a, b), perpendicular_bisector(b, c))
|
|
225
|
+
Circle.new(centre, point_distance(centre, a))
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# => a Point, an Array of points, nil (nothing), or the shared line or circle.
|
|
229
|
+
def intersect(first, second)
|
|
230
|
+
case [first, second]
|
|
231
|
+
in [Line, Line] then line_line(first, second)
|
|
232
|
+
in [Line, Circle] then line_circle(first, second)
|
|
233
|
+
in [Circle, Line] then line_circle(second, first)
|
|
234
|
+
in [Circle, Circle] then circle_circle(first, second)
|
|
235
|
+
else raise Error, "intersect: give two lines, a line and a circle, or two circles"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def line_line(l, m)
|
|
240
|
+
determinant = (l.a * m.b - m.a * l.b).simplify
|
|
241
|
+
return (l == m ? l : nil) if Scalar.zero?(determinant)
|
|
242
|
+
Point.new(((l.b * m.c - m.b * l.c) / determinant).simplify, ((m.a * l.c - l.a * m.c) / determinant).simplify)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def line_circle(l, k)
|
|
246
|
+
foot = l.foot(k.center)
|
|
247
|
+
gap = (k.radius**2 - point_distance(foot, k.center)**2).simplify
|
|
248
|
+
value = gap.is_a?(Num) ? gap.value : gap.evalf
|
|
249
|
+
return [] if value.is_a?(Numeric) && value.negative?
|
|
250
|
+
return [foot] if Scalar.zero?(gap)
|
|
251
|
+
step = (RCAS.sqrt(gap) / RCAS.sqrt((l.a**2 + l.b**2).expand)).simplify
|
|
252
|
+
direction = l.direction
|
|
253
|
+
[Point.new((foot.x + direction[0] * step).simplify, (foot.y + direction[1] * step).simplify),
|
|
254
|
+
Point.new((foot.x - direction[0] * step).simplify, (foot.y - direction[1] * step).simplify)]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Subtracting the two circle equations leaves the radical line.
|
|
258
|
+
def circle_circle(k, l)
|
|
259
|
+
return (k == l ? k : nil) if k.center == l.center
|
|
260
|
+
radical = Line.new((2 * (l.center.x - k.center.x)).simplify,
|
|
261
|
+
(2 * (l.center.y - k.center.y)).simplify,
|
|
262
|
+
(k.center.x**2 + k.center.y**2 - l.center.x**2 - l.center.y**2 - k.radius**2 + l.radius**2).expand)
|
|
263
|
+
line_circle(radical, k)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Gröbner bases.
|
|
5
|
+
#
|
|
6
|
+
# G = groebner([x**2 + y**2 - 1, x - y], [x, y]) # reduced lex basis
|
|
7
|
+
# reduce(x**3, G, [x, y]) # normal form; 0 iff x**3 is in the ideal
|
|
8
|
+
#
|
|
9
|
+
# Buchberger's algorithm with the product criterion (coprime leading
|
|
10
|
+
# monomials give nothing new), normal forms by the multivariate division
|
|
11
|
+
# algorithm, and the basis made reduced (minimal, inter-reduced, monic) at
|
|
12
|
+
# the end. Monomial orders: :lex (the default, for elimination and
|
|
13
|
+
# solving), :grlex, :grevlex; the variables are compared in the order given.
|
|
14
|
+
#
|
|
15
|
+
# Sources (keys: MANUAL.md, Sources): [Buc65]; [CLO15, ch. 2 §§3, 7, 9-10;
|
|
16
|
+
# ch. 3 §1; ch. 5 §3]; [GCL92, ch. 10].
|
|
17
|
+
module Groebner
|
|
18
|
+
ORDERS = %i[lex grlex grevlex].freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# ---- monomial orders -------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
def order_key(exps, order)
|
|
25
|
+
case order
|
|
26
|
+
when :lex then exps
|
|
27
|
+
when :grlex then [exps.sum, exps]
|
|
28
|
+
when :grevlex then [exps.sum, exps.reverse.map(&:-@)]
|
|
29
|
+
else raise ArgumentError, "unknown monomial order #{order.inspect}; use one of #{ORDERS.join(', ')}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# => [exponent vector, coefficient] of the leading term
|
|
34
|
+
def leading(poly, order)
|
|
35
|
+
e = poly.terms.keys.max_by { |k| order_key(k, order) }
|
|
36
|
+
[e, poly.terms[e]]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def divides?(a, b) = a.zip(b).all? { |x, y| x <= y }
|
|
40
|
+
|
|
41
|
+
# ---- division and S-polynomials --------------------------------------------
|
|
42
|
+
|
|
43
|
+
# Normal form of f modulo the polynomials gs [CLO15, ch. 2 §3].
|
|
44
|
+
def reduce(f, gs, order = :lex)
|
|
45
|
+
ring = f.ring
|
|
46
|
+
gs = gs.reject(&:zero?)
|
|
47
|
+
leads = gs.map { |g| leading(g, order) }
|
|
48
|
+
remainder = {}
|
|
49
|
+
p = f
|
|
50
|
+
until p.zero?
|
|
51
|
+
ep, cp = leading(p, order)
|
|
52
|
+
i = leads.index { |eg, _| divides?(eg, ep) }
|
|
53
|
+
if i
|
|
54
|
+
eg, cg = leads[i]
|
|
55
|
+
shift = ep.zip(eg).map { |a, b| a - b }
|
|
56
|
+
p -= Polynomial.new(ring, { shift => Scalar.div(cp, cg) }) * gs[i]
|
|
57
|
+
else
|
|
58
|
+
remainder[ep] = cp
|
|
59
|
+
p -= Polynomial.new(ring, { ep => cp })
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
Polynomial.new(ring, remainder)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def s_polynomial(f, g, order)
|
|
66
|
+
ef, cf = leading(f, order)
|
|
67
|
+
eg, cg = leading(g, order)
|
|
68
|
+
l = ef.zip(eg).map(&:max)
|
|
69
|
+
mf = Polynomial.new(f.ring, { l.zip(ef).map { |a, b| a - b } => Scalar.div(Num.new(1), cf) })
|
|
70
|
+
mg = Polynomial.new(f.ring, { l.zip(eg).map { |a, b| a - b } => Scalar.div(Num.new(1), cg) })
|
|
71
|
+
mf * f - mg * g
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ---- Buchberger ----------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
# Reduced Gröbner basis of the ideal generated by polys (ring elements).
|
|
77
|
+
def basis(polys, order = :lex)
|
|
78
|
+
g = polys.reject(&:zero?)
|
|
79
|
+
return [] if g.empty?
|
|
80
|
+
ring = g.first.ring
|
|
81
|
+
g = g.map { |p| monic(p.ring == ring ? p : p.to_ring(ring), order) }
|
|
82
|
+
pairs = (0...g.size).to_a.combination(2).to_a
|
|
83
|
+
until pairs.empty?
|
|
84
|
+
i, j = pairs.shift
|
|
85
|
+
next if coprime?(leading(g[i], order).first, leading(g[j], order).first) # product criterion
|
|
86
|
+
s = reduce(s_polynomial(g[i], g[j], order), g, order)
|
|
87
|
+
next if s.zero?
|
|
88
|
+
g << s
|
|
89
|
+
k = g.size - 1
|
|
90
|
+
pairs.concat((0...k).map { |m| [m, k] })
|
|
91
|
+
return [ring.one] if s.constant?
|
|
92
|
+
end
|
|
93
|
+
interreduce(g, order)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def coprime?(a, b) = a.zip(b).all? { |x, y| x.zero? || y.zero? }
|
|
97
|
+
|
|
98
|
+
# Minimal (no leading monomial divides another), every element reduced
|
|
99
|
+
# modulo the others, monic; sorted by leading monomial, largest first.
|
|
100
|
+
def interreduce(g, order)
|
|
101
|
+
sorted = g.reject(&:zero?).sort_by { |p| order_key(leading(p, order).first, order) }
|
|
102
|
+
minimal = []
|
|
103
|
+
sorted.each do |p|
|
|
104
|
+
ep = leading(p, order).first
|
|
105
|
+
minimal << p unless minimal.any? { |q| divides?(leading(q, order).first, ep) }
|
|
106
|
+
end
|
|
107
|
+
reduced = minimal.dup
|
|
108
|
+
minimal.each_index do |i|
|
|
109
|
+
others = reduced.each_with_index.reject { |_, j| j == i }.map(&:first)
|
|
110
|
+
reduced[i] = monic(reduce(reduced[i], others, order), order)
|
|
111
|
+
end
|
|
112
|
+
reduced.sort_by { |p| order_key(leading(p, order).first, order) }.reverse
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def monic(p, order)
|
|
116
|
+
return p if p.zero?
|
|
117
|
+
_, c = leading(p, order)
|
|
118
|
+
Scalar.one?(c) ? p : p * Scalar.div(Num.new(1), c)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# ---- the ideal ------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
# Finitely many solutions? Every variable must be the leading monomial of
|
|
124
|
+
# some basis element as a pure power [CLO15, ch. 5 §3, Theorem 6].
|
|
125
|
+
def zero_dimensional?(basis, order = :lex)
|
|
126
|
+
return false if basis.empty?
|
|
127
|
+
n = basis.first.ring.vars.size
|
|
128
|
+
(0...n).all? do |i|
|
|
129
|
+
basis.any? do |p|
|
|
130
|
+
e = leading(p, order).first
|
|
131
|
+
e[i].positive? && e.each_with_index.all? { |v, j| j == i || v.zero? }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# ---- expression front end -----------------------------------------------------
|
|
137
|
+
|
|
138
|
+
# groebner([f, g], [x, y]) with expressions: the ring is QQ[x, y], or
|
|
139
|
+
# Frac(QQ[params])[x, y] when other symbols occur in the coefficients.
|
|
140
|
+
def groebner(polys, vars = nil, order: :lex)
|
|
141
|
+
_ring, elements = lift(polys, vars)
|
|
142
|
+
basis(elements, order)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def normal_form(f, basis, vars = nil, order: :lex)
|
|
146
|
+
_ring, elements = lift(basis + [f], vars)
|
|
147
|
+
reduce(elements.last, elements[0...-1], order)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def lift(polys, vars)
|
|
151
|
+
polys = Array(polys).map { |p| p.is_a?(Polynomial) ? p : Expression.lift(p) }
|
|
152
|
+
if vars.nil? || Array(vars).empty?
|
|
153
|
+
vars = polys.flat_map { |p| p.is_a?(Polynomial) ? p.ring.vars : p.variables }.uniq.sort
|
|
154
|
+
raise ArgumentError, "groebner: no variables in #{polys.map(&:to_s).join(', ')}" if vars.empty?
|
|
155
|
+
end
|
|
156
|
+
vars = Array(vars).map { |v| v.is_a?(Var) ? v.name : v.to_sym }
|
|
157
|
+
params = polys.flat_map { |p| p.is_a?(Polynomial) ? p.ring.vars : p.variables }.uniq - vars
|
|
158
|
+
ring = params.empty? ? QQ[*vars] : QQ[*params].fraction_field[*vars]
|
|
159
|
+
[ring, polys.map { |p| p.is_a?(Polynomial) ? p.to_ring(ring) : ring.call(p) }]
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|