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/random.rb
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
class << self
|
|
5
|
+
# The source of randomness every `random` uses when it is not handed one.
|
|
6
|
+
# RCAS.random = 42 (or a Random) makes a session reproducible.
|
|
7
|
+
def random = @random ||= Random.new
|
|
8
|
+
|
|
9
|
+
def random=(source)
|
|
10
|
+
@random = source.is_a?(Integer) ? Random.new(source) : source
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Random elements of the structures rcas already has: ZZ[x].random(3),
|
|
15
|
+
# (QQ**[2, 2]).random, (ZZ**3).random, ZZ.random(1..100), GF(9).random.
|
|
16
|
+
# Every one of them takes `random:` (a Ruby Random) and otherwise uses
|
|
17
|
+
# RCAS.random, so a session is reproducible by setting that once.
|
|
18
|
+
#
|
|
19
|
+
# The keywords say what the object should be, not how to build it:
|
|
20
|
+
#
|
|
21
|
+
# ZZ[x].random(4, irreducible: true)
|
|
22
|
+
# ZZ[x].random(3, roots: true) # one that really factors
|
|
23
|
+
# QQ[x, y].random(2, terms: 3)
|
|
24
|
+
# (ZZ**[3, 3]).random(unimodular: true) # det +-1, so the inverse is integral
|
|
25
|
+
# (ZZ**[3, 3]).random(eigenvalues: [1, 2, 2])
|
|
26
|
+
# (QQ**[3, 3]).random(definite: true) # a Cholesky factorization exists
|
|
27
|
+
#
|
|
28
|
+
# Where a property cannot be built directly it is sampled for and rejected
|
|
29
|
+
# (an irreducible polynomial, a matrix of full rank); after TRIES attempts
|
|
30
|
+
# the request is refused rather than looped on, because "no such object"
|
|
31
|
+
# and "unlucky" look the same from in here.
|
|
32
|
+
#
|
|
33
|
+
# Sources (keys: MANUAL.md, Sources): rejection sampling for irreducibles is
|
|
34
|
+
# the textbook use of their density 1/n [vzGG13, §14.9]; a unimodular matrix
|
|
35
|
+
# is a product of elementary ones and a positive definite one is L*L^T
|
|
36
|
+
# [Str16, ch. 2, 6].
|
|
37
|
+
module Randoms
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
COEFFICIENTS = (-9..9).freeze # small enough to do by hand
|
|
41
|
+
NATURALS = (0..9).freeze
|
|
42
|
+
DENOMINATORS = (1..9).freeze
|
|
43
|
+
UNIT = (0.0..1.0).freeze
|
|
44
|
+
TRIES = 500
|
|
45
|
+
|
|
46
|
+
def source(given) = given || RCAS.random
|
|
47
|
+
|
|
48
|
+
def refuse(what, wanted)
|
|
49
|
+
raise ArgumentError, "random #{what}: no #{wanted} came up in #{TRIES} tries; loosen the request"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# ---- numbers ---------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
# A number of the set, as Ruby writes it where that is the useful thing
|
|
55
|
+
# (an Integer for NN and ZZ, a Float for RR) and as rcas writes it where
|
|
56
|
+
# that reads better: a Num for QQ, and for CC a Gaussian integer, whose
|
|
57
|
+
# parts become rational when denominators: says so. `prime: true` draws
|
|
58
|
+
# a prime from the range.
|
|
59
|
+
def number(domain, range = nil, prime: false, denominators: nil, random: nil)
|
|
60
|
+
rng = source(random)
|
|
61
|
+
return prime_number(domain, range, rng) if prime
|
|
62
|
+
# NumberSet#=== is membership, so these are compared with ==, never `case`.
|
|
63
|
+
if domain == NN then rng.rand(integer_range(range || NATURALS))
|
|
64
|
+
elsif domain == ZZ then rng.rand(integer_range(range || COEFFICIENTS))
|
|
65
|
+
elsif domain == QQ then rational(range, denominators, rng)
|
|
66
|
+
elsif domain == RR then float(range, rng)
|
|
67
|
+
elsif domain == CC
|
|
68
|
+
Complex(*Array.new(2) { denominators ? rational(range, denominators, rng) : rng.rand(integer_range(range || COEFFICIENTS)) })
|
|
69
|
+
else raise ArgumentError, "random: #{domain} has no random elements"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def integer_range(range)
|
|
74
|
+
first = range.first
|
|
75
|
+
last = range.last
|
|
76
|
+
raise ArgumentError, "random: #{range} is not a range of integers" unless first.is_a?(Integer) && last.is_a?(Integer)
|
|
77
|
+
range.exclude_end? ? (first...last) : (first..last)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def rational(range, denominators, rng)
|
|
81
|
+
numerator = rng.rand(integer_range(range || COEFFICIENTS))
|
|
82
|
+
denominator = rng.rand(integer_range(denominators || DENOMINATORS))
|
|
83
|
+
value = Rational(numerator, denominator.zero? ? 1 : denominator)
|
|
84
|
+
value.denominator == 1 ? value.numerator : value
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def float(range, rng)
|
|
88
|
+
range ||= UNIT
|
|
89
|
+
rng.rand(range.first.to_f..range.last.to_f)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def prime_number(domain, range, rng)
|
|
93
|
+
range = integer_range(range || (2..99))
|
|
94
|
+
TRIES.times do
|
|
95
|
+
candidate = rng.rand(range)
|
|
96
|
+
return candidate if candidate >= 2 && NumberTheory.prime?(candidate) && domain.include?(candidate)
|
|
97
|
+
end
|
|
98
|
+
refuse("number", "prime in #{range}")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A non-zero element of the coefficient domain of a ring or space.
|
|
102
|
+
def coefficient(domain, range, denominators, rng, zero: true)
|
|
103
|
+
TRIES.times do
|
|
104
|
+
value = case domain
|
|
105
|
+
when FiniteField then domain.random_value(rng)
|
|
106
|
+
when AlgebraicField then algebraic(domain, range, denominators, rng)
|
|
107
|
+
when NumberSet then number(domain, range, denominators: denominators, random: rng)
|
|
108
|
+
else number(QQ, range, denominators: denominators, random: rng)
|
|
109
|
+
end
|
|
110
|
+
return value if zero || !Scalar.zero?(Scalar.lift(value))
|
|
111
|
+
end
|
|
112
|
+
refuse("coefficient", "non-zero value")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def algebraic(field, range, denominators, rng)
|
|
116
|
+
coeffs = Array.new(field.degree) { number(QQ, range, denominators: denominators, random: rng) }
|
|
117
|
+
terms = coeffs.each_with_index.to_h { |c, i| [[i], Num.new(c)] }.reject { |_, c| Scalar.zero?(c) }
|
|
118
|
+
field.element(Polynomial.new(field.ring, terms))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# ---- polynomials -----------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
# A polynomial of the ring. `degree` is the total degree (an Integer or a
|
|
124
|
+
# Range to choose from), `terms` how many monomials it should have,
|
|
125
|
+
# `coefficients` the range they are drawn from.
|
|
126
|
+
#
|
|
127
|
+
# monic: leading coefficient 1
|
|
128
|
+
# primitive: content 1 (over ZZ)
|
|
129
|
+
# squarefree: no repeated factor
|
|
130
|
+
# irreducible: no factor at all, over the coefficient domain
|
|
131
|
+
# roots: true, a count, or a list - a product of linear factors,
|
|
132
|
+
# so that it factors over ZZ
|
|
133
|
+
# factors: a product of that many random irreducible factors
|
|
134
|
+
# homogeneous: every monomial of the same total degree
|
|
135
|
+
def polynomial(ring, degree = nil, terms: nil, coefficients: nil, denominators: nil,
|
|
136
|
+
monic: false, primitive: false, squarefree: false, irreducible: false,
|
|
137
|
+
roots: nil, factors: nil, homogeneous: false, random: nil)
|
|
138
|
+
rng = source(random)
|
|
139
|
+
degree = pick(degree || (ring.univariate? ? 3 : 2), rng)
|
|
140
|
+
return from_roots(ring, degree, roots, coefficients, monic, rng) if roots
|
|
141
|
+
return from_factors(ring, degree, factors, coefficients, monic, rng) if factors
|
|
142
|
+
wanted = [("squarefree" if squarefree), ("irreducible" if irreducible)].compact.join(" and ")
|
|
143
|
+
TRIES.times do
|
|
144
|
+
f = raw_polynomial(ring, degree, terms, coefficients, denominators, homogeneous, rng)
|
|
145
|
+
f = monic_form(f) if monic
|
|
146
|
+
f = f.primitive_part if primitive && !monic
|
|
147
|
+
next if f.degree != degree
|
|
148
|
+
next if squarefree && !squarefree?(f)
|
|
149
|
+
next if irreducible && !(f.degree.positive? && f.irreducible?)
|
|
150
|
+
return f
|
|
151
|
+
end
|
|
152
|
+
refuse("polynomial", wanted.empty? ? "polynomial of degree #{degree}" : "#{wanted} polynomial of degree #{degree}")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The monomials of total degree at most d, the ones of degree exactly d
|
|
156
|
+
# first so that the leading term is always there.
|
|
157
|
+
def monomials(ring, degree, homogeneous)
|
|
158
|
+
vars = ring.vars.size
|
|
159
|
+
all = exponent_vectors(vars, degree)
|
|
160
|
+
top, rest = all.partition { |e| e.sum == degree }
|
|
161
|
+
homogeneous ? top : top + rest
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def exponent_vectors(vars, degree)
|
|
165
|
+
return (0..degree).map { |d| [d] } if vars == 1
|
|
166
|
+
(0..degree).flat_map do |d|
|
|
167
|
+
exponent_vectors(vars - 1, degree - d).map { |rest| [d] + rest }
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def raw_polynomial(ring, degree, terms, coefficients, denominators, homogeneous, rng)
|
|
172
|
+
available = monomials(ring, degree, homogeneous)
|
|
173
|
+
leading = available.first(available.count { |e| e.sum == degree })
|
|
174
|
+
count = terms ? pick(terms, rng) : available.size
|
|
175
|
+
count = count.clamp(1, available.size)
|
|
176
|
+
chosen = [leading.sample(random: rng)]
|
|
177
|
+
chosen += (available - chosen).sample(count - 1, random: rng)
|
|
178
|
+
table = chosen.to_h do |exponents|
|
|
179
|
+
[exponents, Num.new(coefficient(ring.base, coefficients, denominators, rng, zero: false))]
|
|
180
|
+
end
|
|
181
|
+
Polynomial.new(ring, table)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The leading coefficient set to 1 rather than divided out, so that a
|
|
185
|
+
# monic polynomial over ZZ still has integer coefficients.
|
|
186
|
+
def monic_form(f)
|
|
187
|
+
return f if f.zero?
|
|
188
|
+
Polynomial.new(f.ring, f.terms.merge(f.leading_exponents => Num.new(1)))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def squarefree?(f)
|
|
192
|
+
f.squarefree_decomposition.all? { |_, multiplicity| multiplicity == 1 }
|
|
193
|
+
rescue DomainError, NotImplementedError, RCAS::Unsupported
|
|
194
|
+
false
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# A product of linear factors, which is a polynomial that really factors.
|
|
198
|
+
def from_roots(ring, degree, roots, coefficients, monic, rng)
|
|
199
|
+
raise ArgumentError, "random: roots: needs one variable" unless ring.univariate?
|
|
200
|
+
values = case roots
|
|
201
|
+
when Array then roots
|
|
202
|
+
when Integer then Array.new(roots) { number(ZZ, coefficients, random: rng) }
|
|
203
|
+
else Array.new(degree) { number(ZZ, coefficients, random: rng) }
|
|
204
|
+
end
|
|
205
|
+
x = ring.gen
|
|
206
|
+
product = values.reduce(ring.one) { |acc, r| acc * (x - Polynomial.constant(ring, r)) }
|
|
207
|
+
monic ? product : product * Polynomial.constant(ring, coefficient(ring.base, (-3..3), nil, rng, zero: false))
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# A product of `count` irreducible factors whose degrees add up to `degree`.
|
|
211
|
+
def from_factors(ring, degree, count, coefficients, monic, rng)
|
|
212
|
+
raise ArgumentError, "random: factors: needs one variable" unless ring.univariate?
|
|
213
|
+
raise ArgumentError, "random: #{count} factors need a degree of at least #{count}" if degree < count
|
|
214
|
+
degrees = split_degree(degree, count, rng)
|
|
215
|
+
product = degrees.reduce(ring.one) do |acc, d|
|
|
216
|
+
acc * polynomial(ring, d, coefficients: coefficients, monic: true, irreducible: true, random: rng)
|
|
217
|
+
end
|
|
218
|
+
monic ? product : product * Polynomial.constant(ring, coefficient(ring.base, (-3..3), nil, rng, zero: false))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def split_degree(degree, count, rng)
|
|
222
|
+
degrees = Array.new(count, 1)
|
|
223
|
+
(degree - count).times { degrees[rng.rand(count)] += 1 }
|
|
224
|
+
degrees
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# An Integer, or one drawn from a Range.
|
|
228
|
+
def pick(value, rng) = value.is_a?(Range) ? rng.rand(integer_range(value)) : value
|
|
229
|
+
|
|
230
|
+
# ---- vectors and matrices --------------------------------------------------
|
|
231
|
+
|
|
232
|
+
# A vector of the space; `nonzero: true` refuses the zero vector.
|
|
233
|
+
def vector(space, entries: nil, denominators: nil, nonzero: false, random: nil)
|
|
234
|
+
rng = source(random)
|
|
235
|
+
TRIES.times do
|
|
236
|
+
v = space.unchecked(Array.new(space.dim) { coefficient(space.base, entries, denominators, rng) })
|
|
237
|
+
return v unless nonzero && v.zero?
|
|
238
|
+
end
|
|
239
|
+
refuse("vector", "non-zero vector")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# A matrix of the space. The shape keywords build what they name;
|
|
243
|
+
# invertible, singular, rank and the rest are sampled for and checked.
|
|
244
|
+
#
|
|
245
|
+
# entries: the range the entries are drawn from
|
|
246
|
+
# density: the proportion of them that is not zero
|
|
247
|
+
# symmetric:, antisymmetric:, diagonal:, triangular: :upper | :lower
|
|
248
|
+
# invertible:, singular:, rank:, det:
|
|
249
|
+
# unimodular: determinant +-1, so that the inverse stays integral
|
|
250
|
+
# eigenvalues: a list - P*D*P**-1 for a unimodular P
|
|
251
|
+
# definite: symmetric positive definite, L*L.transpose
|
|
252
|
+
#
|
|
253
|
+
# Every keyword is honoured or the call is refused: the answer is
|
|
254
|
+
# checked against all of them, because the constructions above build one
|
|
255
|
+
# property each and a second one used to be dropped without a word -
|
|
256
|
+
# rank: 1, symmetric: true came back unsymmetric (third review, L17).
|
|
257
|
+
# `triangular: true` means upper.
|
|
258
|
+
def matrix(space, random: nil, **opts)
|
|
259
|
+
opts[:triangular] = :upper if opts[:triangular] == true
|
|
260
|
+
rng = source(random)
|
|
261
|
+
TRIES.times do
|
|
262
|
+
m = build_matrix(space, random: rng, **opts)
|
|
263
|
+
return m if honours?(m, opts)
|
|
264
|
+
end
|
|
265
|
+
asked = opts.reject { |k, v| %i[entries denominators density].include?(k) || v.nil? || v == false }.keys
|
|
266
|
+
raise ArgumentError, "random matrix: no construction here gives #{asked.join(', ')} together"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Does m have every property the keywords ask for?
|
|
270
|
+
def honours?(m, opts)
|
|
271
|
+
zero = ->(i, j) { Scalar.zero?(m[i, j]) }
|
|
272
|
+
n = m.rows
|
|
273
|
+
checks = {
|
|
274
|
+
symmetric: -> { m.symmetric? },
|
|
275
|
+
antisymmetric: -> { m.square? && (m + m.transpose).zero? },
|
|
276
|
+
diagonal: -> { (0...n).all? { |i| (0...m.cols).all? { |j| i == j || zero.(i, j) } } },
|
|
277
|
+
triangular: lambda {
|
|
278
|
+
side = opts[:triangular]
|
|
279
|
+
(0...n).all? { |i| (0...m.cols).all? { |j| (side == :upper ? i <= j : i >= j) || zero.(i, j) } }
|
|
280
|
+
},
|
|
281
|
+
invertible: -> { m.square? && !Scalar.zero?(m.det) },
|
|
282
|
+
singular: -> { m.square? && Scalar.zero?(m.det) },
|
|
283
|
+
unimodular: -> { m.square? && [1, -1].any? { |u| Scalar.zero?(Scalar.sub(m.det, Scalar.lift(u))) } },
|
|
284
|
+
det: -> { Scalar.zero?(Scalar.sub(m.det, Scalar.lift(opts[:det]))) },
|
|
285
|
+
rank: -> { m.rank == opts[:rank] },
|
|
286
|
+
definite: -> { m.symmetric? }
|
|
287
|
+
}
|
|
288
|
+
checks.all? { |key, check| !opts[key] || check.call }
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def build_matrix(space, entries: nil, denominators: nil, density: nil, symmetric: false,
|
|
292
|
+
antisymmetric: false, diagonal: false, triangular: nil, invertible: false,
|
|
293
|
+
singular: false, unimodular: false, det: nil, rank: nil, eigenvalues: nil,
|
|
294
|
+
definite: false, random: nil)
|
|
295
|
+
rng = source(random)
|
|
296
|
+
if eigenvalues && symmetric
|
|
297
|
+
raise ArgumentError, "random matrix: symmetric with given eigenvalues needs an orthogonal matrix over #{space.base}, which rcas does not construct"
|
|
298
|
+
end
|
|
299
|
+
return unimodular_matrix(space, entries, rng) if unimodular
|
|
300
|
+
return triangular_with_determinant(space, det, triangular, entries, rng) if !det.nil? && triangular
|
|
301
|
+
return with_determinant(space, det, entries, rng) unless det.nil?
|
|
302
|
+
return symmetric_with_rank(space, rank, entries, rng) if !rank.nil? && symmetric
|
|
303
|
+
return with_eigenvalues(space, eigenvalues, entries, rng) if eigenvalues
|
|
304
|
+
return definite_matrix(space, entries, rng) if definite
|
|
305
|
+
return with_rank(space, rank, entries, denominators, rng) unless rank.nil?
|
|
306
|
+
return with_rank(space, [space.rows, space.cols].min - 1, entries, denominators, rng) if singular
|
|
307
|
+
shape = { symmetric: symmetric, antisymmetric: antisymmetric, diagonal: diagonal, triangular: triangular, density: density }
|
|
308
|
+
TRIES.times do
|
|
309
|
+
m = shaped_matrix(space, entries, denominators, shape, rng)
|
|
310
|
+
next if invertible && !(space.square? && !Scalar.zero?(m.det))
|
|
311
|
+
return m
|
|
312
|
+
end
|
|
313
|
+
refuse("matrix", "invertible matrix")
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def shaped_matrix(space, entries, denominators, shape, rng)
|
|
317
|
+
rng_entry = lambda do |i, j|
|
|
318
|
+
return 0 if shape[:diagonal] && i != j
|
|
319
|
+
return 0 if shape[:triangular] == :upper && i > j
|
|
320
|
+
return 0 if shape[:triangular] == :lower && i < j
|
|
321
|
+
return 0 if shape[:antisymmetric] && i == j
|
|
322
|
+
return 0 if shape[:density] && rng.rand > shape[:density]
|
|
323
|
+
coefficient(space.base, entries, denominators, rng)
|
|
324
|
+
end
|
|
325
|
+
rows = Array.new(space.rows) { |i| Array.new(space.cols) { |j| rng_entry.call(i, j) } }
|
|
326
|
+
if shape[:symmetric] || shape[:antisymmetric]
|
|
327
|
+
raise ArgumentError, "random matrix: #{shape[:symmetric] ? 'symmetric' : 'antisymmetric'} needs a square shape" unless space.square?
|
|
328
|
+
sign = shape[:symmetric] ? 1 : -1
|
|
329
|
+
(0...space.rows).each do |i|
|
|
330
|
+
(0...i).each { |j| rows[i][j] = Scalar.mul(Scalar.lift(rows[j][i]), sign) }
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
space.unchecked(rows)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# The identity with 2n row operations done to it and its rows shuffled:
|
|
337
|
+
# the determinant is +-1, so every entry of the inverse is in the ring
|
|
338
|
+
# again. The multipliers are +-1 by default, because the entries grow
|
|
339
|
+
# with them and a matrix to work with by hand is the point.
|
|
340
|
+
def unimodular_matrix(space, entries, rng)
|
|
341
|
+
square!(space, "unimodular")
|
|
342
|
+
n = space.rows
|
|
343
|
+
return space.identity if n < 2
|
|
344
|
+
m = space.identity
|
|
345
|
+
bound = (entries || (-1..1))
|
|
346
|
+
(2 * n).times do
|
|
347
|
+
i, j = (0...n).to_a.sample(2, random: rng)
|
|
348
|
+
factor = coefficient(space.base, bound, nil, rng, zero: false)
|
|
349
|
+
rows = m.to_a
|
|
350
|
+
rows[i] = rows[i].each_with_index.map { |e, k| Scalar.add(e, Scalar.mul(Scalar.lift(rows[j][k]), Scalar.lift(factor))) }
|
|
351
|
+
m = space.unchecked(rows)
|
|
352
|
+
end
|
|
353
|
+
space.unchecked(m.to_a.shuffle(random: rng))
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Upper triangular with the determinant on the diagonal, then mixed by
|
|
357
|
+
# unimodular matrices on both sides, which leaves the determinant alone.
|
|
358
|
+
def with_determinant(space, det, entries, rng)
|
|
359
|
+
square!(space, "det:")
|
|
360
|
+
n = space.rows
|
|
361
|
+
rows = Array.new(n) { |i| Array.new(n) { |j| i == j ? (i.zero? ? det : 1) : (i < j ? coefficient(space.base, entries || (-3..3), nil, rng) : 0) } }
|
|
362
|
+
triangular = space.unchecked(rows)
|
|
363
|
+
m = (unimodular_matrix(space, entries, rng) * triangular * unimodular_matrix(space, entries, rng)).simplify
|
|
364
|
+
# each unimodular factor has determinant +-1: a row changes sign when
|
|
365
|
+
# the two of them came out to -1 between them.
|
|
366
|
+
return m if Scalar.zero?(Scalar.sub(m.det, Scalar.lift(det)))
|
|
367
|
+
space.unchecked(m.to_a.each_with_index.map { |row, i| i.zero? ? row.map { |e| Scalar.neg(e) } : row })
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Triangular with the determinant on the diagonal and nothing mixed in.
|
|
371
|
+
def triangular_with_determinant(space, det, side, entries, rng)
|
|
372
|
+
square!(space, "det:")
|
|
373
|
+
n = space.rows
|
|
374
|
+
rows = Array.new(n) do |i|
|
|
375
|
+
Array.new(n) do |j|
|
|
376
|
+
next(i.zero? ? det : 1) if i == j
|
|
377
|
+
(side == :upper ? i < j : i > j) ? coefficient(space.base, entries || (-3..3), nil, rng) : 0
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
space.unchecked(rows)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# L*D*L.transpose with L of r columns: symmetric, and of rank r when D
|
|
384
|
+
# has no zero on its diagonal and L has full rank.
|
|
385
|
+
def symmetric_with_rank(space, rank, entries, rng)
|
|
386
|
+
square!(space, "symmetric:")
|
|
387
|
+
n = space.rows
|
|
388
|
+
raise ArgumentError, "random matrix: a rank between 0 and #{n} is needed, not #{rank}" unless (0..n).cover?(rank)
|
|
389
|
+
return space.zero if rank.zero?
|
|
390
|
+
l = MatrixSpace.new(space.base, n, rank).unchecked(Array.new(n) { Array.new(rank) { coefficient(space.base, entries || (-2..2), nil, rng) } })
|
|
391
|
+
d = MatrixSpace.new(space.base, rank, rank).unchecked(Array.new(rank) { |i| Array.new(rank) { |j| i == j ? coefficient(space.base, (-2..2), nil, rng, zero: false) : 0 } })
|
|
392
|
+
space.unchecked((l * d * l.transpose).simplify.to_a)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# P*D*P**-1 with P unimodular, so the entries stay in the ring and the
|
|
396
|
+
# eigenvalues are the ones asked for.
|
|
397
|
+
def with_eigenvalues(space, eigenvalues, entries, rng)
|
|
398
|
+
square!(space, "eigenvalues:")
|
|
399
|
+
unless eigenvalues.size == space.rows
|
|
400
|
+
raise ArgumentError, "random matrix: #{eigenvalues.size} eigenvalues for a #{space.rows} by #{space.cols} matrix"
|
|
401
|
+
end
|
|
402
|
+
d = space.unchecked(Array.new(space.rows) { |i| Array.new(space.cols) { |j| i == j ? eigenvalues[i] : 0 } })
|
|
403
|
+
TRIES.times do
|
|
404
|
+
p = unimodular_matrix(space, entries, rng)
|
|
405
|
+
m = (p * d * p.inverse).simplify
|
|
406
|
+
# a triangular answer would show its eigenvalues on the diagonal,
|
|
407
|
+
# which is no exercise at all
|
|
408
|
+
return m unless triangular?(m)
|
|
409
|
+
end
|
|
410
|
+
refuse("matrix", "matrix that is not triangular")
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def triangular?(m)
|
|
414
|
+
[:upper, :lower].any? do |side|
|
|
415
|
+
(0...m.rows).all? do |i|
|
|
416
|
+
(0...m.cols).all? { |j| (side == :upper ? i <= j : i >= j) || Scalar.zero?(m[i, j]) }
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# L*L.transpose for a lower triangular L with a positive diagonal:
|
|
422
|
+
# symmetric, and positive definite because L is invertible.
|
|
423
|
+
def definite_matrix(space, entries, rng)
|
|
424
|
+
square!(space, "definite:")
|
|
425
|
+
n = space.rows
|
|
426
|
+
bound = entries || (-3..3)
|
|
427
|
+
rows = Array.new(n) do |i|
|
|
428
|
+
Array.new(n) do |j|
|
|
429
|
+
if i == j then rng.rand(1..3)
|
|
430
|
+
elsif i > j then coefficient(space.base, bound, nil, rng)
|
|
431
|
+
else 0
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
l = space.unchecked(rows)
|
|
436
|
+
(l * l.transpose).simplify
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# A product of a rows-by-r and an r-by-cols matrix has rank at most r,
|
|
440
|
+
# and generically exactly r.
|
|
441
|
+
def with_rank(space, rank, entries, denominators, rng)
|
|
442
|
+
limit = [space.rows, space.cols].min
|
|
443
|
+
raise ArgumentError, "random matrix: a rank between 0 and #{limit} is needed, not #{rank}" unless (0..limit).cover?(rank)
|
|
444
|
+
return space.zero if rank.zero?
|
|
445
|
+
left = MatrixSpace.new(space.base, space.rows, rank)
|
|
446
|
+
right = MatrixSpace.new(space.base, rank, space.cols)
|
|
447
|
+
TRIES.times do
|
|
448
|
+
m = (matrix(left, entries: entries, denominators: denominators, random: rng) *
|
|
449
|
+
matrix(right, entries: entries, denominators: denominators, random: rng)).simplify
|
|
450
|
+
return space.unchecked(m.to_a) if m.rank == rank
|
|
451
|
+
end
|
|
452
|
+
refuse("matrix", "matrix of rank #{rank}")
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def square!(space, what)
|
|
456
|
+
raise ArgumentError, "random matrix: #{what} needs a square shape, not #{space}" unless space.square?
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# The structures answer `random` themselves, beside zero, one, gen and
|
|
461
|
+
# identity: the domain already says what its elements look like.
|
|
462
|
+
class NumberSet
|
|
463
|
+
# ZZ.random(1..100), QQ.random, ZZ.random(2..99, prime: true)
|
|
464
|
+
def random(range = nil, **opts)
|
|
465
|
+
value = Randoms.number(self, range, **opts)
|
|
466
|
+
value.is_a?(Integer) || value.is_a?(Float) ? value : Num.new(value).simplify
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
class PolynomialRing
|
|
471
|
+
# ZZ[x].random(3), ZZ[x].random(4, irreducible: true), QQ[x, y].random(2, terms: 3)
|
|
472
|
+
def random(degree = nil, **opts) = Randoms.polynomial(self, degree, **opts)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
class FractionField
|
|
476
|
+
# Frac(QQ[x]).random(2): a quotient of two random polynomials, cancelled
|
|
477
|
+
def random(degree = nil, **opts)
|
|
478
|
+
numerator = ring.random(degree, **opts)
|
|
479
|
+
denominator = ring.random(degree, **opts)
|
|
480
|
+
denominator = ring.one if denominator.zero?
|
|
481
|
+
(numerator.to_expr / denominator.to_expr).cancel
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
class VectorSpace
|
|
486
|
+
# (ZZ**3).random, (QQ**3).random(nonzero: true)
|
|
487
|
+
def random(**opts) = Randoms.vector(self, **opts)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
class MatrixSpace
|
|
491
|
+
# (ZZ**[3, 3]).random, (ZZ**[3, 3]).random(unimodular: true)
|
|
492
|
+
def random(**opts) = Randoms.matrix(self, **opts)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
class FiniteField
|
|
496
|
+
# GF(9).random
|
|
497
|
+
def random(random: nil) = random_value(Randoms.source(random))
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
class AlgebraicField
|
|
501
|
+
# QQ(sqrt(2)).random: a + b*sqrt(2) with random rational a and b
|
|
502
|
+
def random(coefficients: nil, denominators: nil, random: nil)
|
|
503
|
+
Randoms.algebraic(self, coefficients, denominators, Randoms.source(random))
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Rational functions as expressions: numerator and denominator of the
|
|
5
|
+
# normal form, partial fractions, and polynomial gcd / lcm / division
|
|
6
|
+
# without building a ring by hand.
|
|
7
|
+
#
|
|
8
|
+
# numer(1/x + 1/(x + 1)) # => 1 + 2*x
|
|
9
|
+
# apart(1/(x**2 - 1), x) # => -1/(2*(1 + x)) + 1/(2*(-1 + x))
|
|
10
|
+
# gcd(x**2 - 1, x**2 + 2*x + 1) # => 1 + x
|
|
11
|
+
# quo(x**3 - 1, x - 1) # => 1 + x + x**2
|
|
12
|
+
#
|
|
13
|
+
# Rational-coefficient input goes through Fraction.as_fraction (one
|
|
14
|
+
# numerator over one denominator, gcd cancelled). Anything else is split
|
|
15
|
+
# syntactically: factors with negative exponents form the denominator.
|
|
16
|
+
#
|
|
17
|
+
# Sources: the partial fraction decomposition is the classical one in
|
|
18
|
+
# [Bro05, §2.1] (see MANUAL.md, Sources): split over pairwise coprime
|
|
19
|
+
# denominators with the extended Euclidean algorithm, then expand each
|
|
20
|
+
# numerator p-adically in its irreducible factor. Denominators are
|
|
21
|
+
# factored over QQ (factor.rb), so no algebraic numbers are introduced.
|
|
22
|
+
module RationalFunction
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# ---- numerator and denominator ----------------------------------------
|
|
26
|
+
|
|
27
|
+
def numer(expr) = numer_denom(expr).first
|
|
28
|
+
def denom(expr) = numer_denom(expr).last
|
|
29
|
+
|
|
30
|
+
# [numerator, denominator] with integer coefficients, denominator with a
|
|
31
|
+
# positive leading coefficient, when the expression is a rational
|
|
32
|
+
# function over QQ; otherwise the syntactic split of the simplified form.
|
|
33
|
+
def numer_denom(expr)
|
|
34
|
+
expr = expr.to_expr if expr.is_a?(Polynomial)
|
|
35
|
+
expr = Expression.lift(expr)
|
|
36
|
+
if (pair = Fraction.as_fraction(expr))
|
|
37
|
+
return integral_pair(*pair).map(&:to_expr)
|
|
38
|
+
end
|
|
39
|
+
coeff, factors = Simplify.factorize(expr, simplify: true)
|
|
40
|
+
up = factors.reject { |_, e| Simplify.negative?(e) }
|
|
41
|
+
down = factors.select { |_, e| Simplify.negative?(e) }.transform_values { |e| Simplify.multiply_exponents(e, -1) }
|
|
42
|
+
num_c, den_c = coeff.is_a?(Rational) ? [coeff.numerator, coeff.denominator] : [coeff, 1]
|
|
43
|
+
[Simplify.rebuild_product(num_c, up), Simplify.rebuild_product(den_c, down)]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Scale two polynomials over QQ to integer coefficients with no common
|
|
47
|
+
# integer factor and a positive leading coefficient in the denominator.
|
|
48
|
+
def integral_pair(num, den)
|
|
49
|
+
scale = [num, den].flat_map { |p| p.terms.values.map { |c| c.value.is_a?(Rational) ? c.value.denominator : 1 } }.reduce(1, :lcm)
|
|
50
|
+
num *= scale
|
|
51
|
+
den *= scale
|
|
52
|
+
g = Polynomial.rational_gcd(num.content, den.content)
|
|
53
|
+
g = -g if Scalar.negative?(den.leading_coefficient)
|
|
54
|
+
[num * Rational(1, g), den * Rational(1, g)]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# ---- partial fractions ------------------------------------------------
|
|
58
|
+
|
|
59
|
+
# Partial fraction decomposition over QQ in one indeterminate; other
|
|
60
|
+
# indeterminates are treated as parameters.
|
|
61
|
+
def apart(expr, var = nil)
|
|
62
|
+
expr = expr.to_expr if expr.is_a?(Polynomial)
|
|
63
|
+
expr = Expression.lift(expr)
|
|
64
|
+
# a constant is its own decomposition (A11)
|
|
65
|
+
return expr.simplify if expr.constant? && var.nil? && (expr.simplify.is_a?(Num))
|
|
66
|
+
pair = Fraction.as_fraction(expr)
|
|
67
|
+
raise DomainError, "apart: #{expr} is not a rational function with rational coefficients" unless pair
|
|
68
|
+
num, den = pair
|
|
69
|
+
vars = num.ring.vars
|
|
70
|
+
x = apart_variable(expr, vars, var)
|
|
71
|
+
return expr.simplify if den.constant?
|
|
72
|
+
|
|
73
|
+
others = vars - [x]
|
|
74
|
+
ring = others.empty? ? QQ[x] : QQ[*others].fraction_field[x]
|
|
75
|
+
factorization = den.factor
|
|
76
|
+
unit = factorization.unit
|
|
77
|
+
numerator = (num * Scalar.div(Num.new(1), unit)).to_ring(ring)
|
|
78
|
+
factors = factorization.factors.map { |p, e| [p.to_ring(ring), e] }
|
|
79
|
+
|
|
80
|
+
polynomial, numerator = numerator.divmod(factors.reduce(ring.one) { |acc, (p, e)| acc * p**e })
|
|
81
|
+
parts = []
|
|
82
|
+
factors.each_with_index do |(p, e), i|
|
|
83
|
+
if i == factors.size - 1
|
|
84
|
+
parts << [numerator, p, e]
|
|
85
|
+
break
|
|
86
|
+
end
|
|
87
|
+
a = p**e
|
|
88
|
+
b = factors[(i + 1)..].reduce(ring.one) { |acc, (q, k)| acc * q**k }
|
|
89
|
+
_, s, t = a.xgcd(b) # s*a + t*b = 1
|
|
90
|
+
qa, ra = (numerator * t).divmod(a)
|
|
91
|
+
qb, rb = (numerator * s).divmod(b)
|
|
92
|
+
polynomial += qa + qb
|
|
93
|
+
parts << [ra, p, e]
|
|
94
|
+
numerator = rb
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
terms = []
|
|
98
|
+
terms << [polynomial.to_expr, false] unless polynomial.zero?
|
|
99
|
+
parts.each do |c, p, e|
|
|
100
|
+
expansion = []
|
|
101
|
+
e.downto(1) do |k|
|
|
102
|
+
c, c0 = c.divmod(p)
|
|
103
|
+
expansion << [c0, k] unless c0.zero?
|
|
104
|
+
end
|
|
105
|
+
expansion.reverse_each do |c0, k|
|
|
106
|
+
n, d = numer_denom(c0.to_expr) # (a/2 + b/2) => (a + b)/2
|
|
107
|
+
term = (n / (d * Simplify.power_node(p.to_expr, k))).simplify
|
|
108
|
+
coeff, factors = Simplify.factorize(term)
|
|
109
|
+
negative = Simplify.negative?(coeff)
|
|
110
|
+
terms << [negative ? Simplify.rebuild_product(-coeff, factors) : term, negative]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
return Num.new(0) if terms.empty?
|
|
114
|
+
Simplify.sum_tree(terms)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def apart_variable(expr, vars, var)
|
|
118
|
+
if var
|
|
119
|
+
v = Coefficients.variable(var)
|
|
120
|
+
raise ArgumentError, "apart: #{v} does not occur in #{expr}" unless vars.include?(v.name)
|
|
121
|
+
return v.name
|
|
122
|
+
end
|
|
123
|
+
return vars.first if vars.size == 1
|
|
124
|
+
raise ArgumentError, "apart: #{expr} has several indeterminates (#{vars.join(', ')}); name one, e.g. apart(f, #{vars.first})"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# ---- gcd, lcm, division -----------------------------------------------
|
|
128
|
+
|
|
129
|
+
def gcd(f, g) = ring_operation(f, g, :gcd)
|
|
130
|
+
def lcm(f, g) = ring_operation(f, g, :lcm)
|
|
131
|
+
|
|
132
|
+
# Quotient and remainder of polynomial division. With several
|
|
133
|
+
# indeterminates, x names the one to divide by; the others are parameters.
|
|
134
|
+
def divmod(f, g, x = nil)
|
|
135
|
+
return f.divmod(g) if numeric?(f) && numeric?(g)
|
|
136
|
+
ring = division_ring(f, g, x)
|
|
137
|
+
q, r = ring.call(f).divmod(ring.call(g))
|
|
138
|
+
[q, r].map { |p| polynomial_result(f, g, p) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def quo(f, g, x = nil) = divmod(f, g, x).first
|
|
142
|
+
def rem(f, g, x = nil) = divmod(f, g, x).last
|
|
143
|
+
|
|
144
|
+
def ring_operation(f, g, op)
|
|
145
|
+
if numeric?(f) && numeric?(g)
|
|
146
|
+
a, b = [f, g].map { |v| v.is_a?(Num) ? v.value : v }
|
|
147
|
+
return a.public_send(op, b) if a.is_a?(Integer) && b.is_a?(Integer)
|
|
148
|
+
return op == :gcd ? Polynomial.rational_gcd(a, b) : Rational(a) * Rational(b) / Polynomial.rational_gcd(a, b)
|
|
149
|
+
end
|
|
150
|
+
first, second = f.is_a?(Polynomial) || !numeric?(f) ? [f, g] : [g, f]
|
|
151
|
+
first = Expression.lift(first).to_poly unless first.is_a?(Polynomial)
|
|
152
|
+
polynomial_result(f, g, first.public_send(op, second))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def division_ring(f, g, x)
|
|
156
|
+
polys = [f, g].map { |e| e.is_a?(Polynomial) ? e : (numeric?(e) ? nil : Expression.lift(e).to_poly) }.compact
|
|
157
|
+
base = polys.map { |p| p.ring.base }.reduce(QQ) { |acc, b| acc.join(b) }
|
|
158
|
+
vars = polys.flat_map { |p| p.ring.vars }.uniq.sort
|
|
159
|
+
if x
|
|
160
|
+
x = Coefficients.variable(x).name
|
|
161
|
+
raise ArgumentError, "#{x} does not occur in #{f} or #{g}" unless vars.include?(x)
|
|
162
|
+
others = vars - [x]
|
|
163
|
+
others.empty? ? base[x] : base[*others].fraction_field[x]
|
|
164
|
+
else
|
|
165
|
+
base[*vars]
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def polynomial_result(f, g, poly)
|
|
170
|
+
f.is_a?(Polynomial) || g.is_a?(Polynomial) ? poly : poly.to_expr
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def numeric?(v) = v.is_a?(Numeric) || (v.is_a?(Num) && !v.value.is_a?(Complex) && !v.finite_field?)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
class Expression
|
|
177
|
+
def numer = RationalFunction.numer(self)
|
|
178
|
+
def denom = RationalFunction.denom(self)
|
|
179
|
+
def apart(var = nil) = RationalFunction.apart(self, var)
|
|
180
|
+
def gcd(other) = RationalFunction.gcd(self, other)
|
|
181
|
+
def lcm(other) = RationalFunction.lcm(self, other)
|
|
182
|
+
def divmod(other, var = nil) = RationalFunction.divmod(self, other, var)
|
|
183
|
+
def quo(other, var = nil) = RationalFunction.quo(self, other, var)
|
|
184
|
+
def rem(other, var = nil) = RationalFunction.rem(self, other, var)
|
|
185
|
+
end
|
|
186
|
+
end
|