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