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,980 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RCAS
|
|
4
|
+
# Probability distributions as objects, with exact and symbolic answers.
|
|
5
|
+
#
|
|
6
|
+
# X = Normal(0, 1) X.pdf(x), X.cdf(1), X.quantile(0.975), X.mean
|
|
7
|
+
# B = Binomial(10, 1/2r) B.pdf(3) # => 15/128
|
|
8
|
+
# Poisson(l).pdf(k) # => exp(-l)*l**k/k!
|
|
9
|
+
# X.probability(x > 1) X.probability(-1..1) X.expectation(x**2, x)
|
|
10
|
+
# D = DiscreteUniform(1, 6) D.mean # => 7/2 D.sample(5, random: Random.new(1))
|
|
11
|
+
#
|
|
12
|
+
# Continuous: Normal, Uniform, Exponential. Discrete: Bernoulli, Binomial,
|
|
13
|
+
# Poisson, Geometric (failures before the first success, k >= 0, as in Maple
|
|
14
|
+
# and Mathematica), DiscreteUniform. Parameters may be symbolic. The normal
|
|
15
|
+
# CDF is expressed with erf; its quantile is numeric except at p = 1/2.
|
|
16
|
+
#
|
|
17
|
+
# Sources (keys: MANUAL.md, Sources): [Ros14, ch. 4-5]; erf [AS64, §7.1].
|
|
18
|
+
module Distributions
|
|
19
|
+
# sum(h**m/m!, m, 0, n - 1) for a rational h = p/q, exactly. Over the
|
|
20
|
+
# common denominator q**(n - 1)*(n - 1)! every term is an Integer, and
|
|
21
|
+
# each follows from the one before by an exact division by a small
|
|
22
|
+
# number, so the sum needs one gcd instead of one per term. The
|
|
23
|
+
# Rational sum term by term cost 1.3 s at n = 5000 under Ruby 3.3 and a
|
|
24
|
+
# minute under a Ruby 4 whose bignum gcd is slower (a review, 23 Sept
|
|
25
|
+
# 2026); the powers and factorials one by one had cost 12 s before that.
|
|
26
|
+
def self.exponential_partial_sum(h, n)
|
|
27
|
+
p, q = h.numerator, h.denominator
|
|
28
|
+
top = n - 1
|
|
29
|
+
denominator = q**top * (1..top).reduce(1, :*)
|
|
30
|
+
term = denominator # the m = 0 term, h**0/0! over the denominator
|
|
31
|
+
sum = 0
|
|
32
|
+
(0..top).each do |m|
|
|
33
|
+
sum += term
|
|
34
|
+
term = term * p / (q * (m + 1)) if m < top
|
|
35
|
+
end
|
|
36
|
+
Rational(sum, denominator)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class Distribution
|
|
40
|
+
attr_reader :params
|
|
41
|
+
|
|
42
|
+
def initialize(*params)
|
|
43
|
+
@params = params.map { |p| Expression.lift(p) }
|
|
44
|
+
@params.each { |p| real!(p) }
|
|
45
|
+
validate
|
|
46
|
+
freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# What the parameters have to be. A symbolic parameter is left alone -
|
|
50
|
+
# Normal(mu, sigma) is a legitimate object - but a constant that cannot
|
|
51
|
+
# be one is refused here rather than returning a negative probability
|
|
52
|
+
# later (Binomial(10, 1.5).pdf(3) was -3.1640625). A constant is any
|
|
53
|
+
# expression without indeterminates, decided by `Decide`: only a Num
|
|
54
|
+
# was checked once, so Exponential(-sqrt(2)) had a cdf below 0 and
|
|
55
|
+
# Normal(0, i) a variance of -1 (a review, 23 Sept 2026). An
|
|
56
|
+
# undecided constant is let through, like a symbol.
|
|
57
|
+
def validate; end
|
|
58
|
+
|
|
59
|
+
# Every parameter of every distribution here is real.
|
|
60
|
+
def real!(param)
|
|
61
|
+
return unless param.variables.empty?
|
|
62
|
+
return if Decide.zero?(ComplexParts.im(param)) != false
|
|
63
|
+
raise ArgumentError, "#{name}: the parameters must be real, got #{param}"
|
|
64
|
+
rescue NotImplementedError, RCAS::Unsupported => e
|
|
65
|
+
RCAS.guard!(e, refused: true) # parts rcas cannot take decide nothing
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Raise when the decided sign of the constant parameter fails.
|
|
69
|
+
def positive(param, what) = requires(param, what, "positive") { |sign| %i[negative zero].include?(sign.call(param)) }
|
|
70
|
+
def nonnegative(param, what) = requires(param, what, "not negative") { |sign| sign.call(param) == :negative }
|
|
71
|
+
|
|
72
|
+
def probability_in(param, what)
|
|
73
|
+
requires(param, what, "between 0 and 1") { |sign| sign.call(param) == :negative || sign.call(param - 1) == :positive }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def whole(param, what)
|
|
77
|
+
requires(param, what, "a non-negative whole number") { |sign| sign.call(param) == :negative || Infer.excluded?(param, ZZ) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def requires(param, what, description)
|
|
81
|
+
return unless param.variables.empty?
|
|
82
|
+
failed = yield(->(e) { Decide.sign(Expression.lift(e).simplify) })
|
|
83
|
+
raise ArgumentError, "#{name}: #{what} must be #{description}, got #{param}" if failed
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def name = self.class.name.split("::").last
|
|
87
|
+
def to_s = "#{name}(#{params.join(', ')})"
|
|
88
|
+
def inspect = to_s
|
|
89
|
+
def to_latex(wrap: nil) = "\\mathrm{#{name}}\\left(#{params.map { |p| LaTeX.of(p) }.join(', ')}\\right)"
|
|
90
|
+
def discrete? = false
|
|
91
|
+
def stdev = RCAS.sqrt(variance).simplify
|
|
92
|
+
def median = quantile(Rational(1, 2))
|
|
93
|
+
def ==(other) = other.class == self.class && other.params == params
|
|
94
|
+
alias eql? ==
|
|
95
|
+
def hash = [self.class, params].hash
|
|
96
|
+
|
|
97
|
+
# P(a <= X <= b) for a range, P(X > c) etc. for an inequality x > c.
|
|
98
|
+
def probability(event)
|
|
99
|
+
case event
|
|
100
|
+
when Range
|
|
101
|
+
lo = Expression.lift(event.begin || Neg.new(OO)).simplify
|
|
102
|
+
hi = Expression.lift(event.end || OO).simplify
|
|
103
|
+
# 3/4..1/4 is empty, not a negative probability (T6); an
|
|
104
|
+
# exclusive end is an open end, 2...9/2 includes 4 (P-9)
|
|
105
|
+
order = Inequalities.compare(lo, hi)
|
|
106
|
+
return Num.new(0) if order == 1 || (order&.zero? && event.exclude_end?)
|
|
107
|
+
interval = Interval.new(lo, hi, right_open: event.exclude_end? && !event.end.nil?)
|
|
108
|
+
found = set_probability(interval)
|
|
109
|
+
return found unless order.nil?
|
|
110
|
+
# a symbolic range may turn out reversed, and then it is empty, not
|
|
111
|
+
# cdf(b) - cdf(a) < 0 (fourth review, T6)
|
|
112
|
+
Piecewise.new([[Inequality.new(lo, event.exclude_end? ? :< : :<=, hi), found], [Piecewise::OTHERWISE, Num.new(0)]])
|
|
113
|
+
when Inequality
|
|
114
|
+
# The direct route is for "X op c" alone: both that the left side
|
|
115
|
+
# is the variable and that the right side is free of it. P(X <= X)
|
|
116
|
+
# is 1, not X (22 Sept 2026, the second review). Everything else is
|
|
117
|
+
# solved for the variable and the probability taken over the set
|
|
118
|
+
# that comes out - reading the operator and the right side alone
|
|
119
|
+
# answered P(-X <= 0) with 0.
|
|
120
|
+
return bound_probability(event.op, event.rhs) if direct_event?(event)
|
|
121
|
+
set_probability(event_set(event))
|
|
122
|
+
else raise ArgumentError, "probability: give a range (a..b) or an inequality (x > 1)"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def direct_event?(event) = event.lhs.is_a?(Var) && !event.rhs.variables.include?(event.lhs.name)
|
|
127
|
+
|
|
128
|
+
# P(X > c), which a distribution with a better formula for its upper
|
|
129
|
+
# tail overrides: 1 - cdf(8) cancels every digit of Normal's 6e-16
|
|
130
|
+
# (third review, P-11).
|
|
131
|
+
def survival(c) = (1 - cdf_at(c)).simplify
|
|
132
|
+
|
|
133
|
+
# P(X op c) with the bound c, the shape every event is reduced to.
|
|
134
|
+
def bound_probability(op, c)
|
|
135
|
+
c = Expression.lift(c)
|
|
136
|
+
if discrete?
|
|
137
|
+
case op
|
|
138
|
+
when :<= then cdf_at(c)
|
|
139
|
+
when :< then (cdf_at(c) - pdf_at(c)).simplify
|
|
140
|
+
when :>= then (survival(c) + pdf_at(c)).simplify
|
|
141
|
+
when :> then survival(c)
|
|
142
|
+
else raise ArgumentError, "probability: use <, <=, > or >="
|
|
143
|
+
end
|
|
144
|
+
else
|
|
145
|
+
case op
|
|
146
|
+
when :<, :<= then cdf_at(c)
|
|
147
|
+
when :>, :>= then Limits.infinite?(c) ? (1 - cdf_at(c)).simplify : survival(c)
|
|
148
|
+
else raise ArgumentError, "probability: use <, <=, > or >="
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The set of values of the random variable that the event describes.
|
|
154
|
+
def event_set(event)
|
|
155
|
+
names = event.lhs.variables | event.rhs.variables
|
|
156
|
+
raise ArgumentError, "probability: name one random variable, got #{event}" unless names.size == 1
|
|
157
|
+
set = Inequalities.solve(event, Var.new(names.first))
|
|
158
|
+
raise ArgumentError, "probability: cannot decide #{event}" unless set.is_a?(RealSet) || set.is_a?(Interval)
|
|
159
|
+
set
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The cdf at a bound. At the two infinities it is 1 and 0, which the
|
|
163
|
+
# closed form does not reach on its own - Normal's erf(2**(1/2)*oo)
|
|
164
|
+
# does not fold, and Uniform's (x - a)/(b - a) runs away.
|
|
165
|
+
def cdf_at(v)
|
|
166
|
+
v = Expression.lift(v)
|
|
167
|
+
return cdf(v) unless Limits.infinite?(v)
|
|
168
|
+
Num.new(v == OO ? 1 : 0)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# No mass sits at an infinite point, whatever the closed form says.
|
|
172
|
+
def pdf_at(v)
|
|
173
|
+
v = Expression.lift(v)
|
|
174
|
+
Limits.infinite?(v) ? Num.new(0) : pdf(v)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def set_probability(set)
|
|
178
|
+
intervals = set.is_a?(Interval) ? [set] : set.intervals
|
|
179
|
+
intervals.map { |i| piece_probability(i) }.reduce(Num.new(0)) { |a, b| a + b }.simplify
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# P over one interval. A discrete distribution counts the closed ends
|
|
183
|
+
# in, which the cdf alone does not: cdf(hi) - cdf(lo) leaves out lo.
|
|
184
|
+
def piece_probability(interval)
|
|
185
|
+
lo, hi = interval.low, interval.high
|
|
186
|
+
total =
|
|
187
|
+
if hi == OO && !Limits.infinite?(lo) then survival(lo)
|
|
188
|
+
elsif !Limits.infinite?(lo) && !Limits.infinite?(hi) then between(lo, hi)
|
|
189
|
+
else cdf_at(hi) - cdf_at(lo)
|
|
190
|
+
end
|
|
191
|
+
return total unless discrete?
|
|
192
|
+
total += pdf_at(interval.low) unless interval.left_open
|
|
193
|
+
total -= pdf_at(interval.high) if interval.right_open
|
|
194
|
+
total
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# P(lo < X <= hi) for finite ends: in the upper tail as a difference of
|
|
198
|
+
# survival functions, where cdf(10) - cdf(9) of the standard normal
|
|
199
|
+
# cancelled to 0 (fourth review, P-11); the cdf elsewhere.
|
|
200
|
+
def between(lo, hi)
|
|
201
|
+
upper = !discrete? && lo.variables.empty? && (c = RCAS.real_float(cdf_at(lo))) && c > 0.5
|
|
202
|
+
upper ? (survival(lo) - survival(hi)).simplify : (cdf_at(hi) - cdf_at(lo)).simplify
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# E[f(X)]: an integral or sum over the support (formal when rcas cannot do it).
|
|
206
|
+
def expectation(expr, var = :x)
|
|
207
|
+
x = Expression.lift(var)
|
|
208
|
+
lo, hi = support
|
|
209
|
+
f = (Expression.lift(expr) * pdf(x)).simplify
|
|
210
|
+
discrete? ? Summation.sum(f, x, lo, hi) : Integrate.definite(f, x, lo, hi)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def moment(k, var = :x) = expectation(Expression.lift(var)**k, var)
|
|
214
|
+
|
|
215
|
+
# Random draws (Float, or Integer for discrete distributions); numeric
|
|
216
|
+
# parameters only. Without `random:` the session's RCAS.random draws,
|
|
217
|
+
# so RCAS.random = 42 pins these as it pins every other random object.
|
|
218
|
+
def sample(n = nil, random: nil)
|
|
219
|
+
values = numeric_params
|
|
220
|
+
rng = random || RCAS.random
|
|
221
|
+
n.nil? ? draw(values, rng) : Array.new(n) { draw(values, rng) }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def numeric_params
|
|
225
|
+
params.map do |p|
|
|
226
|
+
v = p.evalf
|
|
227
|
+
raise ArgumentError, "#{self}: numeric parameters are needed here" unless v.is_a?(Numeric) && !v.is_a?(Complex)
|
|
228
|
+
v
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def numeric(x, what)
|
|
233
|
+
v = Expression.lift(x).evalf
|
|
234
|
+
raise ArgumentError, "#{what}: a number is needed, got #{x}" unless v.is_a?(Numeric) && !v.is_a?(Complex)
|
|
235
|
+
v
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def float?(param) = param.is_a?(Num) && param.value.is_a?(Float)
|
|
239
|
+
def exact?(e) = (e = Expression.lift(e)).variables.empty? && e.each_node.none? { |n| n.is_a?(Num) && n.value.is_a?(Float) }
|
|
240
|
+
|
|
241
|
+
# The least k with cdf(k) >= p, the mass added up exactly and compared
|
|
242
|
+
# exactly (Decide for the constants a rate like 1 brings: e**-1/k!).
|
|
243
|
+
def exact_quantile(p, k, hi)
|
|
244
|
+
total = mass(Num.new(k))
|
|
245
|
+
10_000.times do
|
|
246
|
+
order = Inequalities.compare(total, p)
|
|
247
|
+
raise RCAS::Unsupported, "quantile: cannot compare cdf(#{k}) with #{p}" if order.nil?
|
|
248
|
+
return Num.new(k) if order >= 0 || (hi.is_a?(Num) && k >= hi.value)
|
|
249
|
+
k += 1
|
|
250
|
+
total = (total + mass(Num.new(k))).simplify
|
|
251
|
+
end
|
|
252
|
+
raise RCAS::Unsupported, "quantile: the #{p}-quantile lies beyond 10000 steps from the start"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# A quantile is defined for a probability; a number outside [0, 1] is
|
|
256
|
+
# refused rather than answered with log(-2) (third review, P-13).
|
|
257
|
+
# A constant is compared exactly: 2*sqrt(2) and pi are no probabilities
|
|
258
|
+
# either (only a Num was checked: fourth review, P-13).
|
|
259
|
+
def probability!(p)
|
|
260
|
+
v = Expression.lift(p)
|
|
261
|
+
return unless v.variables.empty?
|
|
262
|
+
below = Inequalities.compare(v, Num.new(0))
|
|
263
|
+
above = Inequalities.compare(v, Num.new(1))
|
|
264
|
+
return unless below == -1 || above == 1
|
|
265
|
+
raise ArgumentError, "quantile: #{p} is not a probability, it must lie in [0, 1]"
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Generic quantile: bisection on the numeric CDF (continuous) or a scan (discrete).
|
|
269
|
+
def quantile(p)
|
|
270
|
+
probability!(p)
|
|
271
|
+
pv = numeric(p, "quantile")
|
|
272
|
+
raise ArgumentError, "quantile: p must be in (0, 1)" unless pv > 0 && pv < 1
|
|
273
|
+
if discrete?
|
|
274
|
+
# the mass added up once, k by k, rather than an exact cdf rebuilt
|
|
275
|
+
# for every k (quadratic, and it overflowed for Poisson(200))
|
|
276
|
+
lo, hi = support
|
|
277
|
+
k = lo.evalf.to_i
|
|
278
|
+
target = Expression.lift(p)
|
|
279
|
+
return exact_quantile(target, k, hi) if exact?(target) && params.all? { |q| exact?(q) }
|
|
280
|
+
values = params.all? { |q| q.is_a?(Num) } ? numeric_params : nil
|
|
281
|
+
weight = ->(j) { values && respond_to?(:float_pmf) ? float_pmf(j, values) : pdf(j).evalf.to_f }
|
|
282
|
+
total = weight.call(k)
|
|
283
|
+
# the least k with cdf(k) >= p, allowing only the rounding the Float
|
|
284
|
+
# sum itself has made (a slack of 1e-12 answered 0 for
|
|
285
|
+
# Binomial(3, 1/2) at 1/8 + 1e-14: fourth review)
|
|
286
|
+
while total < pv * (1 - 4 * (k - lo.evalf.to_i + 1) * Float::EPSILON)
|
|
287
|
+
break if hi.is_a?(Num) && k >= hi.value
|
|
288
|
+
k += 1
|
|
289
|
+
total += weight.call(k)
|
|
290
|
+
end
|
|
291
|
+
Num.new(k)
|
|
292
|
+
else
|
|
293
|
+
lo, hi = numeric_bracket
|
|
294
|
+
# the bracket grows until it holds the quantile: the Cauchy
|
|
295
|
+
# quantile at 0.99999 is 31831, past a fixed 10**4 (P-6)
|
|
296
|
+
lower = support.first
|
|
297
|
+
60.times do
|
|
298
|
+
break if cdf(hi).evalf >= pv
|
|
299
|
+
lo = hi
|
|
300
|
+
hi = hi.positive? ? hi * 4 : hi + 1.0
|
|
301
|
+
end
|
|
302
|
+
60.times do
|
|
303
|
+
break if lo <= (Limits.infinite?(lower) ? -Float::INFINITY : lower.evalf) || cdf(lo).evalf <= pv
|
|
304
|
+
hi = lo
|
|
305
|
+
lo = lo.negative? ? lo * 4 : lo - 1.0
|
|
306
|
+
end
|
|
307
|
+
# in the upper half the survival function is the one to solve: the
|
|
308
|
+
# cdf near 1 moves in steps of 1.1e-16, and the normal quantile at
|
|
309
|
+
# 1 - 1e-15 came out as 7.9364 for 7.9414 (fourth review, P-6)
|
|
310
|
+
upper = pv > 0.5
|
|
311
|
+
tail = 1.0 - pv
|
|
312
|
+
below = ->(v) { upper ? survival(Num.new(v)).evalf > tail : cdf(v).evalf < pv }
|
|
313
|
+
200.times do
|
|
314
|
+
mid = (lo + hi) / 2.0
|
|
315
|
+
break if mid == lo || mid == hi
|
|
316
|
+
below.call(mid) ? lo = mid : hi = mid
|
|
317
|
+
end
|
|
318
|
+
x = (lo + hi) / 2.0
|
|
319
|
+
3.times do # Newton polish, kept only while it is a small correction
|
|
320
|
+
density = pdf(x).evalf
|
|
321
|
+
break unless density.is_a?(Numeric) && density.positive?
|
|
322
|
+
miss = upper ? tail - survival(Num.new(x)).evalf : cdf(x).evalf - pv
|
|
323
|
+
step = x - miss / density
|
|
324
|
+
break unless step.finite? && (step - x).abs <= 1e-6 * [1.0, x.abs].max
|
|
325
|
+
x = step
|
|
326
|
+
end
|
|
327
|
+
Num.new(x)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# ---- continuous ----------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
class Normal < Distribution
|
|
335
|
+
def validate = positive(sigma, "the standard deviation")
|
|
336
|
+
def mu = params[0]
|
|
337
|
+
def sigma = params[1]
|
|
338
|
+
def support = [Neg.new(OO), OO]
|
|
339
|
+
def pdf(x) = (Fn.new(:exp, [-((Expression.lift(x) - mu)**2) / (2 * sigma**2)]) / (sigma * RCAS.sqrt(2 * PI))).simplify
|
|
340
|
+
# Left of the mean the cdf is erfc(...)/2, which keeps the digits of a
|
|
341
|
+
# far tail; 1/2 + erf(...)/2 cancels them all at -8 sigma (P-11).
|
|
342
|
+
def cdf(x)
|
|
343
|
+
z = ((Expression.lift(x) - mu) / (sigma * RCAS.sqrt(2))).simplify
|
|
344
|
+
return (RCAS.erfc(Neg.new(z).simplify) / 2).simplify if Decide.sign(z) == :negative
|
|
345
|
+
((1 + RCAS.erf(z)) / 2).simplify
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def survival(c)
|
|
349
|
+
z = ((Expression.lift(c) - mu) / (sigma * RCAS.sqrt(2))).simplify
|
|
350
|
+
return (RCAS.erfc(z) / 2).simplify if Decide.sign(z) == :positive
|
|
351
|
+
super
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Both ends in one tail: erfc there, which keeps the digits; across
|
|
355
|
+
# the mean: (erf(b) - erf(a))/2, so P(-1 <= X <= 1) is erf(sqrt(2)/2).
|
|
356
|
+
def between(lo, hi)
|
|
357
|
+
a, b = [lo, hi].map { |v| ((Expression.lift(v) - mu) / (sigma * RCAS.sqrt(2))).simplify }
|
|
358
|
+
sa, sb = [a, b].map { |z| Decide.sign(z) }
|
|
359
|
+
return ((RCAS.erfc(a) - RCAS.erfc(b)) / 2).simplify if sa == :positive && %i[positive zero].include?(sb)
|
|
360
|
+
return ((RCAS.erfc(Neg.new(b).simplify) - RCAS.erfc(Neg.new(a).simplify)) / 2).simplify if sb == :negative
|
|
361
|
+
return ((RCAS.erf(b) - RCAS.erf(a)) / 2).simplify if sa && sb
|
|
362
|
+
super
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def mean = mu
|
|
366
|
+
def variance = (sigma**2).simplify
|
|
367
|
+
def median = mu
|
|
368
|
+
def skewness = Num.new(0)
|
|
369
|
+
def kurtosis = Num.new(3)
|
|
370
|
+
|
|
371
|
+
# E[X^k] = sum_j binomial(k, 2j) mu^(k-2j) sigma^(2j) (2j - 1)!!
|
|
372
|
+
def moment(k, _var = :x)
|
|
373
|
+
return super unless k.is_a?(Integer) && k >= 0
|
|
374
|
+
total = (0..k / 2).map do |j|
|
|
375
|
+
double_factorial = (1..2 * j - 1).step(2).reduce(1, :*)
|
|
376
|
+
RCAS.binomial(k, 2 * j) * mu**(k - 2 * j) * sigma**(2 * j) * double_factorial
|
|
377
|
+
end.reduce(:+)
|
|
378
|
+
total.expand.simplify
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def quantile(p)
|
|
382
|
+
probability!(p)
|
|
383
|
+
pv = Expression.lift(p)
|
|
384
|
+
return mu if pv.is_a?(Num) && pv.value == Rational(1, 2)
|
|
385
|
+
super
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def numeric_bracket
|
|
389
|
+
m, s = numeric_params
|
|
390
|
+
[m - 40.0 * s, m + 40.0 * s]
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def draw(values, random)
|
|
394
|
+
m, s = values
|
|
395
|
+
u1 = 1.0 - random.rand
|
|
396
|
+
u2 = random.rand
|
|
397
|
+
m + s * Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2 * Math::PI * u2)
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
class Uniform < Distribution
|
|
402
|
+
def validate
|
|
403
|
+
return unless a.variables.empty? && b.variables.empty?
|
|
404
|
+
raise ArgumentError, "Uniform: the range is empty (#{a} to #{b})" if %i[negative zero].include?(Decide.sign((b - a).simplify))
|
|
405
|
+
end
|
|
406
|
+
def a = params[0]
|
|
407
|
+
def b = params[1]
|
|
408
|
+
def support = [a, b]
|
|
409
|
+
|
|
410
|
+
# The density on the support; 0 outside. A symbolic point gets the
|
|
411
|
+
# case split, which is what makes a later substitution right:
|
|
412
|
+
# P(X <= a) at a = 2 is 1, and a bare (a - 0)/(1 - 0) said 2 (T6).
|
|
413
|
+
def pdf(x)
|
|
414
|
+
x = Expression.lift(x)
|
|
415
|
+
return Num.new(0) if x.is_a?(Num) && (v = numeric_params) && !x.value.between?(v[0], v[1])
|
|
416
|
+
return (1 / (b - a)).simplify if x.is_a?(Num)
|
|
417
|
+
RCAS.piecewise(x < a => 0, x <= b => (1 / (b - a)).simplify, :else => 0)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def cdf(x)
|
|
421
|
+
x = Expression.lift(x)
|
|
422
|
+
if x.is_a?(Num)
|
|
423
|
+
lo, hi = numeric_params
|
|
424
|
+
return Num.new(0) if x.value < lo
|
|
425
|
+
return Num.new(1) if x.value > hi
|
|
426
|
+
return ((x - a) / (b - a)).simplify
|
|
427
|
+
end
|
|
428
|
+
RCAS.piecewise(x < a => 0, x <= b => ((x - a) / (b - a)).simplify, :else => 1)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def quantile(p)
|
|
432
|
+
probability!(p)
|
|
433
|
+
(a + Expression.lift(p) * (b - a)).simplify
|
|
434
|
+
end
|
|
435
|
+
def moment(k, _var = :x) = k.is_a?(Integer) && k >= 0 ? ((b**(k + 1) - a**(k + 1)) / ((k + 1) * (b - a))).cancel : super
|
|
436
|
+
def mean = ((a + b) / 2).simplify
|
|
437
|
+
def variance = ((b - a)**2 / 12).simplify
|
|
438
|
+
def skewness = Num.new(0)
|
|
439
|
+
def kurtosis = Num.new(Rational(9, 5))
|
|
440
|
+
def draw(values, random) = values[0] + random.rand * (values[1] - values[0])
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
class Exponential < Distribution
|
|
444
|
+
def validate = positive(rate, "the rate")
|
|
445
|
+
def rate = params[0]
|
|
446
|
+
def support = [Num.new(0), OO]
|
|
447
|
+
|
|
448
|
+
def pdf(x)
|
|
449
|
+
x = Expression.lift(x)
|
|
450
|
+
return Num.new(0) if x.is_a?(Num) && x.value.negative?
|
|
451
|
+
density = (rate * Fn.new(:exp, [-rate * x])).simplify
|
|
452
|
+
symbolic_support?(x) ? RCAS.piecewise(x < 0 => 0, :else => density) : density
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def cdf(x)
|
|
456
|
+
x = Expression.lift(x)
|
|
457
|
+
return Num.new(0) if x.is_a?(Num) && x.value.negative?
|
|
458
|
+
value = (1 - Fn.new(:exp, [-rate * x])).simplify
|
|
459
|
+
symbolic_support?(x) ? RCAS.piecewise(x < 0 => 0, :else => value) : value
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# A point whose side of 0 is not known: the support has to show.
|
|
463
|
+
def symbolic_support?(x) = !x.variables.empty? && !%i[positive nonnegative].include?(RCAS.sign_of(x))
|
|
464
|
+
|
|
465
|
+
def quantile(p)
|
|
466
|
+
probability!(p)
|
|
467
|
+
p = Expression.lift(p)
|
|
468
|
+
return OO if p.is_a?(Num) && p.value == 1 # the whole mass lies below oo alone
|
|
469
|
+
(Fn.new(:log, [1 / (1 - p)]) / rate).simplify
|
|
470
|
+
end
|
|
471
|
+
def moment(k, _var = :x) = k.is_a?(Integer) && k >= 0 ? (RCAS.factorial(k) / rate**k).simplify : super
|
|
472
|
+
def mean = (1 / rate).simplify
|
|
473
|
+
def variance = (1 / rate**2).simplify
|
|
474
|
+
def skewness = Num.new(2)
|
|
475
|
+
def kurtosis = Num.new(9)
|
|
476
|
+
def draw(values, random) = -Math.log(1.0 - random.rand) / values[0]
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# ---- discrete ---------------------------------------------------------------------
|
|
480
|
+
|
|
481
|
+
class Discrete < Distribution
|
|
482
|
+
def discrete? = true
|
|
483
|
+
|
|
484
|
+
# P(X > c) past the mean as the sum of the tail itself: 1 - cdf in
|
|
485
|
+
# Floats gave Binomial(100, 0.5) the probability -3.2e-14 of X > 95
|
|
486
|
+
# (fourth review, P-11). Exact parameters keep 1 - cdf, which is exact
|
|
487
|
+
# and which evalf now evaluates without cancelling.
|
|
488
|
+
def survival(c)
|
|
489
|
+
c = Expression.lift(c)
|
|
490
|
+
lo, hi = support
|
|
491
|
+
return super unless c.variables.empty? && params.all? { |q| q.is_a?(Num) } && params.any? { |q| float?(q) }
|
|
492
|
+
top = floor_of(c) or return super
|
|
493
|
+
mean_value = RCAS.real_float(mean)
|
|
494
|
+
return super if mean_value.nil? || top < mean_value
|
|
495
|
+
values = numeric_params
|
|
496
|
+
weight = ->(j) { respond_to?(:float_pmf) ? float_pmf(j, values) : mass(Num.new(j)).evalf.to_f }
|
|
497
|
+
last = Limits.infinite?(hi) ? nil : hi.value.to_i
|
|
498
|
+
total = 0.0
|
|
499
|
+
k = [top + 1, lo.value.to_i].max
|
|
500
|
+
100_000.times do
|
|
501
|
+
break if last && k > last
|
|
502
|
+
term = weight.call(k)
|
|
503
|
+
total += term
|
|
504
|
+
break if last.nil? && term <= total * Float::EPSILON / 4
|
|
505
|
+
k += 1
|
|
506
|
+
end
|
|
507
|
+
Num.new(total)
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# The mass at a point. At a symbolic point the formula holds only on
|
|
511
|
+
# the support and at whole numbers, so it comes as a piecewise that
|
|
512
|
+
# says so (the pmf of Binomial(10, 1/2) at 5/2 was not 0, and the cdf
|
|
513
|
+
# of a die was 3/2 at 9: fourth review, T6) - unless the assumptions
|
|
514
|
+
# already put the point there.
|
|
515
|
+
def pdf(k)
|
|
516
|
+
k = Expression.lift(k)
|
|
517
|
+
return mass(k) if k.variables.empty? || on_support?(k)
|
|
518
|
+
lo, hi = support
|
|
519
|
+
branches = [[Inequality.new(k, :<, lo), Num.new(0)]]
|
|
520
|
+
branches << [Inequality.new(hi, :<, k), Num.new(0)] unless Limits.infinite?(hi)
|
|
521
|
+
branches << [Membership.new(k, ZZ), mass(k)]
|
|
522
|
+
Piecewise.new(branches + [[Piecewise::OTHERWISE, Num.new(0)]])
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# k is a whole number between the ends of the support, by the
|
|
526
|
+
# assumptions.
|
|
527
|
+
def on_support?(k)
|
|
528
|
+
domain = Infer.domain(k)
|
|
529
|
+
return false unless domain && domain <= ZZ
|
|
530
|
+
lo, hi = support
|
|
531
|
+
above = Limits.infinite?(lo) || %i[positive nonnegative].include?(RCAS.sign_of((k - lo).simplify)) ||
|
|
532
|
+
(domain <= NN && lo.is_a?(Num) && lo.value <= 0)
|
|
533
|
+
below = Limits.infinite?(hi) || %i[positive nonnegative].include?(RCAS.sign_of((hi - k).simplify))
|
|
534
|
+
above && below
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Sum of the mass over the support up to k, exactly for numeric k; at a
|
|
538
|
+
# symbolic k the sum up to floor(k), inside the support.
|
|
539
|
+
def cdf(k)
|
|
540
|
+
k = Expression.lift(k)
|
|
541
|
+
return symbolic_cdf(k) unless k.variables.empty?
|
|
542
|
+
lo, hi = support
|
|
543
|
+
# a constant bound that is not a number: the values are integers, so
|
|
544
|
+
# X <= sqrt(5) is X <= 2 (a formal sum up to sqrt(5) summed
|
|
545
|
+
# binomial(10, sqrt(5)): third review, P-9)
|
|
546
|
+
k = Num.new(floor_of(k)) if !k.is_a?(Num) && k.variables.empty? && floor_of(k)
|
|
547
|
+
if k.is_a?(Num) && k.value.real? && lo.is_a?(Num)
|
|
548
|
+
return Num.new(0) if k.value < lo.value
|
|
549
|
+
return Num.new(1) if hi.is_a?(Num) && k.value >= hi.value
|
|
550
|
+
top = k.value.floor
|
|
551
|
+
return (lo.value.to_i..top).map { |j| mass(Num.new(j)) }.reduce(:+).simplify
|
|
552
|
+
end
|
|
553
|
+
j = Var.new(:j)
|
|
554
|
+
Summation.sum(mass(j), j, lo, k)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def symbolic_cdf(k)
|
|
558
|
+
integer = (d = Infer.domain(k)) && d <= ZZ
|
|
559
|
+
top = integer ? k : Fn.new(:floor, [k])
|
|
560
|
+
formula = respond_to?(:cdf_formula) ? cdf_formula(top) : Summation.sum(mass(Var.new(:j)), Var.new(:j), support.first, top)
|
|
561
|
+
return formula if on_support?(k)
|
|
562
|
+
lo, hi = support
|
|
563
|
+
branches = [[Inequality.new(k, :<, lo), Num.new(0)]]
|
|
564
|
+
branches << [Inequality.new(hi, :<=, k), Num.new(1)] unless Limits.infinite?(hi)
|
|
565
|
+
Piecewise.new(branches + [[Piecewise::OTHERWISE, formula]])
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# floor of a real constant, decided exactly; nil when it cannot be.
|
|
569
|
+
def floor_of(v)
|
|
570
|
+
f = Analysis.numeric(v) or return nil
|
|
571
|
+
n = f.floor
|
|
572
|
+
n -= 1 if Inequalities.compare(v, Num.new(n)) == -1
|
|
573
|
+
n += 1 if Inequalities.compare(v, Num.new(n + 1)) != -1
|
|
574
|
+
Inequalities.compare(v, Num.new(n)) != -1 && Inequalities.compare(v, Num.new(n + 1)) == -1 ? n : nil
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# No mass at a point that is not an integer.
|
|
578
|
+
def pdf_at(v)
|
|
579
|
+
v = Expression.lift(v)
|
|
580
|
+
return Num.new(0) if Limits.infinite?(v)
|
|
581
|
+
if !v.is_a?(Num) && v.variables.empty? && (f = floor_of(v)) && Inequalities.compare(v, Num.new(f)) != 0
|
|
582
|
+
return Num.new(0)
|
|
583
|
+
end
|
|
584
|
+
pdf(v)
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def draw(values, random)
|
|
588
|
+
u = random.rand
|
|
589
|
+
k = support.first.evalf.to_i
|
|
590
|
+
weight = ->(j) { respond_to?(:float_pmf) ? float_pmf(j, values) : mass(Num.new(j)).evalf }
|
|
591
|
+
total = weight.call(k)
|
|
592
|
+
while total < u
|
|
593
|
+
k += 1
|
|
594
|
+
total += weight.call(k)
|
|
595
|
+
end
|
|
596
|
+
k
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
class Bernoulli < Discrete
|
|
601
|
+
def validate = probability_in(p, "the probability")
|
|
602
|
+
def p = params[0]
|
|
603
|
+
def support = [Num.new(0), Num.new(1)]
|
|
604
|
+
|
|
605
|
+
def mass(k)
|
|
606
|
+
k = Expression.lift(k)
|
|
607
|
+
if k.is_a?(Num)
|
|
608
|
+
return p if k.value == 1
|
|
609
|
+
return (1 - p).simplify if k.value.zero?
|
|
610
|
+
return Num.new(0)
|
|
611
|
+
end
|
|
612
|
+
(p**k * (1 - p)**(1 - k)).simplify
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def mean = p
|
|
616
|
+
def variance = (p * (1 - p)).simplify
|
|
617
|
+
def skewness = ((1 - 2 * p) / RCAS.sqrt(p * (1 - p))).simplify
|
|
618
|
+
def kurtosis = ((1 - 3 * p * (1 - p)) / (p * (1 - p))).simplify
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
class Binomial < Discrete
|
|
622
|
+
def validate
|
|
623
|
+
whole(n, "the number of trials")
|
|
624
|
+
probability_in(p, "the probability")
|
|
625
|
+
end
|
|
626
|
+
def n = params[0]
|
|
627
|
+
def p = params[1]
|
|
628
|
+
def support = [Num.new(0), n]
|
|
629
|
+
|
|
630
|
+
def mass(k)
|
|
631
|
+
k = Expression.lift(k)
|
|
632
|
+
return Num.new(0) if k.is_a?(Num) && n.is_a?(Num) && (k.value.negative? || k.value > n.value || !k.value.integer?)
|
|
633
|
+
# a Float probability is a numeric question, and the exact binomial
|
|
634
|
+
# coefficient times p**k overflows long before the product does
|
|
635
|
+
# (C(1100, 550)/2**1100 was Infinity: third review, P-4)
|
|
636
|
+
if float?(p) && k.is_a?(Num) && n.is_a?(Num) && n.value.is_a?(Integer)
|
|
637
|
+
return Num.new(Distributions.binomial_pmf(n.value, k.value.to_i, p.value))
|
|
638
|
+
end
|
|
639
|
+
(RCAS.binomial(n, k) * p**k * (1 - p)**(n - k)).simplify
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def float_pmf(k, values) = Distributions.binomial_pmf(values[0].to_i, k, values[1].to_f)
|
|
643
|
+
|
|
644
|
+
def mean = (n * p).simplify
|
|
645
|
+
def variance = (n * p * (1 - p)).simplify
|
|
646
|
+
def skewness = ((1 - 2 * p) / RCAS.sqrt(n * p * (1 - p))).simplify
|
|
647
|
+
def kurtosis = (3 + (1 - 6 * p * (1 - p)) / (n * p * (1 - p))).simplify
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
class Poisson < Discrete
|
|
651
|
+
def validate = positive(rate, "the rate")
|
|
652
|
+
def rate = params[0]
|
|
653
|
+
def support = [Num.new(0), OO]
|
|
654
|
+
|
|
655
|
+
def mass(k)
|
|
656
|
+
k = Expression.lift(k)
|
|
657
|
+
return Num.new(0) if k.is_a?(Num) && (k.value.negative? || !k.value.integer?)
|
|
658
|
+
return Num.new(Distributions.poisson_pmf(k.value.to_i, rate.value)) if float?(rate) && k.is_a?(Num)
|
|
659
|
+
(Fn.new(:exp, [-rate]) * rate**k / RCAS.factorial(k)).simplify
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def float_pmf(k, values) = Distributions.poisson_pmf(k, values[0].to_f)
|
|
663
|
+
|
|
664
|
+
def mean = rate
|
|
665
|
+
def variance = rate
|
|
666
|
+
def skewness = (1 / RCAS.sqrt(rate)).simplify
|
|
667
|
+
def kurtosis = (3 + 1 / rate).simplify
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
# Failures before the first success: P(X = k) = (1 - p)**k p, k = 0, 1, ...
|
|
671
|
+
class Geometric < Discrete
|
|
672
|
+
def validate
|
|
673
|
+
positive(p, "the probability")
|
|
674
|
+
probability_in(p, "the probability")
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def p = params[0]
|
|
678
|
+
def support = [Num.new(0), OO]
|
|
679
|
+
|
|
680
|
+
def mass(k)
|
|
681
|
+
k = Expression.lift(k)
|
|
682
|
+
return Num.new(0) if k.is_a?(Num) && (k.value.negative? || !k.value.integer?)
|
|
683
|
+
((1 - p)**k * p).simplify
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# at a whole k >= 0
|
|
687
|
+
def cdf_formula(k) = (1 - (1 - p)**(k + 1)).simplify
|
|
688
|
+
|
|
689
|
+
def mean = ((1 - p) / p).simplify
|
|
690
|
+
def variance = ((1 - p) / p**2).simplify
|
|
691
|
+
def skewness = ((2 - p) / RCAS.sqrt(1 - p)).simplify
|
|
692
|
+
def kurtosis = (9 + p**2 / (1 - p)).simplify
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# ---- sampling helpers ------------------------------------------------------------
|
|
696
|
+
|
|
697
|
+
module_function
|
|
698
|
+
|
|
699
|
+
# The pmfs in logarithms, for Float parameters: C(n, k) p**k (1 - p)**(n - k)
|
|
700
|
+
# and exp(-l) l**k/k! without the overflow of their parts.
|
|
701
|
+
# The exact binomial coefficient times the powers where the Floats hold
|
|
702
|
+
# them - C(10, 5)/2**10 is 0.24609375 to the last bit, which lgamma's
|
|
703
|
+
# exp(log ...) was not (fourth review) - and the logarithms where a
|
|
704
|
+
# product under- or overflows.
|
|
705
|
+
def binomial_pmf(n, k, p)
|
|
706
|
+
return 0.0 if k.negative? || k > n
|
|
707
|
+
return (k == 0 ? 1.0 : 0.0) if p.zero?
|
|
708
|
+
return (k == n ? 1.0 : 0.0) if p == 1.0
|
|
709
|
+
if [k, n - k].min <= 2000
|
|
710
|
+
exact = (1..k).reduce(1) { |c, i| c * (n - k + i) / i }
|
|
711
|
+
# past 2**1024 it is no Float (to_f warns and gives Infinity); the
|
|
712
|
+
# logarithms below take over
|
|
713
|
+
coefficient = exact.to_f if exact.bit_length <= 1023
|
|
714
|
+
end
|
|
715
|
+
if coefficient&.finite?
|
|
716
|
+
rest = p < 1e-4 ? Math.exp((n - k) * log_one_minus(p)) : (1.0 - p)**(n - k)
|
|
717
|
+
direct = coefficient * p**k * rest
|
|
718
|
+
return direct if direct.finite? && direct > 1e-290
|
|
719
|
+
end
|
|
720
|
+
Math.exp(Math.lgamma(n + 1).first - Math.lgamma(k + 1).first - Math.lgamma(n - k + 1).first +
|
|
721
|
+
k * Math.log(p) + (n - k) * log_one_minus(p))
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# log(1 - p) without losing a small p to the 1 (Ruby's Math has no log1p)
|
|
725
|
+
def log_one_minus(p) = p.abs < 1e-4 ? -(p + p * p / 2 + p**3 / 3) : Math.log(1 - p)
|
|
726
|
+
|
|
727
|
+
def poisson_pmf(k, rate)
|
|
728
|
+
return 0.0 if k.negative?
|
|
729
|
+
if k <= 170
|
|
730
|
+
direct = Math.exp(-rate) * rate**k / (1..k).reduce(1, :*).to_f
|
|
731
|
+
return direct if direct.finite? && direct > 1e-290
|
|
732
|
+
end
|
|
733
|
+
Math.exp(-rate + k * Math.log(rate) - Math.lgamma(k + 1).first)
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def normal_variate(random) = Math.sqrt(-2.0 * Math.log(1.0 - random.rand)) * Math.cos(2 * Math::PI * random.rand)
|
|
737
|
+
|
|
738
|
+
# Marsaglia-Tsang squeeze method for Gamma(shape, 1) [MT00].
|
|
739
|
+
def gamma_variate(shape, random)
|
|
740
|
+
return gamma_variate(shape + 1.0, random) * random.rand**(1.0 / shape) if shape < 1.0
|
|
741
|
+
d = shape - 1.0 / 3.0
|
|
742
|
+
c = 1.0 / Math.sqrt(9.0 * d)
|
|
743
|
+
loop do
|
|
744
|
+
x = normal_variate(random)
|
|
745
|
+
v = (1.0 + c * x)**3
|
|
746
|
+
next if v <= 0
|
|
747
|
+
u = random.rand
|
|
748
|
+
return d * v if u < 1.0 - 0.0331 * x**4
|
|
749
|
+
return d * v if Math.log(u) < 0.5 * x * x + d * (1.0 - v + Math.log(v))
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
# ---- the sampling distributions ---------------------------------------------------
|
|
754
|
+
|
|
755
|
+
# Student's t with nu degrees of freedom. The CDF is exact for nu = 1
|
|
756
|
+
# (Cauchy) and nu = 2, numeric otherwise (regularized incomplete beta).
|
|
757
|
+
class StudentT < Distribution
|
|
758
|
+
def validate = positive(nu, "the degrees of freedom")
|
|
759
|
+
def nu = params[0]
|
|
760
|
+
def support = [Neg.new(OO), OO]
|
|
761
|
+
|
|
762
|
+
def pdf(x)
|
|
763
|
+
x = Expression.lift(x)
|
|
764
|
+
(RCAS.gamma((nu + 1) / 2) / (RCAS.sqrt(nu * PI) * RCAS.gamma(nu / 2)) * (1 + x**2 / nu)**(-(nu + 1) / 2)).simplify
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def cdf(x)
|
|
768
|
+
x = Expression.lift(x)
|
|
769
|
+
return ((1 + 2 * Fn.new(:atan, [x]) / PI) / 2).simplify if nu == Num.new(1)
|
|
770
|
+
return (Num.new(1) / 2 + x / (2 * RCAS.sqrt(2 + x**2))).simplify if nu == Num.new(2)
|
|
771
|
+
Num.new(1.0 - upper_tail(numeric(nu, "cdf"), numeric(x, "cdf")))
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# The upper tail directly: atan(1/t)/pi for the Cauchy distribution and
|
|
775
|
+
# 1/(s*(s + t)) with s = sqrt(2 + t**2) for two degrees of freedom, where
|
|
776
|
+
# 1 - cdf cancelled (fourth review, P-11); the incomplete beta otherwise.
|
|
777
|
+
def survival(c)
|
|
778
|
+
c = Expression.lift(c)
|
|
779
|
+
if (nu == Num.new(1) || nu == Num.new(2)) && Decide.sign(c) == :positive
|
|
780
|
+
return (Fn.new(:atan, [1 / c]) / PI).simplify if nu == Num.new(1)
|
|
781
|
+
root = RCAS.sqrt(2 + c**2)
|
|
782
|
+
return (1 / (root * (root + c))).simplify
|
|
783
|
+
end
|
|
784
|
+
return super if [1, 2].any? { |n| nu == Num.new(n) } || !c.variables.empty?
|
|
785
|
+
Num.new(upper_tail(numeric(nu, "cdf"), numeric(c, "cdf")))
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# P(T > t) in Floats. Near 0 the tail is 1/2 - I_y(1/2, nu/2)/2 with
|
|
789
|
+
# y = t**2/(nu + t**2) computed directly - through x = nu/(nu + t**2)
|
|
790
|
+
# the digits of cdf(1e-9) were lost to the rounding of x; far out it is
|
|
791
|
+
# I_x(nu/2, 1/2)/2. For a huge nu the incomplete beta does not settle,
|
|
792
|
+
# and the normal limit with its 1/nu correction is exact to 1/nu**2.
|
|
793
|
+
def upper_tail(v, t)
|
|
794
|
+
if v > 1e7
|
|
795
|
+
phi = Math.exp(-t * t / 2) / Math.sqrt(2 * Math::PI)
|
|
796
|
+
return Math.erfc(t / Math.sqrt(2)) / 2 + phi * (t**3 + t) / (4 * v)
|
|
797
|
+
end
|
|
798
|
+
if t * t < v
|
|
799
|
+
half = Special.beta_i(t * t / (v + t * t), 0.5, v / 2.0) / 2.0
|
|
800
|
+
return t.negative? ? 0.5 + half : 0.5 - half
|
|
801
|
+
end
|
|
802
|
+
tail = Special.beta_i(v / (v + t * t), v / 2.0, 0.5) / 2.0
|
|
803
|
+
t.positive? ? tail : 1.0 - tail
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
# symmetric about 0: the median is 0, not the -2e-8 a bisection stops at
|
|
807
|
+
def quantile(p)
|
|
808
|
+
probability!(p)
|
|
809
|
+
q = Expression.lift(p)
|
|
810
|
+
return Num.new(0) if q.is_a?(Num) && q.value == Rational(1, 2)
|
|
811
|
+
super
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
# A moment exists only for enough degrees of freedom: the Cauchy
|
|
815
|
+
# distribution (nu = 1) has no mean, and its "variance" came out as -1
|
|
816
|
+
# (third review, P-7). For a number below the threshold the answer is
|
|
817
|
+
# oo where the integral diverges to it and undefined where it has no
|
|
818
|
+
# value; a symbolic nu keeps the formula, which holds above it.
|
|
819
|
+
def mean = moment_exists(1, UNDEFINED) { Num.new(0) }
|
|
820
|
+
def median = Num.new(0)
|
|
821
|
+
def variance = moment_exists(2, nu_above?(1) ? OO : UNDEFINED) { (nu / (nu - 2)).simplify }
|
|
822
|
+
def skewness = moment_exists(3, UNDEFINED) { Num.new(0) }
|
|
823
|
+
def kurtosis = moment_exists(4, nu_above?(2) ? OO : UNDEFINED) { (3 + 6 / (nu - 4)).simplify }
|
|
824
|
+
|
|
825
|
+
# The formula only where the moment exists: it is taken after the test,
|
|
826
|
+
# nu/(nu - 2) at nu = 2 divided by zero before oo was ever answered
|
|
827
|
+
# (fourth review, P-7).
|
|
828
|
+
def moment_exists(order, otherwise)
|
|
829
|
+
return yield unless nu.is_a?(Num) && nu.value.real?
|
|
830
|
+
nu.value > order ? yield : otherwise
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
def nu_above?(threshold) = nu.is_a?(Num) && nu.value.real? && nu.value > threshold
|
|
834
|
+
def numeric_bracket = [-10.0, 10.0]
|
|
835
|
+
def draw(values, random) = Distributions.normal_variate(random) / Math.sqrt(2.0 * Distributions.gamma_variate(values[0] / 2.0, random) / values[0])
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
# Chi-square with k degrees of freedom; the CDF is exact for even k.
|
|
839
|
+
class ChiSquare < Distribution
|
|
840
|
+
def validate = positive(k, "the degrees of freedom")
|
|
841
|
+
def k = params[0]
|
|
842
|
+
def support = [Num.new(0), OO]
|
|
843
|
+
|
|
844
|
+
def pdf(x)
|
|
845
|
+
x = Expression.lift(x)
|
|
846
|
+
return Num.new(0) if x.is_a?(Num) && x.value.real? && x.value.negative?
|
|
847
|
+
(x**(k / 2 - 1) * Fn.new(:exp, [-x / 2]) / (2**(k / 2) * RCAS.gamma(k / 2))).simplify
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def cdf(x)
|
|
851
|
+
x = Expression.lift(x)
|
|
852
|
+
return Num.new(0) if x.is_a?(Num) && x.value.real? && !x.value.positive?
|
|
853
|
+
# a Float point is a numeric question: the closed form's terms
|
|
854
|
+
# (x/2)**149/149! overflow at k = 300 (P-4)
|
|
855
|
+
return Num.new(Special.gamma_p(numeric(k, "cdf") / 2.0, x.value / 2.0)) if x.is_a?(Num) && x.value.is_a?(Float)
|
|
856
|
+
if k.is_a?(Num) && k.value.is_a?(Integer) && k.value.even? && k.value.positive?
|
|
857
|
+
half = (x / 2).simplify
|
|
858
|
+
tail = if half.is_a?(Num) && (half.value.is_a?(Integer) || half.value.is_a?(Rational))
|
|
859
|
+
Num.new(Simplify.normalize_number(Distributions.exponential_partial_sum(half.value.to_r, k.value / 2)))
|
|
860
|
+
else
|
|
861
|
+
(0...k.value / 2).map { |m| half**m / RCAS.factorial(m) }.reduce(:+)
|
|
862
|
+
end
|
|
863
|
+
return (1 - Fn.new(:exp, [-half]) * tail).simplify
|
|
864
|
+
end
|
|
865
|
+
Num.new(Special.gamma_p(numeric(k, "cdf") / 2.0, numeric(x, "cdf") / 2.0))
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
def survival(c)
|
|
869
|
+
c = Expression.lift(c)
|
|
870
|
+
return super unless c.variables.empty? && !(k.is_a?(Num) && k.value.is_a?(Integer) && k.value.even? && !c.is_a?(Num))
|
|
871
|
+
return super if k.is_a?(Num) && k.value.is_a?(Integer) && k.value.even? && c.is_a?(Num) && !c.value.is_a?(Float)
|
|
872
|
+
Num.new(Special.gamma_q(numeric(k, "cdf") / 2.0, numeric(c, "cdf") / 2.0))
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def mean = k
|
|
876
|
+
def variance = (2 * k).simplify
|
|
877
|
+
def skewness = RCAS.sqrt(8 / k).simplify
|
|
878
|
+
def kurtosis = (3 + 12 / k).simplify
|
|
879
|
+
|
|
880
|
+
def numeric_bracket
|
|
881
|
+
v = numeric_params.first
|
|
882
|
+
[0.0, v + 20.0 * Math.sqrt(2.0 * v) + 40.0]
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
def draw(values, random) = 2.0 * Distributions.gamma_variate(values[0] / 2.0, random)
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# The F (variance ratio) distribution with d1 and d2 degrees of freedom.
|
|
889
|
+
class FRatio < Distribution
|
|
890
|
+
def validate
|
|
891
|
+
positive(d1, "the numerator degrees of freedom")
|
|
892
|
+
positive(d2, "the denominator degrees of freedom")
|
|
893
|
+
end
|
|
894
|
+
def d1 = params[0]
|
|
895
|
+
def d2 = params[1]
|
|
896
|
+
def support = [Num.new(0), OO]
|
|
897
|
+
|
|
898
|
+
def pdf(x)
|
|
899
|
+
x = Expression.lift(x)
|
|
900
|
+
return Num.new(0) if x.is_a?(Num) && x.value.real? && !x.value.positive?
|
|
901
|
+
beta = (RCAS.gamma(d1 / 2) * RCAS.gamma(d2 / 2) / RCAS.gamma((d1 + d2) / 2)).simplify
|
|
902
|
+
((d1 / d2)**(d1 / 2) * x**(d1 / 2 - 1) * (1 + d1 * x / d2)**(-(d1 + d2) / 2) / beta).simplify
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
def cdf(x)
|
|
906
|
+
x = Expression.lift(x)
|
|
907
|
+
return Num.new(0) if x.is_a?(Num) && x.value.real? && !x.value.positive?
|
|
908
|
+
a = numeric(d1, "cdf")
|
|
909
|
+
b = numeric(d2, "cdf")
|
|
910
|
+
v = numeric(x, "cdf")
|
|
911
|
+
Num.new(Special.beta_i(a * v / (a * v + b), a / 2.0, b / 2.0))
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
# The mean exists for d2 > 2 and is infinite below; the variance needs
|
|
915
|
+
# d2 > 4, is infinite for 2 < d2 <= 4 and undefined below (P-7).
|
|
916
|
+
def mean = numeric_d2 && numeric_d2 <= 2 ? OO : (d2 / (d2 - 2)).simplify
|
|
917
|
+
def variance
|
|
918
|
+
return (numeric_d2 > 2 ? OO : UNDEFINED) if numeric_d2 && numeric_d2 <= 4
|
|
919
|
+
(2 * d2**2 * (d1 + d2 - 2) / (d1 * (d2 - 2)**2 * (d2 - 4))).simplify
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def numeric_d2 = d2.is_a?(Num) && d2.value.real? ? d2.value : nil
|
|
923
|
+
def numeric_bracket = [0.0, 10.0]
|
|
924
|
+
|
|
925
|
+
# I_x(a/2, b/2) has its upper tail as I_(1-x)(b/2, a/2), without 1 - it.
|
|
926
|
+
def survival(c)
|
|
927
|
+
return super unless Expression.lift(c).variables.empty?
|
|
928
|
+
a = numeric(d1, "cdf")
|
|
929
|
+
b = numeric(d2, "cdf")
|
|
930
|
+
v = numeric(c, "cdf")
|
|
931
|
+
return Num.new(1.0) unless v.positive?
|
|
932
|
+
Num.new(Special.beta_i(b / (a * v + b), b / 2.0, a / 2.0))
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
def draw(values, random)
|
|
936
|
+
a, b = values
|
|
937
|
+
(2.0 * Distributions.gamma_variate(a / 2.0, random) / a) / (2.0 * Distributions.gamma_variate(b / 2.0, random) / b)
|
|
938
|
+
end
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
class DiscreteUniform < Discrete
|
|
942
|
+
def validate
|
|
943
|
+
[a, b].each do |e|
|
|
944
|
+
next unless e.variables.empty?
|
|
945
|
+
whole_value = e.is_a?(Num) && e.value.is_a?(Float) ? e.value == e.value.round : !Infer.excluded?(e, ZZ)
|
|
946
|
+
raise ArgumentError, "DiscreteUniform: the ends must be whole numbers, got #{e}" unless whole_value
|
|
947
|
+
end
|
|
948
|
+
return unless a.variables.empty? && b.variables.empty?
|
|
949
|
+
raise ArgumentError, "DiscreteUniform: the range is empty (#{a} to #{b})" if Decide.sign((b - a).simplify) == :negative
|
|
950
|
+
end
|
|
951
|
+
def a = params[0]
|
|
952
|
+
def b = params[1]
|
|
953
|
+
def support = [a, b]
|
|
954
|
+
def count = (b - a + 1).simplify
|
|
955
|
+
|
|
956
|
+
# With symbolic ends the cdf is the count of the integers from a to k
|
|
957
|
+
# over the count of all of them, and the case split says where
|
|
958
|
+
# (sum from a symbolic a crashed: P-15).
|
|
959
|
+
def cdf(k)
|
|
960
|
+
return super if a.is_a?(Num) && b.is_a?(Num)
|
|
961
|
+
k = Expression.lift(k)
|
|
962
|
+
top = k.is_a?(Num) ? Num.new(k.value.floor) : Fn.new(:floor, [k])
|
|
963
|
+
RCAS.piecewise(k < a => 0, k < b => ((top - a + 1) / count).simplify, :else => 1)
|
|
964
|
+
end
|
|
965
|
+
|
|
966
|
+
def mass(k)
|
|
967
|
+
k = Expression.lift(k)
|
|
968
|
+
if k.is_a?(Num) && a.is_a?(Num) && b.is_a?(Num)
|
|
969
|
+
return Num.new(0) unless k.value.integer? && k.value.between?(a.value, b.value)
|
|
970
|
+
end
|
|
971
|
+
(1 / count).simplify
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def mean = ((a + b) / 2).simplify
|
|
975
|
+
def variance = ((count**2 - 1) / 12).simplify
|
|
976
|
+
def skewness = Num.new(0)
|
|
977
|
+
def kurtosis = (Num.new(Rational(3, 5)) * (3 - 4 / (count**2 - 1))).simplify
|
|
978
|
+
end
|
|
979
|
+
end
|
|
980
|
+
end
|