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,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