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,531 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # QQ**[2, 3]: matrices with a fixed shape over a domain, and a domain
5
+ # itself: a matrix answers `domain` with the space it lives in, and the
6
+ # space answers `base` with the domain its entries come from.
7
+ class MatrixSpace < Domain
8
+ attr_reader :base, :rows, :cols
9
+
10
+ def initialize(base, rows, cols = rows)
11
+ unless rows.is_a?(Integer) && cols.is_a?(Integer) && rows >= 0 && cols >= 0
12
+ raise ArgumentError, "matrix shape must be two non-negative integers"
13
+ end
14
+ @base = base
15
+ @rows = rows
16
+ @cols = cols
17
+ freeze
18
+ end
19
+
20
+ # (QQ**[2, 2])[[1, 2], [3, 4]]. This is the element constructor and it
21
+ # shadows Domain#[], the polynomial ring - which rcas cannot build over a
22
+ # space anyway, since a coefficient is a number. A symbol therefore has no
23
+ # meaning here, and saying so is more use than counting rows.
24
+ def [](*row_arrays)
25
+ if (names = row_arrays.grep(Symbol) + row_arrays.grep(Var).map(&:name)).any?
26
+ raise DomainError, "#{self} is not a domain of numbers: polynomial coefficients are scalars. " \
27
+ "Matrices of polynomials are (#{base}[#{names.join(', ')}])**[#{rows}, #{cols}]"
28
+ end
29
+ row_arrays = row_arrays.first if row_arrays.size == 1 && row_arrays.first.first.is_a?(Array)
30
+ unless row_arrays.size == rows && row_arrays.all? { |r| r.size == cols }
31
+ raise DomainError, "#{self} needs #{rows} rows of #{cols} entries"
32
+ end
33
+ entries = row_arrays.map { |r| r.map { |e| Scalar.lift(e) } }
34
+ entries = entries.map { |r| r.map { |e| base.normalize_coefficient(e) } } if base.respond_to?(:normalize_coefficient)
35
+ entries.flatten.each do |e|
36
+ raise DomainError, "#{e} is not in #{base}" unless Infer.where_defined { base.include?(e) }
37
+ end
38
+ Matrix.new(self, entries)
39
+ end
40
+
41
+ def unchecked(entries) = Matrix.new(self, entries.map { |r| r.map { |e| Scalar.lift(e) } })
42
+
43
+ def include?(obj)
44
+ obj = self[*obj] if obj.is_a?(Array)
45
+ obj.is_a?(Matrix) && obj.rows == rows && obj.cols == cols && obj.entries.flatten.all? { |e| base.include?(e) }
46
+ rescue DomainError
47
+ false
48
+ end
49
+ alias member? include?
50
+ def ===(obj) = include?(obj)
51
+
52
+ def zero = unchecked(Array.new(rows) { Array.new(cols, 0) })
53
+
54
+ def identity
55
+ raise ArgumentError, "identity needs a square shape" unless square?
56
+ unchecked(Array.new(rows) { |i| Array.new(cols) { |j| i == j ? 1 : 0 } })
57
+ end
58
+
59
+ def square? = rows == cols
60
+
61
+ # Square matrices over a ring are a ring under multiplication; a shape
62
+ # that is not square is not, and only the 1 by 1 case can be a field.
63
+ def ring? = square? && base.ring?
64
+ def field? = rows == 1 && cols == 1 && base.field?
65
+
66
+ # Matrices are not numbers: no polynomial has them as coefficients.
67
+ def scalar? = false
68
+
69
+ def over(other_base) = MatrixSpace.new(other_base, rows, cols)
70
+
71
+ # The same shape over a larger base: (ZZ**[2, 2]) < (QQ**[2, 2]).
72
+ def subset?(other) = other.is_a?(MatrixSpace) && other.rows == rows && other.cols == cols && base.subset?(other.base)
73
+
74
+ def join(other)
75
+ return MatrixSpace.new(base.join(other.base), rows, cols) if other.is_a?(MatrixSpace) && other.rows == rows && other.cols == cols
76
+ raise DomainError, "no common domain for #{self} and #{other}" if other.is_a?(MatrixSpace) || other.is_a?(VectorSpace)
77
+ MatrixSpace.new(base.join(other), rows, cols) # a domain of scalars: what scaling gives
78
+ end
79
+
80
+ def ==(other) = other.is_a?(MatrixSpace) && other.base == base && other.rows == rows && other.cols == cols
81
+ alias eql? ==
82
+ def hash = [MatrixSpace, base, rows, cols].hash
83
+
84
+ def name = "#{base}**[#{rows}, #{cols}]"
85
+ end
86
+
87
+ class Matrix
88
+ include Algebraic
89
+
90
+ attr_reader :space, :entries
91
+
92
+ def initialize(space, entries)
93
+ @space = space
94
+ @entries = entries.map(&:freeze).freeze
95
+ freeze
96
+ end
97
+
98
+ def rows = space.rows
99
+ def cols = space.cols
100
+ # The space is where the matrix lives, base where its entries come from.
101
+ def domain = space
102
+ def base = space.base
103
+ def square? = space.square?
104
+ def [](i, j) = entries.fetch(i).fetch(j)
105
+ def to_a = entries.map(&:dup)
106
+
107
+ def row(i) = VectorSpace.new(base, cols).unchecked(entries.fetch(i))
108
+ def column(j) = VectorSpace.new(base, rows).unchecked(entries.map { |r| r.fetch(j) })
109
+ def row_vectors = (0...rows).map { |i| row(i) }
110
+ def column_vectors = (0...cols).map { |j| column(j) }
111
+
112
+ # ---- arithmetic -------------------------------------------------------
113
+
114
+ def +(other) = zip_with(other, :+) { |a, b| Scalar.add(a, b) }
115
+ def -(other) = zip_with(other, :-) { |a, b| Scalar.sub(a, b) }
116
+ def -@ = space.unchecked(map_entries { |e| Scalar.neg(e) })
117
+
118
+ # The product goes through MatrixMultiply: Integer and Rational entries
119
+ # on the bare numbers, Strassen for large ones (MatrixMultiply.algorithm
120
+ # chooses; `multiply` names the algorithm for one product).
121
+ def *(other)
122
+ case other
123
+ when Matrix then multiply(other)
124
+ when Vector
125
+ raise ArgumentError, "shape mismatch: #{space} * #{other.space}" unless cols == other.dim
126
+ column = other.entries.map { |e| [e] }
127
+ product = MatrixMultiply.product(entries, column, :schoolbook, columns: 1).map(&:first)
128
+ VectorSpace.new(base.join(other.base), rows).unchecked(product)
129
+ else
130
+ scale(other)
131
+ end
132
+ end
133
+
134
+ # a.multiply(b, algorithm: :strassen): the product by :schoolbook,
135
+ # :strassen [Str69], [Win71] or :auto, for comparing them.
136
+ def multiply(other, algorithm: nil)
137
+ raise TypeError, "can't multiply a Matrix by #{other.class} with an algorithm" unless other.is_a?(Matrix)
138
+ raise ArgumentError, "shape mismatch: #{space} * #{other.space}" unless cols == other.rows
139
+ product = MatrixMultiply.product(entries, other.entries, algorithm || MatrixMultiply.algorithm, columns: other.cols)
140
+ MatrixSpace.new(base.join(other.base), rows, other.cols).unchecked(product)
141
+ end
142
+
143
+ def self.row_times(vector, matrix)
144
+ raise ArgumentError, "shape mismatch: #{vector.space} * #{matrix.space}" unless vector.dim == matrix.rows
145
+ product = MatrixMultiply.product([vector.entries], matrix.entries, :schoolbook, columns: matrix.cols).first
146
+ VectorSpace.new(vector.base.join(matrix.base), matrix.cols).unchecked(product)
147
+ end
148
+
149
+ def /(scalar)
150
+ s = Scalar.lift(scalar)
151
+ space.over(result_base(s).fraction_field).unchecked(map_entries { |e| Scalar.div(e, s) })
152
+ end
153
+
154
+ def scale(scalar)
155
+ s = Scalar.lift(scalar)
156
+ space.over(result_base(s)).unchecked(map_entries { |e| Scalar.mul(e, s) })
157
+ end
158
+
159
+ def coerce(other) = [ScalarProxy.new(other), self]
160
+ def rop(op, left) = op == :* ? scale(left) : super
161
+
162
+ def **(n)
163
+ raise ArgumentError, "matrix powers need a square matrix" unless square?
164
+ raise ArgumentError, "matrix powers must be integers" unless n.is_a?(Integer)
165
+ return inverse**(-n) if n.negative?
166
+ result = space.identity
167
+ base = self
168
+ while n.positive?
169
+ result *= base if n.odd?
170
+ base *= base
171
+ n >>= 1
172
+ end
173
+ result
174
+ end
175
+
176
+ def transpose = MatrixSpace.new(base, cols, rows).unchecked(entries.transpose)
177
+ alias t transpose
178
+
179
+ def trace
180
+ raise ArgumentError, "trace needs a square matrix" unless square?
181
+ (0...rows).map { |i| self[i, i] }.reduce { |s, x| Scalar.add(s, x) } || Num.new(0)
182
+ end
183
+
184
+ # ---- elimination ------------------------------------------------------
185
+
186
+ # Reduced row echelon form, over the fraction field of the base.
187
+ def rref = space.over(base.fraction_field).unchecked(Elimination.rref(entries).first)
188
+
189
+ # A Float matrix counts a pivot below its rounding as zero, the tolerance
190
+ # its eigenvectors use: [[1, 3], [0.1, 0.3]] has proportional rows, and
191
+ # the exact elimination found the -5.6e-17 left of 0.3 - 3*0.1 (fourth
192
+ # review, L10).
193
+ def rank
194
+ found = full_rank_at_a_point
195
+ return found if found
196
+ return Elimination.rref(entries).last.size unless floating?
197
+ rows = Matrix.float_rows(entries)
198
+ Matrix.float_eliminate(rows, Matrix.float_scale(rows) * FLOAT_TOLERANCE).last.size
199
+ end
200
+
201
+ # Integer and Rational entries: modulo many primes (Multimodular), or
202
+ # Gaussian elimination with algorithm: :elimination, to compare. Other
203
+ # numbers: Gaussian elimination. Polynomial or
204
+ # rational-function entries in one indeterminate: evaluation and
205
+ # interpolation (PolyMatrix). Anything else: cofactor expansion.
206
+ def det(algorithm: :auto)
207
+ raise ArgumentError, "determinant needs a square matrix" unless square?
208
+ return Num.new(Multimodular.det(entries)) if Multimodular.use?(entries, algorithm)
209
+ return Elimination.det(entries) if numeric?
210
+ PolyMatrix.det(entries) || Elimination.cofactor_det(entries).expand
211
+ end
212
+
213
+ # Integer and Rational matrices are inverted modulo many primes (as det
214
+ # is), other numeric ones by row reduction, symbolic ones through the
215
+ # adjugate so the entries come out as cofactor/det.
216
+ def inverse(algorithm: :auto)
217
+ raise ArgumentError, "inverse needs a square matrix" unless square?
218
+ target = space.over(base.fraction_field)
219
+ if Multimodular.use?(entries, algorithm)
220
+ inv = Multimodular.inverse(entries) or raise DomainError, "matrix is singular"
221
+ target.unchecked(inv)
222
+ elsif numeric?
223
+ augmented = entries.each_with_index.map { |r, i| r + Array.new(rows) { |j| Num.new(i == j ? 1 : 0) } }
224
+ reduced, pivots = Elimination.rref(augmented)
225
+ raise DomainError, "matrix is singular" unless pivots.first(rows) == (0...rows).to_a
226
+ target.unchecked(reduced.map { |r| r.drop(rows) })
227
+ elsif (inv = PolyMatrix.inverse(entries))
228
+ target.unchecked(inv)
229
+ else
230
+ d = det
231
+ raise DomainError, "matrix is singular" if Scalar.zero?(d)
232
+ target.unchecked(adjugate.entries.map { |r| r.map { |e| Scalar.div(e, d) } })
233
+ end
234
+ end
235
+ alias inv inverse
236
+
237
+ def cofactor(i, j)
238
+ minor = entries.reject.with_index { |_, r| r == i }.map { |r| r.reject.with_index { |_, c| c == j } }
239
+ d = Elimination.cofactor_det(minor).expand
240
+ (i + j).even? ? d : Scalar.neg(d)
241
+ end
242
+
243
+ def adjugate
244
+ raise ArgumentError, "adjugate needs a square matrix" unless square?
245
+ space.unchecked(Array.new(rows) { |i| Array.new(cols) { |j| cofactor(j, i) } })
246
+ end
247
+
248
+ def invertible? = square? && rank == rows
249
+
250
+ # Solve A*x = b. Free variables are set to zero; use #kernel for the rest.
251
+ # A square Integer or Rational A that is not singular goes to the primes
252
+ # or to Dixon's p-adic lifting (Multimodular.solver chooses; algorithm:
253
+ # :multimodular, :dixon or :elimination names one).
254
+ def solve(b, algorithm: :auto)
255
+ b = b.entries if b.is_a?(Vector)
256
+ raise ArgumentError, "right-hand side needs #{rows} entries" unless b.size == rows
257
+ if square? && (solver = Multimodular.solver(entries, b, algorithm)) && (y = solver.solve(entries, b))
258
+ return VectorSpace.new(base.fraction_field, cols).unchecked(y)
259
+ end
260
+ if square? && !numeric? && (y = PolyMatrix.solve(entries, b.map { |e| Scalar.lift(e) }))
261
+ return VectorSpace.new(base.fraction_field, cols).unchecked(y)
262
+ end
263
+ augmented = entries.each_with_index.map { |r, i| r + [Scalar.lift(b[i])] }
264
+ reduced, pivots = Elimination.rref(augmented)
265
+ pivots = pivots.reject { |c| c == cols }
266
+ reduced.each do |r|
267
+ if r.take(cols).all? { |e| Scalar.zero?(e) } && !Scalar.zero?(r[cols])
268
+ raise DomainError, "system is inconsistent"
269
+ end
270
+ end
271
+ x = Array.new(cols) { Num.new(0) }
272
+ pivots.each_with_index { |c, i| x[c] = reduced[i][cols] }
273
+ VectorSpace.new(base.fraction_field, cols).unchecked(x)
274
+ end
275
+
276
+ # Basis of the null space, as vectors over the fraction field.
277
+ def kernel
278
+ space_k = VectorSpace.new(base.fraction_field, cols)
279
+ if !numeric? && (basis = PolyMatrix.kernel(entries))
280
+ return basis.map { |v| space_k.unchecked(v) }
281
+ end
282
+ reduced, pivots = Elimination.rref(entries)
283
+ free = (0...cols).to_a - pivots
284
+ space_k = VectorSpace.new(base.fraction_field, cols)
285
+ free.map do |f|
286
+ v = Array.new(cols) { Num.new(0) }
287
+ v[f] = Num.new(1)
288
+ pivots.each_with_index { |p, i| v[p] = Scalar.neg(reduced[i][f]) }
289
+ space_k.unchecked(v)
290
+ end
291
+ end
292
+ alias nullspace kernel
293
+
294
+ # Characteristic polynomial det(t*I - A) as an element of base[t].
295
+ # The variable is x unless the entries use x themselves, and then the
296
+ # first of lambda, t, s that they do not: det(x*I - A) with the x of A
297
+ # inside collapsed [[x, 1], [1, x]] to -1 (third review, L11). A name
298
+ # given explicitly that the entries use is refused.
299
+ def charpoly(var = nil)
300
+ raise ArgumentError, "charpoly needs a square matrix" unless square?
301
+ used = entries.flatten.flat_map { |e| Expression.lift(e).variables }.uniq
302
+ if var.nil?
303
+ var = %i[x lambda t s].find { |name| !used.include?(name) } || Expression.fresh_variable(:x, used).name
304
+ elsif used.include?(var.to_sym)
305
+ raise ArgumentError, "charpoly: the entries already use #{var}; name another variable"
306
+ end
307
+ t = Var.new(var)
308
+ shifted = Array.new(rows) { |i| Array.new(cols) { |j| i == j ? Scalar.sub(t, self[i, j]) : Scalar.neg(self[i, j]) } }
309
+ ring = (base.ring? ? base : ZZ)[var]
310
+ ring = base[var] if base.is_a?(FiniteField)
311
+ ring.call(PolyMatrix.det(shifted) || Elimination.cofactor_det(shifted).expand)
312
+ end
313
+
314
+ # ---- eigenvalues ------------------------------------------------------
315
+
316
+ # Eigenvalues with multiplicity, exact when the characteristic polynomial
317
+ # factors into pieces of degree <= 2 over QQ, numeric otherwise.
318
+ def eigenvalues
319
+ p = charpoly(:_l)
320
+ return p.roots if base.is_a?(FiniteField)
321
+ # Over a polynomial ring the charpoly lives in QQ[x, _l], where
322
+ # coeff(k) reads an exponent vector and the total degree is not the
323
+ # one in _l: QQ[x].matrix([[x, 1], [1, x]]) had no eigenvalues.
324
+ coefficients = (0..p.degree(:_l)).map { |k| p.coefficient_in(:_l, k).to_expr }
325
+ Solve.polynomial_roots(coefficients)
326
+ end
327
+
328
+ # [[eigenvalue, multiplicity, [basis of the eigenspace]], ...]
329
+ def eigenvectors
330
+ values = eigenvalues
331
+ Solve.dedupe(values).map do |l|
332
+ shifted = self - space.identity.scale(l)
333
+ vectors = if floating?
334
+ float_kernel(shifted, Matrix.float_scale(Matrix.float_rows(entries)))
335
+ else
336
+ shifted.kernel.map { |v| v.space.unchecked(v.entries.map { |e| e.rationalize }) }
337
+ end
338
+ [l, values.count { |v| v == l }, vectors]
339
+ end
340
+ end
341
+
342
+ def floating? = entries.flatten.any? { |e| e.is_a?(Num) && (e.value.is_a?(Float) || (e.value.is_a?(Complex) && e.value.real.is_a?(Float))) }
343
+
344
+ # The kernel of a Float matrix, by elimination with partial pivoting and
345
+ # a pivot counted as zero below rounding relative to the matrix: A - l*I
346
+ # at a Float eigenvalue is singular only up to its last digits, and the
347
+ # exact kernel of it was empty (third review, L10). Relative to the
348
+ # matrix it came from, and to nothing else: max(|A|, 1) made the
349
+ # tolerance absolute for a small matrix, and a Jordan block of size
350
+ # 1e-12 was diagonalizable (fourth review).
351
+ def float_kernel(m, scale = nil)
352
+ rows = Matrix.float_rows(m.entries)
353
+ tolerance = (scale || Matrix.float_scale(rows)) * FLOAT_TOLERANCE
354
+ reduced, pivots = Matrix.float_eliminate(rows, tolerance)
355
+ n = cols
356
+ free = (0...n).to_a - pivots
357
+ space_v = VectorSpace.new(base, n)
358
+ free.map do |f|
359
+ v = Array.new(n, Complex(0.0))
360
+ v[f] = Complex(1.0)
361
+ pivots.each_with_index { |c, i| v[c] = -reduced[i][f] }
362
+ space_v.unchecked(v.map { |z| Num.new(z.imaginary.abs <= tolerance ? z.real : z) })
363
+ end
364
+ end
365
+
366
+ FLOAT_TOLERANCE = 1e-9
367
+
368
+ # The rank of a symbolic matrix is the generic one, and full rank at a
369
+ # single rational point proves it: a maximal minor that is not 0 there
370
+ # is not the zero function. The elimination over rational functions
371
+ # swells (a generic 5x5 ran for minutes: fourth review, section 4), so
372
+ # it is left for a matrix that is deficient at the point. nil otherwise.
373
+ def full_rank_at_a_point
374
+ return nil if floating?
375
+ names = entries.flatten.flat_map { |e| Expression.lift(e).variables }.uniq
376
+ return nil if names.empty?
377
+ point = names.each_with_index.to_h { |name, i| [name, Num.new(Rational(Scalar::PRIMES[i % Scalar::PRIMES.size] + i, 7 + 2 * i))] }
378
+ values = entries.map { |row| row.map { |e| Expression.lift(e).subs(point).simplify } }
379
+ return nil unless values.flatten.all? { |v| v.is_a?(Num) && (v.value.is_a?(Integer) || v.value.is_a?(Rational)) }
380
+ full = [rows, cols].min
381
+ Elimination.rref(values).last.size == full ? full : nil
382
+ rescue ZeroDivisionError
383
+ nil
384
+ end
385
+
386
+ def self.float_rows(entries) = entries.map { |row| row.map { |e| Complex(Expression.lift(e).evalf) } }
387
+ def self.float_scale(rows) = rows.flatten.map(&:abs).max || 0.0
388
+
389
+ # Gauss-Jordan with partial pivoting on Complex Floats; [rows, pivots].
390
+ def self.float_eliminate(rows, tolerance)
391
+ rows = rows.map(&:dup)
392
+ pivots = []
393
+ r = 0
394
+ (0...(rows.first&.size || 0)).each do |c|
395
+ break if r >= rows.size
396
+ best = (r...rows.size).max_by { |i| rows[i][c].abs }
397
+ next if rows[best][c].abs <= tolerance
398
+ rows[r], rows[best] = rows[best], rows[r]
399
+ pivot = rows[r][c]
400
+ rows[r] = rows[r].map { |v| v / pivot }
401
+ rows.each_index do |i|
402
+ next if i == r || rows[i][c].zero?
403
+ factor = rows[i][c]
404
+ rows[i] = rows[i].each_with_index.map { |v, j| v - factor * rows[r][j] }
405
+ end
406
+ pivots << c
407
+ r += 1
408
+ end
409
+ [rows, pivots]
410
+ end
411
+
412
+ def diagonalizable? = eigenvectors.sum { |_, _, vs| vs.size } == rows
413
+
414
+ # ---- misc -------------------------------------------------------------
415
+
416
+ def numeric? = entries.flatten.all? { |e| Scalar.numeric?(e) }
417
+ def zero? = entries.flatten.all? { |e| Scalar.zero?(e) }
418
+ def identity? = square? && self == space.identity
419
+ def symmetric? = square? && self == transpose
420
+
421
+ def ==(other)
422
+ other = space.unchecked(other) if other.is_a?(Array)
423
+ other.is_a?(Matrix) && other.rows == rows && other.cols == cols &&
424
+ entries.flatten.zip(other.entries.flatten).all? { |a, b| Scalar.zero?(Scalar.sub(a, b)) }
425
+ end
426
+ alias eql? ==
427
+ def hash = [Matrix, entries.flatten.map(&:simplify)].hash
428
+
429
+ def simplify = space.unchecked(map_entries(&:simplify))
430
+ def subs(*args) = space.unchecked(map_entries { |e| e.subs(*args) })
431
+ def call(**bindings) = space.unchecked(map_entries { |e| Scalar.lift(e.call(**bindings)) })
432
+
433
+ def to_s
434
+ return "[]" if rows.zero? || cols.zero?
435
+ cells = entries.map { |r| r.map(&:to_s) }
436
+ widths = (0...cols).map { |j| cells.map { |r| r[j].size }.max }
437
+ cells.map { |r| "[" + r.each_with_index.map { |c, j| c.rjust(widths[j]) }.join(" ") + "]" }.join("\n")
438
+ end
439
+ alias inspect to_s
440
+
441
+ private
442
+
443
+ def map_entries(&block) = entries.map { |r| r.map(&block) }
444
+
445
+ def zip_with(other, op)
446
+ raise TypeError, "can't apply #{op} to Matrix and #{other.class}" unless other.is_a?(Matrix)
447
+ raise ArgumentError, "shape mismatch: #{space} #{op} #{other.space}" unless rows == other.rows && cols == other.cols
448
+ MatrixSpace.new(base.join(other.base), rows, cols).unchecked(
449
+ entries.zip(other.entries).map { |ra, rb| ra.zip(rb).map { |a, b| yield a, b } }
450
+ )
451
+ end
452
+
453
+ def result_base(scalar)
454
+ d = Scalar.domain(scalar)
455
+ d ? base.join(d) : base
456
+ end
457
+ end
458
+
459
+ # Row reduction on plain arrays of Expressions. Symbolic entries that are
460
+ # not identically zero are treated as non-zero pivots (the generic case).
461
+ module Elimination
462
+ module_function
463
+
464
+ # => [reduced_rows, pivot_columns]
465
+ def rref(rows)
466
+ rows = rows.map(&:dup)
467
+ m = rows.size
468
+ n = rows.first&.size || 0
469
+ pivots = []
470
+ r = 0
471
+ n.times do |c|
472
+ break if r == m
473
+ p = (r...m).find { |i| !Scalar.zero?(rows[i][c]) }
474
+ next unless p
475
+ rows[r], rows[p] = rows[p], rows[r]
476
+ pivot = rows[r][c]
477
+ rows[r] = rows[r].map { |v| Scalar.div(v, pivot) } unless Scalar.one?(pivot)
478
+ m.times do |i|
479
+ next if i == r
480
+ f = rows[i][c]
481
+ next if Scalar.zero?(f)
482
+ rows[i] = rows[i].zip(rows[r]).map { |a, b| Scalar.sub(a, Scalar.mul(f, b)) }
483
+ end
484
+ pivots << c
485
+ r += 1
486
+ end
487
+ # an entry that is zero but not written so reads as a pivot:
488
+ # (x - 1)**2 - (x - 1)*(x**2 - 1)/(x + 1) is 0 (fourth review)
489
+ rows = rows.map { |row| row.map { |v| !v.is_a?(Num) && v.is_a?(Expression) && Scalar.zero?(v) ? Num.new(0) : v } }
490
+ [rows, pivots]
491
+ end
492
+
493
+ # Determinant by Gaussian elimination (exact for numeric entries).
494
+ def det(rows)
495
+ rows = rows.map(&:dup)
496
+ n = rows.size
497
+ result = Num.new(1)
498
+ n.times do |c|
499
+ p = (c...n).find { |i| !Scalar.zero?(rows[i][c]) }
500
+ return Num.new(0) unless p
501
+ if p != c
502
+ rows[c], rows[p] = rows[p], rows[c]
503
+ result = Scalar.neg(result)
504
+ end
505
+ pivot = rows[c][c]
506
+ result = Scalar.mul(result, pivot)
507
+ ((c + 1)...n).each do |i|
508
+ f = Scalar.div(rows[i][c], pivot)
509
+ next if Scalar.zero?(f)
510
+ rows[i] = rows[i].zip(rows[c]).map { |a, b| Scalar.sub(a, Scalar.mul(f, b)) }
511
+ end
512
+ end
513
+ result
514
+ end
515
+
516
+ # Laplace expansion along the first row; fine for small symbolic matrices.
517
+ def cofactor_det(rows)
518
+ n = rows.size
519
+ return Num.new(1) if n.zero?
520
+ return rows[0][0] if n == 1
521
+ total = Num.new(0)
522
+ rows[0].each_with_index do |a, j|
523
+ next if Scalar.zero?(a)
524
+ minor = rows.drop(1).map { |r| r.reject.with_index { |_, k| k == j } }
525
+ term = Scalar.mul(a, cofactor_det(minor))
526
+ total = j.even? ? Scalar.add(total, term) : Scalar.sub(total, term)
527
+ end
528
+ total
529
+ end
530
+ end
531
+ end