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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module RCAS
|
|
6
|
+
module Chat
|
|
7
|
+
# The object and binding that user code and tool code run in. Bare
|
|
8
|
+
# identifiers that are not yet defined become symbols (and locals), as
|
|
9
|
+
# in bin/rcas; Functions (sin, sqrt, assume, matrix, show, ...) and the
|
|
10
|
+
# number sets are in scope.
|
|
11
|
+
class Workspace
|
|
12
|
+
IDENTIFIER = RCAS::IDENTIFIER # Unicode names (α, β₁, ∞) included
|
|
13
|
+
|
|
14
|
+
module AutoSymbol
|
|
15
|
+
def method_missing(name, *args, &block)
|
|
16
|
+
return super unless block.nil? && name.match?(IDENTIFIER)
|
|
17
|
+
return RCAS.unknown_function(name, args) || super unless args.empty? # u(n + 1): an unknown function
|
|
18
|
+
__rcas_workspace__.binding.local_variable_set(name, name)
|
|
19
|
+
name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Deliberately false so Ruby's implicit conversions are unaffected.
|
|
23
|
+
def respond_to_missing?(_name, _include_private = false) = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :binding, :main
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
Object.include(Sets) unless Object.include?(Sets)
|
|
30
|
+
@main = Object.new
|
|
31
|
+
workspace = self
|
|
32
|
+
@main.singleton_class.include(Functions)
|
|
33
|
+
@main.singleton_class.prepend(AutoSymbol)
|
|
34
|
+
RCAS.undefine_kernel_printers(@main) # p, pp, j, jj are indeterminates here
|
|
35
|
+
@main.define_singleton_method(:__rcas_workspace__) { workspace }
|
|
36
|
+
@main.define_singleton_method(:to_s) { "rcas" }
|
|
37
|
+
@main.define_singleton_method(:inspect) { "rcas" }
|
|
38
|
+
# A method body has a fresh local scope, so the binding starts empty.
|
|
39
|
+
@main.singleton_class.class_eval("def __rcas_binding__ = binding")
|
|
40
|
+
@binding = @main.__rcas_binding__
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Evaluate +code+; returns [value, captured_stdout]. With capture:
|
|
44
|
+
# false output goes straight to $stdout and the second value is "".
|
|
45
|
+
def eval(code, capture: false)
|
|
46
|
+
output = +""
|
|
47
|
+
value =
|
|
48
|
+
if capture
|
|
49
|
+
previous = $stdout
|
|
50
|
+
$stdout = StringIO.new(output)
|
|
51
|
+
begin
|
|
52
|
+
run(code)
|
|
53
|
+
ensure
|
|
54
|
+
$stdout = previous
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
run(code)
|
|
58
|
+
end
|
|
59
|
+
@binding.local_variable_set(:_, value)
|
|
60
|
+
[value, output]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A line that does not parse raises its SyntaxError, which the REPL
|
|
64
|
+
# reports; Ruby also printed a warning of its own for some of them,
|
|
65
|
+
# past the UI and onto stderr (`? hello` without Claude gave "invalid
|
|
66
|
+
# character syntax; use ?\s" and then the error). Such a line is
|
|
67
|
+
# evaluated quietly, so the error is still the same one. A line that
|
|
68
|
+
# parses runs as it is, with whatever warnings its running gives.
|
|
69
|
+
def run(code)
|
|
70
|
+
parses = begin
|
|
71
|
+
Hold.parse(code)
|
|
72
|
+
true
|
|
73
|
+
rescue SyntaxError
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
parses ? @binding.eval(code, "(rcas)", 1) : Hold.quietly { @binding.eval(code, "(rcas)", 1) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Run code for its side effects only (used when restoring a session).
|
|
80
|
+
def replay(code)
|
|
81
|
+
eval(code, capture: true)
|
|
82
|
+
rescue StandardError, ScriptError, SystemStackError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def locals
|
|
87
|
+
@binding.local_variables.reject { |v| v == :_ }.to_h { |v| [v, @binding.local_variable_get(v)] }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def [](name) = @binding.local_variable_get(name)
|
|
91
|
+
|
|
92
|
+
# Does +code+ parse as Ruby?
|
|
93
|
+
def ruby?(code)
|
|
94
|
+
Hold.parse(code)
|
|
95
|
+
true
|
|
96
|
+
rescue SyntaxError
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Does +code+ call, with arguments, a bare name the workspace does not
|
|
101
|
+
# define (foo(1), x(squared))? Such calls would become unknown functions.
|
|
102
|
+
def undefined_calls?(code)
|
|
103
|
+
receiver = @binding.receiver
|
|
104
|
+
stack = [Hold.parse(code)]
|
|
105
|
+
until stack.empty?
|
|
106
|
+
node = stack.pop
|
|
107
|
+
next unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
|
108
|
+
return true if node.type == :FCALL && !receiver.respond_to?(node.children.first, true) && !@binding.local_variable_defined?(node.children.first)
|
|
109
|
+
stack.concat(node.children)
|
|
110
|
+
end
|
|
111
|
+
false
|
|
112
|
+
rescue SyntaxError
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Is +code+ Ruby that merely stops early (open block, string, paren)?
|
|
117
|
+
def incomplete?(code)
|
|
118
|
+
Hold.parse(code)
|
|
119
|
+
false
|
|
120
|
+
rescue SyntaxError => e
|
|
121
|
+
e.message.match?(/unexpected end-of-input|unexpected end of input|unexpected end-of-file|unterminated .* meets end of file|embedded document meets end of file/i)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Tab-completion candidates.
|
|
125
|
+
def complete(prefix)
|
|
126
|
+
names = locals.keys.map(&:to_s) + Functions.instance_methods(false).map(&:to_s) + Sets::ALL.map(&:to_s)
|
|
127
|
+
names += %w[simplify expand factor diff integrate subs call evalf to_latex show to_poly variables domain]
|
|
128
|
+
names.uniq.select { |n| n.start_with?(prefix) }.sort
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
data/lib/rcas/chat.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rcas"
|
|
4
|
+
|
|
5
|
+
module RCAS
|
|
6
|
+
# A Claude-Code-style terminal front end for rcas (bin/rcas-chat).
|
|
7
|
+
#
|
|
8
|
+
# * Ruby is evaluated in a session where bare identifiers are variables,
|
|
9
|
+
# like bin/rcas; results are shown as text and, in iTerm2, typeset.
|
|
10
|
+
# * Anything that is not Ruby is a question for Claude, which answers by
|
|
11
|
+
# running rcas code in the same session through the rcas_eval tool.
|
|
12
|
+
# * /commands configure the session, !cmd runs a shell command, and
|
|
13
|
+
# sessions are saved so `rcas-chat --continue` picks up where you left.
|
|
14
|
+
module Chat
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
|
|
17
|
+
# The anthropic gem, loaded on first need: when credentials are present,
|
|
18
|
+
# or a test hands the assistant a scripted runner. A session nobody asks
|
|
19
|
+
# Claude in never loads it - it was a thousand files and a fifth of a
|
|
20
|
+
# second on every start of rcas-chat, and one of its dependencies
|
|
21
|
+
# (standardwebhooks) warns under -w. true when the gem is there.
|
|
22
|
+
def self.load_anthropic
|
|
23
|
+
return true if defined?(EvalTool)
|
|
24
|
+
return false if @anthropic_missing
|
|
25
|
+
quietly do
|
|
26
|
+
require "anthropic"
|
|
27
|
+
require_relative "chat/tool"
|
|
28
|
+
end
|
|
29
|
+
true
|
|
30
|
+
rescue LoadError
|
|
31
|
+
@anthropic_missing = true
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# $VERBOSE off for the block and back to what it was on every way out.
|
|
36
|
+
def self.quietly
|
|
37
|
+
verbose = $VERBOSE
|
|
38
|
+
$VERBOSE = nil
|
|
39
|
+
yield
|
|
40
|
+
ensure
|
|
41
|
+
$VERBOSE = verbose
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
require_relative "chat/settings"
|
|
47
|
+
require_relative "chat/style"
|
|
48
|
+
require_relative "chat/workspace"
|
|
49
|
+
require_relative "chat/ui"
|
|
50
|
+
require_relative "chat/usage"
|
|
51
|
+
require_relative "chat/assistant"
|
|
52
|
+
require_relative "chat/session"
|
|
53
|
+
require_relative "chat/picker"
|
|
54
|
+
require_relative "chat/repl"
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Polynomial structure of expressions: degree, leading and trailing
|
|
5
|
+
# coefficient, single coefficients, the coefficient list, and collecting
|
|
6
|
+
# in one indeterminate.
|
|
7
|
+
#
|
|
8
|
+
# degree((x + 1)**3, x) # => 3
|
|
9
|
+
# coeff(a*x**2 + b*x + c, x, 2) # => a
|
|
10
|
+
# collect((x + y)**2 + a*x, x) # => y**2 + (a + 2*y)*x + x**2
|
|
11
|
+
#
|
|
12
|
+
# Everything works on Expand.table, so the input need not be expanded and
|
|
13
|
+
# the coefficients come back in canonical form. Without an indeterminate
|
|
14
|
+
# the functions look at all indeterminates of the expression: `degree` is
|
|
15
|
+
# the total degree, `lcoeff`/`tcoeff` belong to the last/first term of the
|
|
16
|
+
# canonical sum, `coeffs` lists the coefficients in that order.
|
|
17
|
+
#
|
|
18
|
+
# An expression that is not a polynomial in the indeterminate (x inside a
|
|
19
|
+
# function, in a denominator, with a fractional or symbolic exponent)
|
|
20
|
+
# raises DomainError rather than guessing.
|
|
21
|
+
module Coefficients
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# Highest exponent; -1 for the zero polynomial (as Polynomial#degree).
|
|
25
|
+
def degree(expr, var = nil)
|
|
26
|
+
terms = terms_in(expr, var)
|
|
27
|
+
terms.empty? ? -1 : terms.map(&:first).max
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Lowest exponent; -1 for the zero polynomial.
|
|
31
|
+
def ldegree(expr, var = nil)
|
|
32
|
+
terms = terms_in(expr, var)
|
|
33
|
+
terms.empty? ? -1 : terms.map(&:first).min
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Coefficient of the highest power of var (last term of the canonical sum
|
|
37
|
+
# when var is nil).
|
|
38
|
+
def lcoeff(expr, var = nil)
|
|
39
|
+
terms = terms_in(expr, var)
|
|
40
|
+
return Num.new(0) if terms.empty?
|
|
41
|
+
var ? coefficient(terms, terms.map(&:first).max) : terms.last[1]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Coefficient of the lowest power of var (first term of the canonical sum
|
|
45
|
+
# when var is nil).
|
|
46
|
+
def tcoeff(expr, var = nil)
|
|
47
|
+
terms = terms_in(expr, var)
|
|
48
|
+
return Num.new(0) if terms.empty?
|
|
49
|
+
var ? coefficient(terms, terms.map(&:first).min) : terms.first[1]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# coeff(f, x, k) or coeff(f, x**k): the coefficient of x**k.
|
|
53
|
+
def coeff(expr, var, k = 1)
|
|
54
|
+
var, k = split_power(var, k)
|
|
55
|
+
coefficient(terms_in(expr, var), k)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Coefficients of x**0, x**1, ..., x**degree (zeros included), or the
|
|
59
|
+
# coefficients of the canonical sum's terms when var is nil.
|
|
60
|
+
def coeffs(expr, var = nil)
|
|
61
|
+
terms = terms_in(expr, var)
|
|
62
|
+
return terms.map(&:last) unless var
|
|
63
|
+
return [] if terms.empty?
|
|
64
|
+
(0..terms.map(&:first).max).map { |k| coefficient(terms, k) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Sum of coeff(f, x, k) * x**k over the exponents that occur, ascending.
|
|
68
|
+
def collect(expr, var)
|
|
69
|
+
var = variable(var)
|
|
70
|
+
terms = terms_in(expr, var)
|
|
71
|
+
return Num.new(0) if terms.empty?
|
|
72
|
+
parts = terms.map(&:first).uniq.sort.map { |k| collected_term(coefficient(terms, k), var, k) }
|
|
73
|
+
Simplify.sum_tree(parts)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# ---- internals --------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
# With var: [[exponent of var, coefficient expression], ...] sorted by
|
|
79
|
+
# exponent, one entry per exponent. Without var: one entry per term of
|
|
80
|
+
# the canonical sum, [total degree, coefficient], in canonical order.
|
|
81
|
+
def terms_in(expr, var)
|
|
82
|
+
expr = expr.to_expr if expr.respond_to?(:to_expr)
|
|
83
|
+
expr = Expression.lift(expr)
|
|
84
|
+
var = variable(var) if var
|
|
85
|
+
names = var ? [var.name] : expr.variables
|
|
86
|
+
constant, table = Expand.table(expr)
|
|
87
|
+
grouped = Hash.new { |h, k| h[k] = [0, {}] }
|
|
88
|
+
grouped[var ? 0 : {}] = [constant, {}] unless constant.zero?
|
|
89
|
+
table.each do |factors, c|
|
|
90
|
+
exponents = {}
|
|
91
|
+
rest = {}
|
|
92
|
+
factors.each do |base, exp|
|
|
93
|
+
if base.is_a?(Var) && names.include?(base.name)
|
|
94
|
+
polynomial_exponent!(expr, base, exp)
|
|
95
|
+
exponents[base] = exp
|
|
96
|
+
else
|
|
97
|
+
# exp(x) is e**x: the exponent counts, or exp(x) was the constant
|
|
98
|
+
# coefficient of itself (third review, A3)
|
|
99
|
+
offending = (base.variables | (exp.is_a?(Expression) ? exp.variables : [])) & names
|
|
100
|
+
raise DomainError, "#{expr} is not a polynomial in #{offending.first}" unless offending.empty?
|
|
101
|
+
rest[base] = exp
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
key = var ? exponents.fetch(var, 0) : exponents
|
|
105
|
+
entry = grouped[key]
|
|
106
|
+
if rest.empty?
|
|
107
|
+
entry[0] += c
|
|
108
|
+
else
|
|
109
|
+
Expand.add_term(entry[1], rest, c)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
if var
|
|
113
|
+
grouped.sort.map { |k, (c, t)| [k, Simplify.rebuild_sum(c, t)] }
|
|
114
|
+
else
|
|
115
|
+
grouped.sort_by { |exponents, _| Simplify.sort_key(Simplify.rebuild_product(1, exponents)) }
|
|
116
|
+
.map { |exponents, (c, t)| [exponents.values.sum, Simplify.rebuild_sum(c, t)] }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def polynomial_exponent!(expr, base, exp)
|
|
121
|
+
return if exp.is_a?(Integer) && exp >= 0
|
|
122
|
+
raise DomainError, "#{expr} is not a polynomial in #{base}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def coefficient(terms, k)
|
|
126
|
+
entry = terms.assoc(k)
|
|
127
|
+
entry ? entry[1] : Num.new(0)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def variable(var)
|
|
131
|
+
v = Expression.lift(var)
|
|
132
|
+
raise ArgumentError, "#{var} is not an indeterminate" unless v.is_a?(Var)
|
|
133
|
+
v
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def split_power(var, k)
|
|
137
|
+
k = k.value if k.is_a?(Num) # literals arrive as Num inside hold { }
|
|
138
|
+
v = var.is_a?(Symbol) ? var : Expression.lift(var)
|
|
139
|
+
if v.is_a?(Pow) && v.exponent.is_a?(Num) && v.exponent.value.is_a?(Integer)
|
|
140
|
+
[variable(v.base), v.exponent.value]
|
|
141
|
+
else
|
|
142
|
+
raise ArgumentError, "coeff: the exponent must be an integer" unless k.is_a?(Integer)
|
|
143
|
+
[variable(v), k]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# [expression, negative?] for one term of a collected sum.
|
|
148
|
+
def collected_term(coefficient, var, k)
|
|
149
|
+
return [coefficient, false] if k.zero?
|
|
150
|
+
power = { var => k }
|
|
151
|
+
case coefficient
|
|
152
|
+
when Add, Sub then [Mul.new(coefficient, Simplify.rebuild_product(1, power)), false]
|
|
153
|
+
else
|
|
154
|
+
c, factors = Simplify.factorize(coefficient)
|
|
155
|
+
negative = Simplify.sign_negative?(c)
|
|
156
|
+
[Simplify.rebuild_product(negative ? -c : c, factors.merge(power)), negative]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class Expression
|
|
162
|
+
def degree(var = nil) = Coefficients.degree(self, var)
|
|
163
|
+
def ldegree(var = nil) = Coefficients.ldegree(self, var)
|
|
164
|
+
def lcoeff(var = nil) = Coefficients.lcoeff(self, var)
|
|
165
|
+
def tcoeff(var = nil) = Coefficients.tcoeff(self, var)
|
|
166
|
+
def coeff(var, k = 1) = Coefficients.coeff(self, var, k)
|
|
167
|
+
def coeffs(var = nil) = Coefficients.coeffs(self, var)
|
|
168
|
+
def collect(var) = Coefficients.collect(self, var)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# factorial, binomial and gamma: exact values, the (k+1)!/k! = k+1
|
|
5
|
+
# cancellation used by simplify and Gosper, and the classical power series
|
|
6
|
+
# (exp, sin, cos, sinh, cosh, log, atan, the binomial theorem) that turn
|
|
7
|
+
# infinite hypergeometric sums into closed forms.
|
|
8
|
+
module Combinatorics
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Fibonacci numbers by fast doubling; F(-n) = (-1)**(n + 1) F(n).
|
|
12
|
+
def fibonacci(n)
|
|
13
|
+
return (n.even? ? -1 : 1) * fibonacci(-n) if n.negative?
|
|
14
|
+
a, b = 0, 1 # F(k), F(k + 1)
|
|
15
|
+
n.bit_length.downto(1) do |i|
|
|
16
|
+
c = a * (2 * b - a) # F(2k)
|
|
17
|
+
d = a * a + b * b # F(2k + 1)
|
|
18
|
+
a, b = n[i - 1] == 1 ? [d, c + d] : [c, d]
|
|
19
|
+
end
|
|
20
|
+
a
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def factorial_value(n)
|
|
24
|
+
case n
|
|
25
|
+
when Integer then n >= 0 ? Num.new((1..n).reduce(1, :*)) : nil
|
|
26
|
+
when Rational then gamma_value(n + 1)
|
|
27
|
+
when Float then n < 0 && n == n.round ? UNDEFINED : Num.new(Math.gamma(n + 1))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# gamma(n) for integers and half-integers exactly, floats numerically.
|
|
32
|
+
def gamma_value(v)
|
|
33
|
+
case v
|
|
34
|
+
when Integer then v >= 1 ? Num.new((1..v - 1).reduce(1, :*)) : nil
|
|
35
|
+
when Rational
|
|
36
|
+
return nil unless v.denominator == 2
|
|
37
|
+
# gamma(n + 1/2) = (2n)! / (4**n n!) sqrt(pi), extended downwards by gamma(x) = gamma(x + 1) / x
|
|
38
|
+
n = (v - Rational(1, 2)).to_i
|
|
39
|
+
if n >= 0
|
|
40
|
+
c = Rational((1..2 * n).reduce(1, :*), 4**n * (1..n).reduce(1, :*))
|
|
41
|
+
else
|
|
42
|
+
c = Rational(1)
|
|
43
|
+
(n...0).each { |j| c /= (j + Rational(1, 2)) }
|
|
44
|
+
end
|
|
45
|
+
(Num.new(c) * RCAS.sqrt(PI)).simplify
|
|
46
|
+
# a pole (0, -1, -2, ... as a Float) has no value: undefined, not a
|
|
47
|
+
# Math::DomainError reaching the user (third review, C6)
|
|
48
|
+
when Float then v <= 0 && v == v.round ? UNDEFINED : Num.new(Math.gamma(v))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def binomial_value(n, k)
|
|
53
|
+
# a Float that is a whole number is one (evalf floats both arguments)
|
|
54
|
+
k = Num.new(k.value.to_i) if k.is_a?(Num) && k.value.is_a?(Float) && k.value.finite? && k.value == k.value.round
|
|
55
|
+
if k.is_a?(Num) && k.value.is_a?(Float) && n.is_a?(Num) && n.value.is_a?(Float) &&
|
|
56
|
+
[n.value + 1, k.value + 1, n.value - k.value + 1].all?(&:positive?)
|
|
57
|
+
return Num.new(Math.exp(Math.lgamma(n.value + 1).first - Math.lgamma(k.value + 1).first - Math.lgamma(n.value - k.value + 1).first))
|
|
58
|
+
end
|
|
59
|
+
return nil unless k.is_a?(Num) && k.value.is_a?(Integer)
|
|
60
|
+
kk = k.value
|
|
61
|
+
return Num.new(0) if kk.negative?
|
|
62
|
+
return Num.new(1) if kk.zero?
|
|
63
|
+
return n if kk == 1
|
|
64
|
+
if n.is_a?(Num) && n.value.is_a?(Integer)
|
|
65
|
+
nn = n.value
|
|
66
|
+
return Num.new(0) if nn >= 0 && kk > nn
|
|
67
|
+
return Num.new((0...kk).reduce(1) { |acc, i| acc * (nn - i) } / (1..kk).reduce(1, :*))
|
|
68
|
+
end
|
|
69
|
+
# binomial(1/2, 3) is a number too: the falling factorial over k!
|
|
70
|
+
if n.is_a?(Num) && (n.value.is_a?(Rational) || n.value.is_a?(Float))
|
|
71
|
+
falling = (0...kk).reduce(1) { |acc, i| acc * (n.value - i) }
|
|
72
|
+
return Num.new(Simplify.normalize_number(falling / (1..kk).reduce(1, :*)))
|
|
73
|
+
end
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# binomial(n, k) with a numeric k as the polynomial n(n-1)...(n-k+1)/k!
|
|
78
|
+
def expand_binomial(n, k)
|
|
79
|
+
return nil unless k.is_a?(Num) && k.value.is_a?(Integer) && k.value >= 0
|
|
80
|
+
kk = k.value
|
|
81
|
+
product = (0...kk).reduce(Num.new(1)) { |acc, i| acc * (n - i) }
|
|
82
|
+
(product / (1..kk).reduce(1, :*)).expand
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Replace binomials by factorials (for ratios of consecutive terms).
|
|
86
|
+
def to_factorials(expr)
|
|
87
|
+
expr = expr.map_children { |c| to_factorials(c) }
|
|
88
|
+
if expr.is_a?(Fn) && expr.name == :binomial
|
|
89
|
+
n, k = expr.args
|
|
90
|
+
return Fn.new(:factorial, [n]) / (Fn.new(:factorial, [k]) * Fn.new(:factorial, [(n - k).simplify]))
|
|
91
|
+
end
|
|
92
|
+
expr
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Merge factorials whose arguments differ by an integer:
|
|
96
|
+
# (k + 2)! / k! => (k + 1)(k + 2). Mutates +factors+ (base => exponent).
|
|
97
|
+
def merge_factorials(factors)
|
|
98
|
+
loop do
|
|
99
|
+
facts = factors.keys.select { |b| b.is_a?(Fn) && b.name == :factorial }
|
|
100
|
+
pair = nil
|
|
101
|
+
facts.combination(2).each do |a, b|
|
|
102
|
+
d = (a.args.first - b.args.first).simplify
|
|
103
|
+
next unless d.is_a?(Num) && d.value.is_a?(Integer) && !d.zero?
|
|
104
|
+
pair = d.value.positive? ? [a, b, d.value] : [b, a, -d.value]
|
|
105
|
+
break
|
|
106
|
+
end
|
|
107
|
+
return factors unless pair
|
|
108
|
+
big, small, m = pair
|
|
109
|
+
e = factors.delete(big)
|
|
110
|
+
Simplify.add_factor(factors, small, e)
|
|
111
|
+
(1..m).each { |i| Simplify.add_factor(factors, (small.args.first + i).simplify, e) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A closed form built out of gammas and factorials is often a binomial
|
|
116
|
+
# coefficient wearing a disguise: the sum of binomial(n, k)**2 comes
|
|
117
|
+
# back as 2**(2*n)*gamma(1/2 + n)/(pi**(1/2)*n!), which is
|
|
118
|
+
# binomial(2*n, n). The shape is guessed - one of a handful of linear
|
|
119
|
+
# arguments - and then checked at several integers, which is what makes
|
|
120
|
+
# guessing safe (fps offers its coefficient in three forms and lets the
|
|
121
|
+
# same kind of check decide). nil when nothing fits.
|
|
122
|
+
UPPER = [1, 2].freeze
|
|
123
|
+
LOWER = [0, 1, 2].freeze
|
|
124
|
+
SHIFTS = [0, 1, -1, 2, -2].freeze # nearest first, so binomial(2*n, n) wins over 2*binomial(2*n - 1, n - 1)
|
|
125
|
+
SAMPLES = [3, 4, 5, 6].freeze
|
|
126
|
+
|
|
127
|
+
def as_binomial(expr, var)
|
|
128
|
+
expr = Expression.lift(expr)
|
|
129
|
+
return nil unless expr.each_node.any? { |n| n.is_a?(Fn) && %i[gamma factorial].include?(n.name) }
|
|
130
|
+
values = SAMPLES.map { |m| numeric_at(expr, var, m) }
|
|
131
|
+
return nil if values.any? { |v| v.nil? || v.zero? }
|
|
132
|
+
|
|
133
|
+
UPPER.each do |a|
|
|
134
|
+
LOWER.each do |c|
|
|
135
|
+
next if c > a
|
|
136
|
+
SHIFTS.each do |b|
|
|
137
|
+
SHIFTS.each do |d|
|
|
138
|
+
ratio = constant_ratio(values, a, b, c, d)
|
|
139
|
+
next unless ratio
|
|
140
|
+
form = (Expression.lift(ratio) * Fn.new(:binomial, [(a * var + b).simplify, (c * var + d).simplify])).simplify
|
|
141
|
+
return form
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The same rational multiple of binomial(a*m + b, c*m + d) at every
|
|
150
|
+
# sample, or nil. A "nice" rational only: a ratio that needs sixty digits
|
|
151
|
+
# is the numerics talking, not an identity.
|
|
152
|
+
def constant_ratio(values, a, b, c, d)
|
|
153
|
+
first = nil
|
|
154
|
+
SAMPLES.each_with_index do |m, i|
|
|
155
|
+
top = a * m + b
|
|
156
|
+
bottom = c * m + d
|
|
157
|
+
return nil if bottom.negative? || top < bottom
|
|
158
|
+
binomial = (0...bottom).reduce(1) { |acc, j| acc * (top - j) } / (1..bottom).reduce(1, :*)
|
|
159
|
+
return nil if binomial.zero?
|
|
160
|
+
ratio = values[i] / binomial.to_f
|
|
161
|
+
rational = Rational(ratio).rationalize(Rational(1, 10**9))
|
|
162
|
+
return nil if rational.numerator.abs > 64 || rational.denominator > 64
|
|
163
|
+
first ||= rational
|
|
164
|
+
return nil unless (ratio - first).abs < 1e-9 * [1.0, ratio.abs].max
|
|
165
|
+
end
|
|
166
|
+
first
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def numeric_at(expr, var, m)
|
|
170
|
+
value = expr.evalf(var.name => m)
|
|
171
|
+
value = value.value if value.is_a?(Num)
|
|
172
|
+
value.is_a?(Numeric) && value.real? && value.finite? ? value.to_f : nil
|
|
173
|
+
rescue StandardError => rescued
|
|
174
|
+
RCAS.guard!(rescued)
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# sum_k binomial(n, k)*x**k is a polynomial when n is a non-negative
|
|
179
|
+
# integer and otherwise converges only for |x| < 1. Without the test
|
|
180
|
+
# sum((-1)**k, k, 0, oo) came back as 1/2 - the value the formula gives
|
|
181
|
+
# at x = 1, where the series does not converge at all.
|
|
182
|
+
def binomial_series_converges?(n, x)
|
|
183
|
+
return true unless n.is_a?(Num) # a symbolic upper index: the terms vanish past k = n
|
|
184
|
+
return true if n.value.is_a?(Integer) && n.value >= 0
|
|
185
|
+
value = x.is_a?(Num) ? x.value : (x.variables.empty? ? x.evalf : nil)
|
|
186
|
+
value = value.value if value.is_a?(Num)
|
|
187
|
+
return true unless value.is_a?(Numeric) # symbolic: convergence is assumed, as elsewhere here
|
|
188
|
+
value.abs < 1
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# |x| <= 1, decided; a symbolic x is assumed inside, as elsewhere here.
|
|
192
|
+
# A constant whose modulus cannot be compared with 1 is not.
|
|
193
|
+
def within_radius?(x)
|
|
194
|
+
return true unless x.variables.empty?
|
|
195
|
+
re, im = ComplexParts.parts(x)
|
|
196
|
+
sign = Decide.sign((re**2 + im**2 - 1).simplify)
|
|
197
|
+
sign == :negative || sign == :zero
|
|
198
|
+
rescue StandardError => rescued
|
|
199
|
+
RCAS.guard!(rescued)
|
|
200
|
+
false
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# log(0), oo, and atan(+-i): the arctangent series has radius 1 and its
|
|
204
|
+
# singularities at +-i, where sum 1/(2k + 1) diverges (fourth review).
|
|
205
|
+
def divergent?(value)
|
|
206
|
+
value.each_node.any? do |n|
|
|
207
|
+
(n.is_a?(Fn) && n.name == :log && n.args.first.is_a?(Num) && n.args.first.zero?) || n == OO ||
|
|
208
|
+
(n.is_a?(Fn) && n.name == :atan && plus_minus_i?(n.args.first.simplify))
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def plus_minus_i?(u) = u.is_a?(Num) && u.value.is_a?(Complex) && u.value.real.zero? && u.value.imaginary.abs == 1
|
|
213
|
+
|
|
214
|
+
# sqrt(e) when e is a perfect square (x**2, 4*y**2); any sqrt when not exact_only.
|
|
215
|
+
def root_of(e, exact_only)
|
|
216
|
+
coeff, factors = Simplify.factorize(e)
|
|
217
|
+
if factors.values.all? { |x| x.is_a?(Integer) && x.even? } && coeff.is_a?(Integer) && coeff.positive? &&
|
|
218
|
+
Integer.sqrt(coeff)**2 == coeff
|
|
219
|
+
return Simplify.rebuild_product(Integer.sqrt(coeff), factors.transform_values { |x| x / 2 })
|
|
220
|
+
end
|
|
221
|
+
exact_only ? nil : RCAS.sqrt(e)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# ---- classical series ---------------------------------------------------
|
|
225
|
+
|
|
226
|
+
# Infinite hypergeometric sums with a known closed form. r is the ratio
|
|
227
|
+
# f(k+1)/f(k) as a rational function of k; m0 is the natural first index.
|
|
228
|
+
# Returns [start_index, sum of the series whose first term is 1].
|
|
229
|
+
def known_series(r, k)
|
|
230
|
+
one = Num.new(1)
|
|
231
|
+
two = Num.new(2)
|
|
232
|
+
candidates = []
|
|
233
|
+
candidates << [0, ->(x) { Fn.new(:exp, [x]) }, (r * (k + 1)).cancel, :direct]
|
|
234
|
+
candidates << [0, ->(x) { Fn.new(:cos, [x]) }, (r * (2 * k + 1) * (2 * k + 2)).cancel, :neg_square]
|
|
235
|
+
candidates << [0, ->(x) { Fn.new(:sin, [x]) / x }, (r * (2 * k + 2) * (2 * k + 3)).cancel, :neg_square]
|
|
236
|
+
candidates << [0, ->(x) { Fn.new(:cosh, [x]) }, (r * (2 * k + 1) * (2 * k + 2)).cancel, :square]
|
|
237
|
+
candidates << [0, ->(x) { Fn.new(:sinh, [x]) / x }, (r * (2 * k + 2) * (2 * k + 3)).cancel, :square]
|
|
238
|
+
candidates << [1, ->(x) { Fn.new(:log, [one + x]) / x }, (r * (k + 1) / k).cancel, :negated, true]
|
|
239
|
+
candidates << [0, ->(x) { Fn.new(:atan, [x]) / x }, (r * (2 * k + 3) / (2 * k + 1)).cancel, :neg_square, true]
|
|
240
|
+
[true, false].each do |exact_roots_only|
|
|
241
|
+
candidates.each do |m0, template, p, kind, radius_one|
|
|
242
|
+
next if p.variables.include?(k.name)
|
|
243
|
+
x =
|
|
244
|
+
case kind
|
|
245
|
+
when :direct then p
|
|
246
|
+
when :negated then (-p).simplify
|
|
247
|
+
when :square then root_of(p, exact_roots_only)
|
|
248
|
+
when :neg_square then root_of((-p).simplify, exact_roots_only)
|
|
249
|
+
end
|
|
250
|
+
next if x.nil? || (x.is_a?(Num) && x.zero?)
|
|
251
|
+
# log(1 + x) and atan(x) are power series of radius 1: outside it
|
|
252
|
+
# the terms do not tend to 0 and the sum has no value, whatever
|
|
253
|
+
# the formula gives there (sum((-2)**k/k) is not -log(3)).
|
|
254
|
+
next if radius_one && !within_radius?(x)
|
|
255
|
+
value = template.call(x).simplify
|
|
256
|
+
next if divergent?(value) # log(1 + x) at x = -1: the harmonic series
|
|
257
|
+
return [m0, value]
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
# binomial theorem: r = (n - k) x / (k + 1)
|
|
261
|
+
p = (r * (k + 1)).cancel
|
|
262
|
+
coeffs = Solve.polynomial_coefficients(p, k)
|
|
263
|
+
if coeffs && coeffs.size == 2
|
|
264
|
+
x = (-coeffs[1]).simplify
|
|
265
|
+
n = (coeffs[0] / x).cancel
|
|
266
|
+
if [n, x].none? { |v| v.variables.include?(k.name) } && binomial_series_converges?(n, x)
|
|
267
|
+
return [0, ((one + x)**n).simplify]
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
_ = two
|
|
271
|
+
nil
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|