rcas 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/CITATION.cff +17 -0
  3. data/DESIGN.md +783 -0
  4. data/LICENSE +21 -0
  5. data/MANUAL.md +6265 -0
  6. data/README.md +267 -0
  7. data/bin/rcas +9 -0
  8. data/bin/rcas-app +9 -0
  9. data/bin/rcas-chat +9 -0
  10. data/lib/rcas/algebraic.rb +481 -0
  11. data/lib/rcas/analysis.rb +966 -0
  12. data/lib/rcas/app/launcher.rb +203 -0
  13. data/lib/rcas/app/public/app.css +402 -0
  14. data/lib/rcas/app/public/app.js +449 -0
  15. data/lib/rcas/app/public/index.html +46 -0
  16. data/lib/rcas/app/server.rb +220 -0
  17. data/lib/rcas/app/window.rb +94 -0
  18. data/lib/rcas/app/worksheet.rb +290 -0
  19. data/lib/rcas/app.rb +168 -0
  20. data/lib/rcas/background.rb +758 -0
  21. data/lib/rcas/chat/assistant.rb +199 -0
  22. data/lib/rcas/chat/picker.rb +164 -0
  23. data/lib/rcas/chat/repl.rb +583 -0
  24. data/lib/rcas/chat/session.rb +137 -0
  25. data/lib/rcas/chat/settings.rb +71 -0
  26. data/lib/rcas/chat/style.rb +30 -0
  27. data/lib/rcas/chat/tool.rb +53 -0
  28. data/lib/rcas/chat/ui.rb +316 -0
  29. data/lib/rcas/chat/usage.rb +62 -0
  30. data/lib/rcas/chat/workspace.rb +132 -0
  31. data/lib/rcas/chat.rb +54 -0
  32. data/lib/rcas/coefficients.rb +170 -0
  33. data/lib/rcas/combinatorics.rb +274 -0
  34. data/lib/rcas/complex_parts.rb +160 -0
  35. data/lib/rcas/constants.rb +129 -0
  36. data/lib/rcas/core_ext.rb +35 -0
  37. data/lib/rcas/decide.rb +501 -0
  38. data/lib/rcas/decompositions.rb +241 -0
  39. data/lib/rcas/differentiate.rb +144 -0
  40. data/lib/rcas/discussion.rb +558 -0
  41. data/lib/rcas/distributions.rb +980 -0
  42. data/lib/rcas/dixon.rb +95 -0
  43. data/lib/rcas/docs.rb +321 -0
  44. data/lib/rcas/domains.rb +728 -0
  45. data/lib/rcas/expand.rb +174 -0
  46. data/lib/rcas/expression.rb +613 -0
  47. data/lib/rcas/factor.rb +605 -0
  48. data/lib/rcas/finite_field.rb +577 -0
  49. data/lib/rcas/fourier.rb +118 -0
  50. data/lib/rcas/fps.rb +678 -0
  51. data/lib/rcas/fraction.rb +126 -0
  52. data/lib/rcas/functions.rb +1136 -0
  53. data/lib/rcas/gcd.rb +112 -0
  54. data/lib/rcas/geometry.rb +266 -0
  55. data/lib/rcas/groebner.rb +162 -0
  56. data/lib/rcas/hold.rb +277 -0
  57. data/lib/rcas/hypothesis.rb +364 -0
  58. data/lib/rcas/inequalities.rb +689 -0
  59. data/lib/rcas/integral_functions.rb +260 -0
  60. data/lib/rcas/integrate.rb +1589 -0
  61. data/lib/rcas/integrate_substitutions.rb +434 -0
  62. data/lib/rcas/interpolate.rb +40 -0
  63. data/lib/rcas/irb.rb +146 -0
  64. data/lib/rcas/laplace.rb +159 -0
  65. data/lib/rcas/latex.rb +556 -0
  66. data/lib/rcas/lattice.rb +172 -0
  67. data/lib/rcas/linear_algebra.rb +117 -0
  68. data/lib/rcas/linear_program.rb +416 -0
  69. data/lib/rcas/lint.rb +79 -0
  70. data/lib/rcas/matrix.rb +531 -0
  71. data/lib/rcas/matrix_multiply.rb +202 -0
  72. data/lib/rcas/multimodular.rb +286 -0
  73. data/lib/rcas/named_polynomials.rb +274 -0
  74. data/lib/rcas/number_theory.rb +443 -0
  75. data/lib/rcas/numerics.rb +825 -0
  76. data/lib/rcas/ode.rb +488 -0
  77. data/lib/rcas/openmath/objects.rb +364 -0
  78. data/lib/rcas/openmath/phrasebook.rb +551 -0
  79. data/lib/rcas/openmath/popcorn.rb +518 -0
  80. data/lib/rcas/openmath/xml.rb +309 -0
  81. data/lib/rcas/openmath.rb +49 -0
  82. data/lib/rcas/petkovsek.rb +165 -0
  83. data/lib/rcas/piecewise.rb +488 -0
  84. data/lib/rcas/plot.rb +763 -0
  85. data/lib/rcas/plot3d.rb +419 -0
  86. data/lib/rcas/poly_matrix.rb +318 -0
  87. data/lib/rcas/poly_recurrence.rb +117 -0
  88. data/lib/rcas/polynomial.rb +466 -0
  89. data/lib/rcas/precision.rb +925 -0
  90. data/lib/rcas/printer.rb +150 -0
  91. data/lib/rcas/product.rb +155 -0
  92. data/lib/rcas/q_difference.rb +296 -0
  93. data/lib/rcas/q_functions.rb +158 -0
  94. data/lib/rcas/q_summation.rb +308 -0
  95. data/lib/rcas/q_zeilberger.rb +199 -0
  96. data/lib/rcas/random.rb +506 -0
  97. data/lib/rcas/rational_function.rb +186 -0
  98. data/lib/rcas/recurrence.rb +323 -0
  99. data/lib/rcas/render.rb +431 -0
  100. data/lib/rcas/results.rb +192 -0
  101. data/lib/rcas/scalar.rb +219 -0
  102. data/lib/rcas/series.rb +726 -0
  103. data/lib/rcas/simplify.rb +649 -0
  104. data/lib/rcas/solve.rb +2002 -0
  105. data/lib/rcas/special.rb +163 -0
  106. data/lib/rcas/statistics.rb +175 -0
  107. data/lib/rcas/steps.rb +835 -0
  108. data/lib/rcas/summation.rb +532 -0
  109. data/lib/rcas/trig.rb +264 -0
  110. data/lib/rcas/van_hoeij.rb +241 -0
  111. data/lib/rcas/vector.rb +175 -0
  112. data/lib/rcas/vector_calculus.rb +411 -0
  113. data/lib/rcas/version.rb +5 -0
  114. data/lib/rcas/zeilberger.rb +358 -0
  115. data/lib/rcas.rb +91 -0
  116. data/package.json +8 -0
  117. metadata +206 -0
@@ -0,0 +1,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