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