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/trig.rb
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Trigonometric and logarithmic rewriting: expand_trig, trigsimp,
|
|
5
|
+
# expand_log, logcombine.
|
|
6
|
+
module Trigonometry
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# sin(a + b), cos(a + b) and integer multiples written out.
|
|
10
|
+
def expand_trig(expr)
|
|
11
|
+
expr = Expression.lift(expr).map_children { |c| expand_trig(c) }
|
|
12
|
+
return expr unless expr.is_a?(Fn) && %i[sin cos tan].include?(expr.name) && expr.args.size == 1
|
|
13
|
+
return (expand_trig(Fn.new(:sin, expr.args)) / expand_trig(Fn.new(:cos, expr.args))).simplify if expr.name == :tan
|
|
14
|
+
|
|
15
|
+
arg = expr.args.first.simplify
|
|
16
|
+
constant, terms = Simplify.termize(arg)
|
|
17
|
+
addends = terms.map { |factors, coeff| Simplify.rebuild_product(coeff, factors) }
|
|
18
|
+
addends << Num.new(constant) unless constant.zero?
|
|
19
|
+
if addends.size >= 2
|
|
20
|
+
a = addends.first
|
|
21
|
+
b = addends.drop(1).reduce { |acc, t| acc + t }
|
|
22
|
+
return addition_formula(expr.name, a, b)
|
|
23
|
+
end
|
|
24
|
+
coeff, factors = Simplify.factorize(arg)
|
|
25
|
+
if coeff.is_a?(Integer) && coeff.abs >= 2 && !factors.empty?
|
|
26
|
+
return multiple_angle(expr.name, coeff, Simplify.rebuild_product(1, factors))
|
|
27
|
+
end
|
|
28
|
+
expr
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# sin(n*u) and cos(n*u) by de Moivre, (cos u + i sin u)**n [AS64, 4.3.
|
|
32
|
+
# 29-30]: one binomial sum. Peeling one u at a time and expanding sin
|
|
33
|
+
# and cos of the rest separately doubled the work per multiple, and
|
|
34
|
+
# sin(16*x) took nine seconds (third review, section 5).
|
|
35
|
+
def multiple_angle(name, n, u)
|
|
36
|
+
s = expand_trig(Fn.new(:sin, [u]))
|
|
37
|
+
c = expand_trig(Fn.new(:cos, [u]))
|
|
38
|
+
m = n.abs
|
|
39
|
+
terms = (0..m).filter_map do |k|
|
|
40
|
+
next nil if name == :sin ? k.even? : k.odd?
|
|
41
|
+
sign = (-1)**(k / 2)
|
|
42
|
+
Num.new(sign * (0...k).reduce(1) { |acc, i| acc * (m - i) } / (1..k).reduce(1, :*)) * c**(m - k) * s**k
|
|
43
|
+
end
|
|
44
|
+
total = terms.reduce(:+).expand
|
|
45
|
+
name == :sin && n.negative? ? Simplify.negate(total).expand : total
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def addition_formula(name, a, b)
|
|
49
|
+
sa, ca = expand_trig(Fn.new(:sin, [a])), expand_trig(Fn.new(:cos, [a]))
|
|
50
|
+
sb, cb = expand_trig(Fn.new(:sin, [b])), expand_trig(Fn.new(:cos, [b]))
|
|
51
|
+
case name
|
|
52
|
+
when :sin then (sa * cb + ca * sb).expand
|
|
53
|
+
when :cos then (ca * cb - sa * sb).expand
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# sin and cos repeat every 2*pi, tan and cot every pi, so an added term
|
|
58
|
+
# that is an integer multiple of the period drops out of the argument:
|
|
59
|
+
# sin(pi/6 + 2*pi*k) is sin(pi/6) for an integer k. The multiple has to
|
|
60
|
+
# be *known* to be one, which is what assume(k: ZZ) says - for an
|
|
61
|
+
# undeclared k nothing happens, because k = 1/2 would be another matter.
|
|
62
|
+
PERIODS = { sin: 2, cos: 2, tan: 1 }.freeze
|
|
63
|
+
|
|
64
|
+
def reduce_period(name, arg)
|
|
65
|
+
period = PERIODS[name]
|
|
66
|
+
return nil if period.nil?
|
|
67
|
+
constant, terms = Simplify.termize(Expression.lift(arg))
|
|
68
|
+
kept = terms.reject { |factors, coeff| whole_period?(factors, coeff, period) }
|
|
69
|
+
return nil if kept.size == terms.size
|
|
70
|
+
Simplify.rebuild_sum(constant, kept)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Is coeff * factors an integer multiple of period*pi?
|
|
74
|
+
def whole_period?(factors, coeff, period)
|
|
75
|
+
return false unless factors[PI] == 1
|
|
76
|
+
return false unless coeff.is_a?(Integer) && (coeff % period).zero?
|
|
77
|
+
rest = factors.reject { |base, _| base == PI }
|
|
78
|
+
return true if rest.empty?
|
|
79
|
+
domain = Infer.domain(Simplify.rebuild_product(1, rest))
|
|
80
|
+
!domain.nil? && domain <= ZZ
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The function whose even powers are replaced when +keep+ is kept, and
|
|
84
|
+
# what its square becomes: sin**2 + cos**2 = 1, cosh**2 - sinh**2 = 1.
|
|
85
|
+
SQUARES = {
|
|
86
|
+
sin: [:cos, ->(p) { Num.new(1) - p**2 }],
|
|
87
|
+
cos: [:sin, ->(p) { Num.new(1) - p**2 }],
|
|
88
|
+
sinh: [:cosh, ->(p) { Num.new(1) + p**2 }],
|
|
89
|
+
cosh: [:sinh, ->(p) { p**2 - Num.new(1) }]
|
|
90
|
+
}.freeze
|
|
91
|
+
|
|
92
|
+
# Shortest form among several rewritings using sin**2 + cos**2 = 1 and
|
|
93
|
+
# cosh**2 - sinh**2 = 1.
|
|
94
|
+
def trigsimp(expr)
|
|
95
|
+
base = tan_to_sin_cos(Expression.lift(expr)).simplify
|
|
96
|
+
candidates = [Expression.lift(expr).simplify, base, polynomial_cancel(base)]
|
|
97
|
+
[false, true].each do |expanded|
|
|
98
|
+
f = expanded ? expand_trig(base) : base
|
|
99
|
+
SQUARES.each_key do |keep|
|
|
100
|
+
candidates << polynomial_cancel(reduce_squares(f, keep))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
candidates.compact.map(&:simplify).min_by { |c| [c.each_node.count, c.to_s.size] }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def tan_to_sin_cos(expr)
|
|
107
|
+
expr = expr.map_children { |c| tan_to_sin_cos(c) }
|
|
108
|
+
expr.is_a?(Fn) && expr.name == :tan ? Fn.new(:sin, expr.args) / Fn.new(:cos, expr.args) : expr
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Replace even powers of the function that is not kept: cos**2 => 1 - sin**2.
|
|
112
|
+
def reduce_squares(expr, keep)
|
|
113
|
+
num, den = numerator_denominator(expr)
|
|
114
|
+
(reduce_table(num, keep) / reduce_table(den, keep)).simplify
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def numerator_denominator(expr)
|
|
118
|
+
_, table = Expand.table(expr)
|
|
119
|
+
den_factors = {}
|
|
120
|
+
table.each_key do |factors|
|
|
121
|
+
factors.each do |base, exp|
|
|
122
|
+
next unless exp.is_a?(Integer) && exp.negative?
|
|
123
|
+
den_factors[base] = [den_factors[base] || 0, -exp].max
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
den = Simplify.rebuild_product(1, den_factors)
|
|
127
|
+
[(expr * den).expand, den]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def reduce_table(expr, keep)
|
|
131
|
+
victim, square = SQUARES[keep]
|
|
132
|
+
constant, table = Expand.table(expr)
|
|
133
|
+
result = Num.new(constant)
|
|
134
|
+
table.each do |factors, coeff|
|
|
135
|
+
reducible = factors.select { |base, exp| base.is_a?(Fn) && base.name == victim && exp.is_a?(Integer) && exp >= 2 }
|
|
136
|
+
term = Simplify.rebuild_product(coeff, factors.reject { |b, _| reducible.key?(b) })
|
|
137
|
+
reducible.each do |base, exp|
|
|
138
|
+
partner = Fn.new(keep, [base.args.first])
|
|
139
|
+
term = (term * square.call(partner)**(exp / 2) * base**(exp % 2)).expand
|
|
140
|
+
end
|
|
141
|
+
result += term
|
|
142
|
+
end
|
|
143
|
+
result.expand
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Cancel common factors treating each function application as a variable.
|
|
147
|
+
def polynomial_cancel(expr)
|
|
148
|
+
atoms = expr.each_node.select { |n| n.is_a?(Fn) }.uniq
|
|
149
|
+
return expr.cancel if atoms.empty?
|
|
150
|
+
names = atoms.each_with_index.to_h { |a, i| [a, Var.new(:"_f#{i}")] }
|
|
151
|
+
substituted = expr.subs(names)
|
|
152
|
+
return nil if substituted.each_node.any? { |n| n.is_a?(Fn) }
|
|
153
|
+
substituted.cancel.subs(names.invert)
|
|
154
|
+
rescue DomainError, NotImplementedError, RCAS::Unsupported
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# log(a*b**n/c) => log(a) + n*log(b) - log(c), for the factors it holds
|
|
159
|
+
# for: log(p*u) = log(p) + log(u) and log(p**n) = n*log(p) when p > 0,
|
|
160
|
+
# which is proved (Decide for a constant, the assumptions for a symbol).
|
|
161
|
+
# The fifth review's decision: the rules no longer assume an undeclared
|
|
162
|
+
# argument positive - log(x**2) is 0 at x = -1 and 2*log(x) is 2*i*pi.
|
|
163
|
+
# A factor whose sign is declared negative is taken by its absolute value
|
|
164
|
+
# when it is all there is, with the i*pi an odd number of them leaves
|
|
165
|
+
# (third review, C4, A5). `force: true` is the textbook manipulation,
|
|
166
|
+
# every argument taken as positive (SymPy's name for MuPAD's
|
|
167
|
+
# IgnoreAnalyticConstraints).
|
|
168
|
+
def expand_log(expr, force: false)
|
|
169
|
+
expr = Expression.lift(expr).map_children { |c| expand_log(c, force: force) }
|
|
170
|
+
return expr unless expr.is_a?(Fn) && expr.name == :log && expr.args.size == 1
|
|
171
|
+
coeff, factors = Simplify.factorize(expr.args.first.simplify)
|
|
172
|
+
return expr if factors.empty? || (factors.size == 1 && factors.values.first == 1 && coeff == 1)
|
|
173
|
+
pieces = []
|
|
174
|
+
kept = {}
|
|
175
|
+
negatives = {}
|
|
176
|
+
factors.each do |base, exp|
|
|
177
|
+
sign = base.variables.empty? ? Decide.sign(base) : RCAS.sign_of(base)
|
|
178
|
+
if sign == :negative && exp.is_a?(Integer)
|
|
179
|
+
negatives[base] = exp
|
|
180
|
+
elsif force || sign == :positive
|
|
181
|
+
pieces << Expression.lift(exp) * Fn.new(:log, [base])
|
|
182
|
+
else
|
|
183
|
+
kept[base] = exp
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
unless kept.empty?
|
|
187
|
+
# what is not proved positive stays inside one logarithm, with the
|
|
188
|
+
# negative factors and a negative coefficient
|
|
189
|
+
negatives.each { |base, exp| kept[base] = exp }
|
|
190
|
+
if coeff.is_a?(Numeric) && coeff.real? && coeff.positive?
|
|
191
|
+
pieces.concat(coefficient_logs(coeff))
|
|
192
|
+
coeff = 1
|
|
193
|
+
end
|
|
194
|
+
pieces << Fn.new(:log, [Simplify.rebuild_product(coeff, kept)])
|
|
195
|
+
return pieces.reduce { |acc, p| acc + p }.simplify
|
|
196
|
+
end
|
|
197
|
+
flips = negatives.values.sum
|
|
198
|
+
negatives.each { |base, exp| pieces << Num.new(exp) * Fn.new(:log, [Neg.new(base).simplify]) }
|
|
199
|
+
pieces << I * PI if flips.odd? && !(coeff.is_a?(Numeric) && coeff.real? && coeff.negative?)
|
|
200
|
+
if flips.odd? && coeff.is_a?(Numeric) && coeff.real? && coeff.negative?
|
|
201
|
+
coeff = -coeff # two negatives: the product is positive after all
|
|
202
|
+
end
|
|
203
|
+
if coeff.is_a?(Numeric) && coeff.real? && coeff.negative?
|
|
204
|
+
return expr if pieces.empty?
|
|
205
|
+
pieces << Fn.new(:log, [Num.new(coeff)])
|
|
206
|
+
else
|
|
207
|
+
pieces.concat(coefficient_logs(coeff))
|
|
208
|
+
end
|
|
209
|
+
pieces.reduce { |acc, p| acc + p }.simplify
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def coefficient_logs(coeff)
|
|
213
|
+
return [] if coeff == 1
|
|
214
|
+
return [Fn.new(:log, [Num.new(coeff)])] unless coeff.is_a?(Rational)
|
|
215
|
+
logs = []
|
|
216
|
+
logs << Fn.new(:log, [Num.new(coeff.numerator)]) unless coeff.numerator == 1
|
|
217
|
+
logs << -Fn.new(:log, [Num.new(coeff.denominator)])
|
|
218
|
+
logs
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# a*log(u) + b*log(v) => log(u**a * v**b) for rational a, b - for the
|
|
222
|
+
# arguments it holds for: positive ones, proved (2*log(-1) is 2*i*pi and
|
|
223
|
+
# log((-1)**2) is 0, and 3*log(i) is not log(-i): third review, A4).
|
|
224
|
+
# One argument whose sign is not known may still join the positive
|
|
225
|
+
# ones with coefficient 1, since log(p*u) = log(p) + log(u) for p > 0.
|
|
226
|
+
# Undeclared arguments are no longer assumed positive (the fifth
|
|
227
|
+
# review's decision); `force: true` combines every logarithm.
|
|
228
|
+
def logcombine(expr, force: false)
|
|
229
|
+
expr = Expression.lift(expr).simplify
|
|
230
|
+
constant, terms = Simplify.termize(expr)
|
|
231
|
+
inside = {}
|
|
232
|
+
rest = {}
|
|
233
|
+
loose = nil
|
|
234
|
+
terms.each do |factors, coeff|
|
|
235
|
+
log = factors.size == 1 && factors.values.first == 1 && factors.keys.first.is_a?(Fn) && factors.keys.first.name == :log &&
|
|
236
|
+
(coeff.is_a?(Integer) || coeff.is_a?(Rational))
|
|
237
|
+
argument = log && factors.keys.first.args.first
|
|
238
|
+
if log && (force || combinable?(argument))
|
|
239
|
+
Simplify.add_factor(inside, argument, coeff)
|
|
240
|
+
elsif log && coeff == 1 && loose.nil? && !argument.each_node.any? { |n| n.is_a?(Num) && n.value.is_a?(Complex) }
|
|
241
|
+
loose = factors
|
|
242
|
+
else
|
|
243
|
+
rest[factors] = coeff
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
if loose
|
|
247
|
+
if inside.empty?
|
|
248
|
+
rest[loose] = 1
|
|
249
|
+
else
|
|
250
|
+
Simplify.add_factor(inside, loose.keys.first.args.first, 1)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
return expr if inside.size < 2 && rest.size + inside.size == terms.size && inside.values.all? { |e| e == 1 }
|
|
254
|
+
combined = Simplify.rebuild_sum(constant, rest)
|
|
255
|
+
combined += Fn.new(:log, [Simplify.rebuild_product(1, inside)]) unless inside.empty?
|
|
256
|
+
combined.simplify
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def combinable?(u)
|
|
260
|
+
return Decide.sign(u) == :positive if u.variables.empty?
|
|
261
|
+
RCAS.sign_of(u) == :positive
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
module Factor
|
|
5
|
+
# How the p-adic factors are put back together: :van_hoeij (the lattice),
|
|
6
|
+
# :zassenhaus (subsets of increasing size, the original method), or
|
|
7
|
+
# :auto, the subsets for as long as they are cheap and the lattice for
|
|
8
|
+
# what is left.
|
|
9
|
+
#
|
|
10
|
+
# The number of local factors does not say which is faster; the size of
|
|
11
|
+
# the subsets the true factors need does. Products of small factors
|
|
12
|
+
# (x**2 - c, cubics, SD(3)) came out about twice as fast by subsets at
|
|
13
|
+
# every count from 17 to 26 local factors, because they are found among
|
|
14
|
+
# the pairs and triples and every one found shrinks the problem, while
|
|
15
|
+
# three SD(4)s (24 local factors, eight to each true factor) took 1.7 s
|
|
16
|
+
# by subsets and 0.9 s by the lattice, and SD(6) (32, all in one) more
|
|
17
|
+
# than three minutes against 3 s (24 Sept 2026). So :auto counts the
|
|
18
|
+
# subsets of the next size, C(r, s), and hands over to the lattice when
|
|
19
|
+
# that exceeds SUBSET_BUDGET - at about 4 microseconds a subset, the
|
|
20
|
+
# budget is what the lattice costs at twenty local factors. The old
|
|
21
|
+
# method stays so that the two can be compared.
|
|
22
|
+
RECOMBINATIONS = %i[auto van_hoeij zassenhaus].freeze
|
|
23
|
+
SUBSET_BUDGET = 50_000
|
|
24
|
+
|
|
25
|
+
def self.recombination = Thread.current[:rcas_recombination] || :auto
|
|
26
|
+
|
|
27
|
+
# The recombination for the calls made inside the block; nil keeps the
|
|
28
|
+
# one already in force, so an outer choice reaches the inner calls.
|
|
29
|
+
def self.with_recombination(method)
|
|
30
|
+
return yield if method.nil?
|
|
31
|
+
method = method.to_sym
|
|
32
|
+
raise ArgumentError, "factor: recombination must be one of #{RECOMBINATIONS.join(', ')}, got #{method}" unless RECOMBINATIONS.include?(method)
|
|
33
|
+
saved = Thread.current[:rcas_recombination]
|
|
34
|
+
Thread.current[:rcas_recombination] = method
|
|
35
|
+
yield
|
|
36
|
+
ensure
|
|
37
|
+
Thread.current[:rcas_recombination] = saved
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The number of subsets the search may try at one size before :auto
|
|
41
|
+
# hands the rest over to the lattice; nil for no limit.
|
|
42
|
+
def self.subset_budget = recombination == :auto ? SUBSET_BUDGET : nil
|
|
43
|
+
|
|
44
|
+
# Van Hoeij's recombination of the p-adic factors by lattice reduction.
|
|
45
|
+
#
|
|
46
|
+
# Zassenhaus tries subsets of the r local factors, and an irreducible
|
|
47
|
+
# polynomial with many of them - a Swinnerton-Dyer polynomial splits into
|
|
48
|
+
# quadratics modulo every prime - makes that 2**(r - 1) trials. Van Hoeij
|
|
49
|
+
# asks instead which 0/1 vectors v (v_i = 1 when f_i is in the factor)
|
|
50
|
+
# belong to a true factor, and finds them all at once as short vectors
|
|
51
|
+
# of a lattice: a knapsack problem LLL can solve [vHo02].
|
|
52
|
+
#
|
|
53
|
+
# What makes the lattice is a quantity that is additive over the local
|
|
54
|
+
# factors and small for a true one. The coefficients of the logarithmic
|
|
55
|
+
# derivative times f serve [HvHN11]: for a factor g of f in ZZ[x],
|
|
56
|
+
# f*g'/g = (f/g)*g' has integer coefficients, bounded by B_j (the sum
|
|
57
|
+
# over the roots of g of f/(x - root), each root below the Fujiwara
|
|
58
|
+
# bound); and f*(g*h)'/(g*h) = f*g'/g + f*h'/h, so modulo p**k the
|
|
59
|
+
# coefficient of a product of local factors is the sum of theirs. The
|
|
60
|
+
# lattice spanned by
|
|
61
|
+
#
|
|
62
|
+
# [ e_i | c_i,j / B_j ] for each local factor f_i
|
|
63
|
+
# [ 0 | p**k / B_j ] for each coefficient j used
|
|
64
|
+
#
|
|
65
|
+
# contains (v, small) for every true factor, with squared length at most
|
|
66
|
+
# M = r + (number of columns). After LLL, a lattice vector whose top
|
|
67
|
+
# Gram-Schmidt length exceeds sqrt(M) cannot be one of those, so the
|
|
68
|
+
# true vectors lie in the span of the first s reduced vectors, where s is
|
|
69
|
+
# the last index with |b*_s|**2 <= M. Their first r coordinates are the
|
|
70
|
+
# basis the next round starts from, with more coefficients. When the
|
|
71
|
+
# reduced echelon form of that basis is a 0/1 matrix with one 1 in every
|
|
72
|
+
# column, its rows are the candidate factors; each is checked by
|
|
73
|
+
# division, so a wrong guess costs a round, never a wrong answer. When
|
|
74
|
+
# the coefficients run out undecided, the precision is raised, and after
|
|
75
|
+
# that the subsets take over.
|
|
76
|
+
#
|
|
77
|
+
# Sources (keys: MANUAL.md, Sources): the algorithm [vHo02]; the
|
|
78
|
+
# coefficients of the logarithmic derivative and their bounds [HvHN11];
|
|
79
|
+
# LLL [LLL82], as in lattice.rb; the root bound [Fuj16].
|
|
80
|
+
module VanHoeij
|
|
81
|
+
MAX_RAISES = 3 # precision doublings before giving up to the subsets
|
|
82
|
+
COLUMNS_PER_ROUND = 2
|
|
83
|
+
SAFETY_BITS = 10 # how far p**k has to stand above B_j * 2**(r/2) for column j
|
|
84
|
+
|
|
85
|
+
module_function
|
|
86
|
+
|
|
87
|
+
# The irreducible factors of f (primitive, squarefree, positive leading
|
|
88
|
+
# coefficient), or nil when the lattice did not decide - the caller
|
|
89
|
+
# then recombines by subsets. `lifted` are the local factors already
|
|
90
|
+
# lifted to p**k, when the subsets handed over: lifting again from p
|
|
91
|
+
# cost 0.8 of 3.7 s on SD(6), and is done only when the lattice needs
|
|
92
|
+
# more precision than the subsets had.
|
|
93
|
+
def recombine(f, modular, p, k, lifted: nil)
|
|
94
|
+
r = modular.size
|
|
95
|
+
bounds = cld_bounds(f)
|
|
96
|
+
# enough precision for the first few columns, which have the
|
|
97
|
+
# smallest bounds: the top coefficients
|
|
98
|
+
wanted = bounds.last(3).max * 2**(r / 2 + SAFETY_BITS)
|
|
99
|
+
if p**k <= wanted
|
|
100
|
+
k += 1 while p**k <= wanted
|
|
101
|
+
lifted = nil
|
|
102
|
+
end
|
|
103
|
+
MAX_RAISES.times do
|
|
104
|
+
lifted ||= Zassenhaus.hensel_lift_leading(f, modular, p, k)
|
|
105
|
+
found = attempt(f, lifted, p**k, bounds)
|
|
106
|
+
return found if found
|
|
107
|
+
k *= 2
|
|
108
|
+
lifted = nil
|
|
109
|
+
end
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def attempt(f, lifted, m, bounds)
|
|
114
|
+
r = lifted.size
|
|
115
|
+
n = Dense.deg(f)
|
|
116
|
+
cld = lifted.map { |fi| coefficients(f, fi, m, n) }
|
|
117
|
+
# the columns whose bound stands well below p**k, smallest bound first
|
|
118
|
+
usable = (0...n).select { |j| m > bounds[j] * 2**(r / 2 + SAFETY_BITS) }.sort_by { |j| [bounds[j], -j] }
|
|
119
|
+
basis = Array.new(r) { |i| Array.new(r) { |l| i == l ? 1 : 0 } }
|
|
120
|
+
until usable.empty?
|
|
121
|
+
columns = usable.shift(COLUMNS_PER_ROUND)
|
|
122
|
+
basis = cut(basis, cld, columns, m, bounds)
|
|
123
|
+
return nil if basis.empty? # nothing short survived: the bounds were wrong
|
|
124
|
+
subsets = partition(basis)
|
|
125
|
+
next unless subsets
|
|
126
|
+
factors = candidates(f, lifted, subsets, m)
|
|
127
|
+
return factors if factors
|
|
128
|
+
end
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# f*fi'/fi modulo m, symmetric, as n coefficients.
|
|
133
|
+
def coefficients(f, fi, m, n)
|
|
134
|
+
q = Dense.divmod_mod(Dense.mod(f, m), fi, m).first
|
|
135
|
+
c = Dense.sym_mod(Dense.mul_mod(q, Dense.derivative(fi), m), m)
|
|
136
|
+
c + Array.new(n - c.size, 0)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# One round: the lattice of the current basis with the new columns,
|
|
140
|
+
# reduced, and cut to the vectors a true factor can lie among.
|
|
141
|
+
def cut(basis, cld, columns, m, bounds)
|
|
142
|
+
r = cld.size
|
|
143
|
+
rows = basis.map do |w|
|
|
144
|
+
traces = columns.map do |j|
|
|
145
|
+
t = w.each_index.sum { |i| w[i] * cld[i][j] } % m
|
|
146
|
+
t -= m if t > m / 2
|
|
147
|
+
Rational(t, bounds[j])
|
|
148
|
+
end
|
|
149
|
+
w.map(&:to_r) + traces
|
|
150
|
+
end
|
|
151
|
+
columns.each_with_index do |j, c|
|
|
152
|
+
rows << Array.new(r, 0r) + Array.new(columns.size) { |d| d == c ? Rational(m, bounds[j]) : 0r }
|
|
153
|
+
end
|
|
154
|
+
reduced, = Lattice.reduce(rows, Lattice::DELTA)
|
|
155
|
+
_, lengths = Lattice.gram_schmidt(reduced)
|
|
156
|
+
bound = r + columns.size
|
|
157
|
+
s = lengths.rindex { |b| b <= bound }
|
|
158
|
+
return [] if s.nil?
|
|
159
|
+
reduced.first(s + 1).map { |row| row.first(r).map { |e| e.to_i } }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The rows of the reduced echelon form when it is a 0/1 matrix with
|
|
163
|
+
# exactly one 1 in every column - a partition of the local factors -
|
|
164
|
+
# as lists of indices; nil otherwise.
|
|
165
|
+
def partition(basis)
|
|
166
|
+
echelon = rref(basis.map { |row| row.map(&:to_r) }).reject { |row| row.all?(&:zero?) }
|
|
167
|
+
return nil unless echelon.flatten.all? { |e| e.zero? || e == 1 }
|
|
168
|
+
return nil unless echelon.transpose.all? { |column| column.count(1) == 1 }
|
|
169
|
+
echelon.map { |row| row.each_index.select { |i| row[i] == 1 } }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def rref(rows)
|
|
173
|
+
rows = rows.map(&:dup)
|
|
174
|
+
lead = 0
|
|
175
|
+
cols = rows.first&.size || 0
|
|
176
|
+
rows.each_index do |i|
|
|
177
|
+
lead += 1 while lead < cols && rows[i..].all? { |row| row[lead].zero? }
|
|
178
|
+
break if lead >= cols
|
|
179
|
+
j = (i...rows.size).find { |l| !rows[l][lead].zero? }
|
|
180
|
+
rows[i], rows[j] = rows[j], rows[i]
|
|
181
|
+
pivot = rows[i][lead]
|
|
182
|
+
rows[i] = rows[i].map { |e| e / pivot }
|
|
183
|
+
rows.each_index do |l|
|
|
184
|
+
next if l == i || rows[l][lead].zero?
|
|
185
|
+
factor = rows[l][lead]
|
|
186
|
+
rows[l] = rows[l].zip(rows[i]).map { |a, b| a - factor * b }
|
|
187
|
+
end
|
|
188
|
+
lead += 1
|
|
189
|
+
end
|
|
190
|
+
rows
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# lc(f) times the product of each subset of local factors, made
|
|
194
|
+
# primitive; the list when every one divides f and they multiply to
|
|
195
|
+
# its degree, nil otherwise.
|
|
196
|
+
def candidates(f, lifted, subsets, m)
|
|
197
|
+
lc = f.last
|
|
198
|
+
rest = f
|
|
199
|
+
factors = subsets.map do |indices|
|
|
200
|
+
product = indices.reduce([lc]) { |acc, i| Dense.mul_mod(acc, lifted[i], m) }
|
|
201
|
+
g = Dense.primitive(Dense.sym_mod(product, m))
|
|
202
|
+
g = Dense.scale(g, -1) if g.last.negative?
|
|
203
|
+
quotient = Dense.div_exact(rest, g)
|
|
204
|
+
return nil if quotient.nil?
|
|
205
|
+
rest = quotient
|
|
206
|
+
g
|
|
207
|
+
end
|
|
208
|
+
return nil unless Dense.deg(rest).zero?
|
|
209
|
+
factors.sort_by { |g| [Dense.deg(g), g] }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# B_j: the coefficient of x**j in f*g'/g for any factor g of f is at
|
|
213
|
+
# most n * sum_{i > j} |f_i| * R**(i - j - 1), with R a bound on the
|
|
214
|
+
# absolute values of the roots of f.
|
|
215
|
+
def cld_bounds(f)
|
|
216
|
+
n = Dense.deg(f)
|
|
217
|
+
radius = root_bound(f)
|
|
218
|
+
(0...n).map do |j|
|
|
219
|
+
n * (j + 1..n).sum { |i| f[i].abs * radius**(i - j - 1) } + 1
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Fujiwara's bound: every root lies within 2*max |a_(n-i)/a_n|**(1/i)
|
|
224
|
+
# [Fuj16], rounded up to an integer.
|
|
225
|
+
def root_bound(f)
|
|
226
|
+
n = Dense.deg(f)
|
|
227
|
+
lead = f.last.abs
|
|
228
|
+
terms = (1..n).map do |i|
|
|
229
|
+
a = f[n - i].abs
|
|
230
|
+
a = Rational(a, 2) if i == n # the constant term enters halved
|
|
231
|
+
next 0 if a.zero?
|
|
232
|
+
ratio = Rational(a, lead)
|
|
233
|
+
root = Simplify.integer_root(ratio.ceil, i)
|
|
234
|
+
root += 1 while root**i < ratio
|
|
235
|
+
root
|
|
236
|
+
end
|
|
237
|
+
2 * terms.max
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
data/lib/rcas/vector.rb
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# QQ**3: column vectors of fixed dimension over a domain, and a domain
|
|
5
|
+
# itself: a vector answers `domain` with the space it lives in, and the
|
|
6
|
+
# space answers `base` with the domain its entries come from. Over a ring
|
|
7
|
+
# such as ZZ this is a free module rather than a vector space, but the API
|
|
8
|
+
# is the same.
|
|
9
|
+
class VectorSpace < Domain
|
|
10
|
+
attr_reader :base, :dim
|
|
11
|
+
|
|
12
|
+
def initialize(base, dim)
|
|
13
|
+
raise ArgumentError, "dimension must be a non-negative integer" unless dim.is_a?(Integer) && dim >= 0
|
|
14
|
+
@base = base
|
|
15
|
+
@dim = dim
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# QQ**3 [1, 2, 3] or (QQ**3)[1, 2, 3]
|
|
20
|
+
def [](*entries)
|
|
21
|
+
entries = entries.flatten(1) if entries.size == 1 && entries.first.is_a?(Array)
|
|
22
|
+
raise DomainError, "#{self} needs #{dim} entries, got #{entries.size}" unless entries.size == dim
|
|
23
|
+
entries = entries.map { |e| Scalar.lift(e) }
|
|
24
|
+
entries = entries.map { |e| base.normalize_coefficient(e) } if base.respond_to?(:normalize_coefficient)
|
|
25
|
+
entries.each do |e|
|
|
26
|
+
next if Infer.where_defined { base.include?(e) }
|
|
27
|
+
hint = e.variables.reject { |v| RCAS.assumption(v) }
|
|
28
|
+
hint = hint.empty? ? "" : " (declare #{hint.join(', ')} with assume(#{hint.first}: #{base}))"
|
|
29
|
+
raise DomainError, "#{e} is not in #{base}#{hint}"
|
|
30
|
+
end
|
|
31
|
+
Vector.new(self, entries)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def unchecked(entries) = Vector.new(self, entries.map { |e| Scalar.lift(e) })
|
|
35
|
+
|
|
36
|
+
def include?(obj)
|
|
37
|
+
obj = self[*obj] if obj.is_a?(Array) && obj.size == dim
|
|
38
|
+
obj.is_a?(Vector) && obj.dim == dim && obj.entries.all? { |e| base.include?(e) }
|
|
39
|
+
rescue DomainError
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
alias member? include?
|
|
43
|
+
def ===(obj) = include?(obj)
|
|
44
|
+
|
|
45
|
+
def zero = unchecked(Array.new(dim, 0))
|
|
46
|
+
def basis = (0...dim).map { |i| unchecked(Array.new(dim) { |j| i == j ? 1 : 0 }) }
|
|
47
|
+
|
|
48
|
+
# A module, not a ring: vectors have no multiplication between them.
|
|
49
|
+
def ring? = false
|
|
50
|
+
def field? = false
|
|
51
|
+
|
|
52
|
+
# Vectors are not numbers: no polynomial has them as coefficients.
|
|
53
|
+
def scalar? = false
|
|
54
|
+
|
|
55
|
+
def over(other_base) = VectorSpace.new(other_base, dim)
|
|
56
|
+
|
|
57
|
+
# The same dimension over a larger base: (ZZ**3) < (QQ**3).
|
|
58
|
+
def subset?(other) = other.is_a?(VectorSpace) && other.dim == dim && base.subset?(other.base)
|
|
59
|
+
|
|
60
|
+
def join(other)
|
|
61
|
+
return VectorSpace.new(base.join(other.base), dim) if other.is_a?(VectorSpace) && other.dim == dim
|
|
62
|
+
raise DomainError, "no common domain for #{self} and #{other}" if other.is_a?(VectorSpace) || other.is_a?(MatrixSpace)
|
|
63
|
+
VectorSpace.new(base.join(other), dim) # a domain of scalars: what scaling gives
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ==(other) = other.is_a?(VectorSpace) && other.base == base && other.dim == dim
|
|
67
|
+
alias eql? ==
|
|
68
|
+
def hash = [VectorSpace, base, dim].hash
|
|
69
|
+
|
|
70
|
+
def name = "#{base}**#{dim}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class Vector
|
|
74
|
+
include Algebraic
|
|
75
|
+
include Enumerable
|
|
76
|
+
|
|
77
|
+
attr_reader :space, :entries
|
|
78
|
+
|
|
79
|
+
def initialize(space, entries)
|
|
80
|
+
@space = space
|
|
81
|
+
@entries = entries.freeze
|
|
82
|
+
freeze
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def dim = entries.size
|
|
86
|
+
alias size dim
|
|
87
|
+
# The space is where the vector lives, base where its entries come from.
|
|
88
|
+
def domain = space
|
|
89
|
+
def base = space.base
|
|
90
|
+
def [](i) = entries.fetch(i)
|
|
91
|
+
def each(&block) = entries.each(&block)
|
|
92
|
+
def to_a = entries.dup
|
|
93
|
+
|
|
94
|
+
# ---- arithmetic -------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def +(other) = zip_with(other, :+) { |a, b| Scalar.add(a, b) }
|
|
97
|
+
def -(other) = zip_with(other, :-) { |a, b| Scalar.sub(a, b) }
|
|
98
|
+
def -@ = space.unchecked(entries.map { |e| Scalar.neg(e) })
|
|
99
|
+
|
|
100
|
+
# Vector * Vector is the dot product, Vector * Matrix a row-vector product,
|
|
101
|
+
# anything else scales.
|
|
102
|
+
def *(other)
|
|
103
|
+
case other
|
|
104
|
+
when Vector then dot(other)
|
|
105
|
+
when Matrix then Matrix.row_times(self, other)
|
|
106
|
+
else scale(other)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def /(scalar)
|
|
111
|
+
s = Scalar.lift(scalar)
|
|
112
|
+
space.over(result_domain(s).fraction_field).unchecked(entries.map { |e| Scalar.div(e, s) })
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def scale(scalar)
|
|
116
|
+
s = Scalar.lift(scalar)
|
|
117
|
+
space.over(result_domain(s)).unchecked(entries.map { |e| Scalar.mul(e, s) })
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def coerce(other) = [ScalarProxy.new(other), self]
|
|
121
|
+
def rop(op, left) = op == :* ? scale(left) : super
|
|
122
|
+
|
|
123
|
+
def dot(other)
|
|
124
|
+
check_dim!(other)
|
|
125
|
+
entries.zip(other.entries).map { |a, b| Scalar.mul(a, b) }.reduce { |s, x| Scalar.add(s, x) } || Num.new(0)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def cross(other)
|
|
129
|
+
check_dim!(other)
|
|
130
|
+
raise ArgumentError, "cross product needs dimension 3" unless dim == 3
|
|
131
|
+
a, b = entries, other.entries
|
|
132
|
+
VectorSpace.new(base.join(other.base), 3).unchecked([
|
|
133
|
+
Scalar.sub(Scalar.mul(a[1], b[2]), Scalar.mul(a[2], b[1])),
|
|
134
|
+
Scalar.sub(Scalar.mul(a[2], b[0]), Scalar.mul(a[0], b[2])),
|
|
135
|
+
Scalar.sub(Scalar.mul(a[0], b[1]), Scalar.mul(a[1], b[0]))
|
|
136
|
+
])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def norm = LinearAlgebra.norm(self) # Hermitian for complex entries
|
|
140
|
+
def zero? = entries.all? { |e| Scalar.zero?(e) }
|
|
141
|
+
|
|
142
|
+
def ==(other)
|
|
143
|
+
other = space.unchecked(other) if other.is_a?(Array) && other.size == dim
|
|
144
|
+
other.is_a?(Vector) && other.dim == dim &&
|
|
145
|
+
entries.zip(other.entries).all? { |a, b| Scalar.zero?(Scalar.sub(a, b)) }
|
|
146
|
+
end
|
|
147
|
+
alias eql? ==
|
|
148
|
+
def hash = [Vector, entries.map(&:simplify)].hash
|
|
149
|
+
|
|
150
|
+
def simplify = space.unchecked(entries.map(&:simplify))
|
|
151
|
+
def subs(*args) = space.unchecked(entries.map { |e| e.subs(*args) })
|
|
152
|
+
def call(**bindings) = space.unchecked(entries.map { |e| Scalar.lift(e.call(**bindings)) })
|
|
153
|
+
|
|
154
|
+
def to_s = "(#{entries.join(', ')})"
|
|
155
|
+
alias inspect to_s
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def check_dim!(other)
|
|
160
|
+
raise TypeError, "expected a Vector, got #{other.class}" unless other.is_a?(Vector)
|
|
161
|
+
raise ArgumentError, "dimension mismatch: #{dim} vs #{other.dim}" unless dim == other.dim
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def zip_with(other, op)
|
|
165
|
+
raise TypeError, "can't apply #{op} to Vector and #{other.class}" unless other.is_a?(Vector)
|
|
166
|
+
check_dim!(other)
|
|
167
|
+
VectorSpace.new(base.join(other.base), dim).unchecked(entries.zip(other.entries).map { |a, b| yield a, b })
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def result_domain(scalar)
|
|
171
|
+
d = Scalar.domain(scalar)
|
|
172
|
+
d ? base.join(d) : base
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|