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/matrix.rb
ADDED
|
@@ -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
|