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,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # Lattice basis reduction: the LLL algorithm, exactly.
5
+ #
6
+ # lll(matrix([[1, 1, 1], [-1, 0, 2], [3, 5, 6]])) # rows (0, 1, 0), (1, 0, 1), (-1, 0, 2)
7
+ # b, u = lll(m, transform: true) # u*m == b, u unimodular
8
+ # m.lll(delta: 99/100r)
9
+ #
10
+ # A lattice is every integer combination of some linearly independent
11
+ # vectors, its basis; many bases give the same lattice, and most of them
12
+ # are long and nearly parallel. A reduced basis is short and nearly
13
+ # orthogonal. Its first vector is at most 2**((n - 1)/2) times as long as
14
+ # the shortest non-zero vector of the lattice (for delta = 3/4), which is
15
+ # what makes the algorithm useful far beyond lattices: integer relations
16
+ # between real numbers, minimal polynomials from a decimal expansion,
17
+ # simultaneous approximation, knapsacks.
18
+ #
19
+ # The vectors are the rows, as in the literature. The arithmetic is exact
20
+ # (Rational), so the Gram-Schmidt coefficients are kept and updated rather
21
+ # than recomputed, as in Cohen's formulation: size reduction touches one
22
+ # row of mu, and a swap changes two rows and two columns of it. The price
23
+ # of exactness is the size of those rationals; the integral version
24
+ # [Coh93, Algorithm 2.6.7] keeps them as integers, and floating-point
25
+ # variants trade exactness for speed, neither of which rcas needs at the
26
+ # sizes a student reduces.
27
+ #
28
+ # Sources (keys: MANUAL.md, Sources): the algorithm and its bound
29
+ # [LLL82]; the formulation with the incremental Gram-Schmidt update
30
+ # [Coh93, §2.6, Algorithm 2.6.3]; [vzGG13, ch. 16] for the analysis.
31
+ module Lattice
32
+ DELTA = Rational(3, 4)
33
+
34
+ module_function
35
+
36
+ # The reduced basis in the shape it came in: a Matrix for a Matrix, an
37
+ # Array of vectors otherwise; with transform: true also the unimodular
38
+ # matrix U with U*B equal to the result.
39
+ def lll(basis, delta: DELTA, transform: false)
40
+ rows, shape = rows_of(basis)
41
+ delta = rational(delta) { "lll: delta must be a rational number, got #{delta.inspect}" }
42
+ raise ArgumentError, "lll: delta must lie in (1/4, 1], got #{delta}" unless delta > Rational(1, 4) && delta <= 1
43
+ reduced, u = reduce(rows, delta)
44
+ out = shape.call(reduced)
45
+ transform ? [out, RCAS.matrix(u)] : out
46
+ end
47
+
48
+ # Cohen's Algorithm 2.6.3. mu[i][j] (j < i) are the Gram-Schmidt
49
+ # coefficients and bb[i] the squared lengths of the orthogonalised
50
+ # vectors; k is the first vector not yet known to be reduced against
51
+ # the ones before it.
52
+ def reduce(rows, delta)
53
+ b = rows.map(&:dup)
54
+ n = b.size
55
+ u = Array.new(n) { |i| Array.new(n) { |j| i == j ? 1 : 0 } }
56
+ mu, bb = gram_schmidt(b)
57
+ if (i = bb.index(&:zero?))
58
+ raise ArgumentError, "lll: the vectors are linearly dependent (vector #{i + 1} lies in the span of the ones before it); LLL reduces a basis"
59
+ end
60
+ k = 1
61
+ while k < n
62
+ size_reduce(b, u, mu, k, k - 1)
63
+ # Lovasz's condition: the orthogonalised k-th vector is not much
64
+ # shorter than the one before it
65
+ if bb[k] >= (delta - mu[k][k - 1]**2) * bb[k - 1]
66
+ (k - 2).downto(0) { |l| size_reduce(b, u, mu, k, l) }
67
+ k += 1
68
+ else
69
+ swap(b, u, mu, bb, k)
70
+ k = [k - 1, 1].max
71
+ end
72
+ end
73
+ [b, u]
74
+ end
75
+
76
+ # b_k -= q*b_l with q the integer nearest mu[k][l], which leaves
77
+ # |mu[k][l]| <= 1/2; mu[k][j] for j < l changes with it.
78
+ def size_reduce(b, u, mu, k, l)
79
+ q = mu[k][l].round
80
+ return if q.zero?
81
+ b[k] = b[k].zip(b[l]).map { |x, y| x - q * y }
82
+ u[k] = u[k].zip(u[l]).map { |x, y| x - q * y }
83
+ mu[k][l] -= q
84
+ (0...l).each { |j| mu[k][j] -= q * mu[l][j] }
85
+ end
86
+
87
+ # Exchange b_(k-1) and b_k and update the Gram-Schmidt data in place
88
+ # [Coh93, Algorithm 2.6.3, step 3]: only the two orthogonalised vectors
89
+ # involved change, and with them rows k-1, k and columns k-1, k of mu.
90
+ def swap(b, u, mu, bb, k)
91
+ b[k], b[k - 1] = b[k - 1], b[k]
92
+ u[k], u[k - 1] = u[k - 1], u[k]
93
+ (0...k - 1).each { |j| mu[k][j], mu[k - 1][j] = mu[k - 1][j], mu[k][j] }
94
+ m = mu[k][k - 1]
95
+ big = bb[k] + m * m * bb[k - 1]
96
+ mu[k][k - 1] = m * bb[k - 1] / big
97
+ bb[k] = bb[k - 1] * bb[k] / big
98
+ bb[k - 1] = big
99
+ (k + 1...b.size).each do |i|
100
+ t = mu[i][k]
101
+ mu[i][k] = mu[i][k - 1] - m * t
102
+ mu[i][k - 1] = t + mu[k][k - 1] * mu[i][k]
103
+ end
104
+ end
105
+
106
+ # The Gram-Schmidt coefficients and squared lengths, exactly.
107
+ def gram_schmidt(b)
108
+ n = b.size
109
+ mu = Array.new(n) { Array.new(n, 0r) }
110
+ bb = Array.new(n, 0r)
111
+ star = []
112
+ b.each_with_index do |v, i|
113
+ w = v.dup
114
+ (0...i).each do |j|
115
+ next if bb[j].zero?
116
+ mu[i][j] = dot(v, star[j]) / bb[j]
117
+ w = w.zip(star[j]).map { |x, y| x - mu[i][j] * y }
118
+ end
119
+ star << w
120
+ bb[i] = dot(w, w)
121
+ end
122
+ [mu, bb]
123
+ end
124
+
125
+ # Whether the rows are LLL-reduced: size-reduced (|mu| <= 1/2) and
126
+ # Lovasz's condition between neighbours. The tests ask this of every
127
+ # answer.
128
+ def reduced?(rows, delta: DELTA)
129
+ rows = rows_of(rows).first
130
+ mu, bb = gram_schmidt(rows)
131
+ size = (1...rows.size).all? { |i| (0...i).all? { |j| mu[i][j].abs <= Rational(1, 2) } }
132
+ size && (1...rows.size).all? { |k| bb[k] >= (Rational(delta) - mu[k][k - 1]**2) * bb[k - 1] }
133
+ end
134
+
135
+ def dot(v, w) = v.zip(w).sum { |x, y| x * y }
136
+
137
+ # [rows as Arrays of Rationals, a lambda giving the answer its shape]
138
+ def rows_of(basis)
139
+ rows, shape =
140
+ case basis
141
+ when Matrix then [basis.to_a, ->(r) { RCAS.matrix(numbers(r)) }]
142
+ when Array
143
+ vectors = basis.map { |v| v.is_a?(Vector) ? v.entries : v }
144
+ raise ArgumentError, "lll: expected a matrix or a list of vectors, got #{basis.inspect}" unless vectors.all?(Array)
145
+ [vectors, ->(r) { numbers(r).map { |row| RCAS.vector(*row) } }]
146
+ else raise ArgumentError, "lll: expected a matrix or a list of vectors, got #{basis.inspect}"
147
+ end
148
+ raise ArgumentError, "lll: no vectors to reduce" if rows.empty?
149
+ raise ArgumentError, "lll: the vectors have different lengths" unless rows.map(&:size).uniq.size == 1
150
+ entries = rows.map do |row|
151
+ row.map do |e|
152
+ rational(e) { "lll: the entries must be rational numbers, got #{e}; scale and round a real lattice first" }
153
+ end
154
+ end
155
+ [entries, shape]
156
+ end
157
+
158
+ def rational(e)
159
+ e = e.value if e.is_a?(Num)
160
+ e = Expression.lift(e).simplify.then { |s| s.is_a?(Num) ? s.value : s } if e.is_a?(Expression)
161
+ return e.to_r if e.is_a?(Integer) || e.is_a?(Rational)
162
+ raise ArgumentError, yield
163
+ end
164
+
165
+ def numbers(rows) = rows.map { |row| row.map { |x| Simplify.normalize_number(x) } }
166
+ end
167
+
168
+ class Matrix
169
+ # The LLL-reduced basis of the lattice spanned by the rows; see Lattice.
170
+ def lll(delta: Lattice::DELTA, transform: false) = Lattice.lll(self, delta: delta, transform: transform)
171
+ end
172
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RCAS
4
+ # The second course in linear algebra: orthogonality and least squares.
5
+ #
6
+ # gram_schmidt([vector(1, 1, 0), vector(1, 0, 1)]) # orthogonal basis
7
+ # gram_schmidt(vs, normalize: true) # orthonormal
8
+ # least_squares(matrix([[1, 1], [1, 2], [1, 3]]), vector(1, 2, 4))
9
+ # project(vector(1, 2), onto: vector(1, 0))
10
+ #
11
+ # Exact throughout, so an orthonormal basis keeps its square roots.
12
+ #
13
+ # Sources (keys: MANUAL.md, Sources): Gram-Schmidt and the normal equations
14
+ # as in [Str16, ch. 4]; the numerically stabler QR route is not used because
15
+ # the arithmetic here is exact.
16
+ module LinearAlgebra
17
+ module_function
18
+
19
+ # The inner product, Hermitian where an entry carries an explicit
20
+ # complex number: (1, i).(1, i) is 1 + i*i = 0 bilinearly, and |(1, i)|
21
+ # is sqrt(2), not 0 (third review, L12). Symbolic entries are taken as
22
+ # they stand, so the real case is unchanged.
23
+ def dot(u, v)
24
+ same_length!(u, v, "dot")
25
+ u.entries.zip(v.entries).map { |a, b| Scalar.mul(a, conjugate(b)) }.reduce { |x, y| Scalar.add(x, y) }.simplify
26
+ end
27
+
28
+ # zip drops the extra coordinates of the longer vector, which made
29
+ # project((1, 2), onto: (1, 0, 3)) a plausible (1/10, 0) (a review,
30
+ # 23 Sept 2026): vectors of two different spaces have no inner product.
31
+ def same_length!(u, v, name)
32
+ return if u.entries.size == v.entries.size
33
+ raise ArgumentError, "#{name}: the vectors have #{u.entries.size} and #{v.entries.size} entries; they must lie in the same space"
34
+ end
35
+
36
+ def conjugate(e)
37
+ e = Expression.lift(e)
38
+ return e unless e.each_node.any? { |n| n.is_a?(Num) && n.value.is_a?(Complex) }
39
+ ComplexParts.conj(e)
40
+ end
41
+
42
+ def norm(v) = RCAS.sqrt(dot(v, v)).simplify
43
+
44
+ def scale(v, factor) = v.space.unchecked(v.entries.map { |e| (e * factor).simplify })
45
+
46
+ def subtract(u, v)
47
+ same_length!(u, v, "subtract")
48
+ u.space.unchecked(u.entries.zip(v.entries).map { |a, b| (a - b).simplify })
49
+ end
50
+
51
+ # The projection of v onto a vector or onto the span of several.
52
+ #
53
+ # The sum of the single projections is the projection onto the span only
54
+ # when the targets are pairwise orthogonal; a slanted pair has to be
55
+ # orthogonalised first. project(e1, onto: [e1, e1 + e2]) is e1 - those
56
+ # two span the plane, so the projection onto it is the identity - and
57
+ # was (3/2, 1/2) before (22 Sept 2026, from a review). Dependent
58
+ # generators drop out on the way, as they do in gram_schmidt.
59
+ def project(v, onto:)
60
+ targets = onto.is_a?(Array) ? onto : [onto]
61
+ targets.each do |u|
62
+ raise ArgumentError, "project: cannot project onto the zero vector" if Scalar.zero?(dot(u, u))
63
+ end
64
+ targets = gram_schmidt(targets) unless orthogonal_family?(targets)
65
+ onto_orthogonal(v, targets)
66
+ end
67
+
68
+ def orthogonal_family?(targets) = targets.combination(2).all? { |u, w| orthogonal?(u, w) }
69
+
70
+ # The sum of the single projections, which is the projection itself once
71
+ # the targets are known to be pairwise orthogonal. gram_schmidt builds
72
+ # such a family as it goes and calls this directly, so that project does
73
+ # not orthogonalise what it has just orthogonalised.
74
+ def onto_orthogonal(v, targets)
75
+ targets.reduce(v.space.unchecked(Array.new(v.entries.size, Num.new(0)))) do |sum, u|
76
+ add(sum, scale(u, (dot(v, u) / dot(u, u)).simplify))
77
+ end
78
+ end
79
+
80
+ def add(u, v)
81
+ same_length!(u, v, "add")
82
+ u.space.unchecked(u.entries.zip(v.entries).map { |a, b| (a + b).simplify })
83
+ end
84
+
85
+ # An orthogonal (or orthonormal) basis of the same span; dependent
86
+ # vectors drop out rather than appearing as zeros.
87
+ def gram_schmidt(vectors, normalize: false)
88
+ basis = []
89
+ Array(vectors).each do |v|
90
+ w = basis.empty? ? v : subtract(v, onto_orthogonal(v, basis))
91
+ next if w.entries.all? { |e| Scalar.zero?(e) }
92
+ basis << w
93
+ end
94
+ return basis unless normalize
95
+ basis.map { |w| scale(w, (1 / norm(w)).simplify) }
96
+ end
97
+
98
+ # The x minimising |A x - b|, from the normal equations A* A x = A* b
99
+ # with A* the conjugate transpose. The plain transpose is right only for
100
+ # real entries: for A = (1, 2i)' it gave -1/3 where the minimiser of
101
+ # |z - 1|**2 + |2iz|**2 is 1/5, and (1, i)' had "dependent columns"
102
+ # (a review, 23 Sept 2026). `conjugate` leaves a symbolic entry alone,
103
+ # as `dot` does, so the real case is unchanged.
104
+ def least_squares(matrix, target)
105
+ unless target.entries.size == matrix.rows
106
+ raise ArgumentError, "least_squares: the matrix has #{matrix.rows} rows and the vector #{target.entries.size} entries"
107
+ end
108
+ adjoint = RCAS.matrix(matrix.transpose.to_a.map { |row| row.map { |e| conjugate(e) } })
109
+ normal = adjoint * matrix
110
+ right = adjoint * target
111
+ raise ArgumentError, "least_squares: the columns are dependent, so the solution is not unique" if Scalar.zero?(normal.det)
112
+ normal.solve(right.entries)
113
+ end
114
+
115
+ def orthogonal?(u, v) = Scalar.zero?(dot(u, v))
116
+ end
117
+ end