pynumerics 2.3.0__py3-none-any.whl

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 (46) hide show
  1. pynumerics/__init__.py +73 -0
  2. pynumerics/_bin/Microsoft.CodeAnalysis.CSharp.dll +0 -0
  3. pynumerics/_bin/Microsoft.CodeAnalysis.LICENSE.txt +27 -0
  4. pynumerics/_bin/Microsoft.CodeAnalysis.dll +0 -0
  5. pynumerics/_bin/Numerics.dll +0 -0
  6. pynumerics/_bin/pynumerics.runtimeconfig.json +15 -0
  7. pynumerics/_coverage.py +3825 -0
  8. pynumerics/_util.py +237 -0
  9. pynumerics/_version.py +40 -0
  10. pynumerics/bootstrap.py +593 -0
  11. pynumerics/callbacks.py +791 -0
  12. pynumerics/copulas.py +910 -0
  13. pynumerics/data.py +2233 -0
  14. pynumerics/differentiation.py +348 -0
  15. pynumerics/distributions.py +4043 -0
  16. pynumerics/download.py +623 -0
  17. pynumerics/enums.py +119 -0
  18. pynumerics/errors.py +245 -0
  19. pynumerics/functions.py +1142 -0
  20. pynumerics/goodness_of_fit.py +699 -0
  21. pynumerics/graphs.py +556 -0
  22. pynumerics/hypothesis_tests.py +445 -0
  23. pynumerics/identity.py +144 -0
  24. pynumerics/integration.py +677 -0
  25. pynumerics/linalg.py +1408 -0
  26. pynumerics/machine_learning.py +1335 -0
  27. pynumerics/marshal.py +733 -0
  28. pynumerics/mathematics.py +1053 -0
  29. pynumerics/mcmc.py +1406 -0
  30. pynumerics/multivariate.py +1539 -0
  31. pynumerics/ode.py +156 -0
  32. pynumerics/optimization.py +1736 -0
  33. pynumerics/probability.py +966 -0
  34. pynumerics/py.typed +0 -0
  35. pynumerics/regional.py +522 -0
  36. pynumerics/roots.py +274 -0
  37. pynumerics/runtime.py +384 -0
  38. pynumerics/sampling.py +1758 -0
  39. pynumerics/special.py +1079 -0
  40. pynumerics/statistics.py +1958 -0
  41. pynumerics/timeseries.py +1326 -0
  42. pynumerics-2.3.0.dist-info/METADATA +142 -0
  43. pynumerics-2.3.0.dist-info/RECORD +46 -0
  44. pynumerics-2.3.0.dist-info/WHEEL +5 -0
  45. pynumerics-2.3.0.dist-info/licenses/LICENSE +10 -0
  46. pynumerics-2.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1539 @@
1
+ """Multivariate distributions from ``Numerics.Distributions``.
2
+
3
+ Every class wraps one .NET distribution object (``.net``) and derives from
4
+ :class:`MultivariateDistribution`, which carries the members every Numerics multivariate
5
+ distribution declares (``PDF``, ``CDF``, ``CCDF`` and their logarithms, ``Dimension``,
6
+ ``Clone``). Numerics evaluates those members at one point (``double[]``); a two-dimensional
7
+ array is evaluated row by row with the bound member, one crossing per row. Samples cross back
8
+ as one ``double[,]`` block copy.
9
+
10
+ Random generators: members that take a ``System.Random`` accept a
11
+ :class:`pynumerics.sampling.MersenneTwister` or any .NET ``System.Random``; the ``seed`` argument
12
+ of ``generate_random_values`` (and of ``MultivariateNormal.latin_hypercube_random_values``)
13
+ accepts an integer seed or such a generator.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+
20
+ import numpy as np
21
+
22
+ from pynumerics._util import _nd, cached_overload, namespace, net_enum, overload, unwrap
23
+ from pynumerics.enums import NetEnum, Transform
24
+ from pynumerics.errors import translate_public
25
+ from pynumerics.marshal import (
26
+ _flat_doubles,
27
+ to_net_doubles,
28
+ to_net_ints,
29
+ to_net_matrix,
30
+ to_numpy_doubles,
31
+ to_numpy_ints,
32
+ to_numpy_matrix,
33
+ )
34
+ from pynumerics.sampling import _bins
35
+
36
+
37
+ class MultivariateDistributionType(NetEnum):
38
+ """Multivariate distribution types (``Numerics.Distributions.MultivariateDistributionType``)."""
39
+
40
+ BIVARIATE_EMPIRICAL_DISTRIBUTION = "BivariateEmpiricalDistribution"
41
+ MULTIVARIATE_NORMAL = "MultivariateNormal"
42
+ DIRICHLET = "Dirichlet"
43
+ MULTINOMIAL = "Multinomial"
44
+ MULTIVARIATE_STUDENT_T = "MultivariateStudentT"
45
+
46
+
47
+ def _categorical_indices() -> Any:
48
+ """Return ``Multinomial.CategoricalIndices(double[], double[], bool)``.
49
+
50
+ The two-argument overload calls this one with ``inclusive`` false, so binding the
51
+ three-argument overload once serves both forms without a per-call overload resolution.
52
+ """
53
+ return overload(_nd().Multinomial.CategoricalIndices, "Double[]", "Double[]", "Boolean")
54
+
55
+
56
+ def _generator(random: Any) -> Any:
57
+ """Return the .NET ``System.Random`` behind a :class:`pynumerics.sampling.MersenneTwister` or a .NET generator."""
58
+ return getattr(random, "net", random)
59
+
60
+
61
+ def _wrap_generator(net: Any) -> Any:
62
+ """Wrap a .NET generator as :class:`pynumerics.sampling.MersenneTwister` when it is one, else return it."""
63
+ from pynumerics.sampling import MersenneTwister # noqa: PLC0415 - imported on use
64
+
65
+ concrete = unwrap(net)
66
+ if str(concrete.GetType().FullName) != "Numerics.Sampling.MersenneTwister":
67
+ return concrete
68
+ instance = MersenneTwister.__new__(MersenneTwister)
69
+ instance.net = concrete
70
+ return instance
71
+
72
+
73
+ def _seeded_generator(value: Any) -> Any:
74
+ """Return the .NET generator for a generator argument, or a Numerics ``MersenneTwister`` for an integer seed."""
75
+ if isinstance(value, (int, np.integer)):
76
+ return namespace("Numerics.Sampling").MersenneTwister(int(value))
77
+ return _generator(value)
78
+
79
+
80
+ def _draw(member: Any, sample_size: int, seed: Any) -> np.ndarray:
81
+ """Call a ``(int, int seed)`` or ``(int, Random)`` sampling overload and return the ``double[,]`` as NumPy.
82
+
83
+ An integer ``seed`` calls the seed overload; any other value is taken as a generator for the
84
+ ``System.Random`` overload.
85
+ """
86
+ import System # noqa: PLC0415 - the runtime is running (the owner exists)
87
+
88
+ if isinstance(seed, (int, np.integer)):
89
+ return to_numpy_matrix(member.Overloads[System.Int32, System.Int32](int(sample_size), int(seed)))
90
+ return to_numpy_matrix(member.Overloads[System.Int32, System.Random](int(sample_size), _generator(seed)))
91
+
92
+
93
+ def _message(error: Any) -> str | None:
94
+ """Return the message of an exception Numerics built without throwing, or None."""
95
+ return None if error is None else str(error.Message)
96
+
97
+
98
+ class MultivariateDistribution:
99
+ """A wrapped ``Numerics.Distributions.MultivariateDistribution``.
100
+
101
+ Parameters
102
+ ----------
103
+ net : Numerics.Distributions.MultivariateDistribution
104
+ The .NET distribution; interface and base-class wrappers are unwrapped.
105
+
106
+ Attributes
107
+ ----------
108
+ net : Numerics.Distributions.MultivariateDistribution
109
+ The wrapped .NET object.
110
+
111
+ Notes
112
+ -----
113
+ The point members accept one point of length ``dimension`` and return a float, or a
114
+ two-dimensional array of points (one per row) and return one value per row.
115
+ """
116
+
117
+ def __init__(self, net: Any) -> None:
118
+ self.net = unwrap(net)
119
+ self._bind()
120
+
121
+ def _bind(self) -> None:
122
+ """Bind members once after construction; subclasses override it."""
123
+
124
+ @classmethod
125
+ def from_net(cls, net: Any) -> MultivariateDistribution:
126
+ """Wrap an existing .NET distribution without copying it.
127
+
128
+ Parameters
129
+ ----------
130
+ net : Numerics.Distributions.MultivariateDistribution
131
+ The .NET object; interface and base-class wrappers are unwrapped.
132
+
133
+ Returns
134
+ -------
135
+ MultivariateDistribution
136
+ A wrapper of class ``cls`` sharing ``net``.
137
+ """
138
+ instance = cls.__new__(cls)
139
+ MultivariateDistribution.__init__(instance, net)
140
+ return instance
141
+
142
+ def _point(self, name: str, x: Any) -> Any:
143
+ """Evaluate a ``(double[])`` member at one point, or row by row for a 2-D array."""
144
+ cache = self.__dict__.setdefault("_points", {})
145
+ member = cache.get(name)
146
+ if member is None:
147
+ import System # noqa: PLC0415 - the runtime is running (self.net exists)
148
+
149
+ group = getattr(self.net, name)
150
+ try:
151
+ member = group.Overloads[System.Array[System.Double]]
152
+ except (KeyError, TypeError):
153
+ member = group
154
+ cache[name] = member
155
+ array = np.asarray(x, dtype=np.float64)
156
+ if array.ndim <= 1:
157
+ return float(member(to_net_doubles(array)))
158
+ if array.ndim != 2:
159
+ raise ValueError(f"Expected one point or a 2-D array of points; got shape {array.shape}.")
160
+ return np.array([float(member(to_net_doubles(row))) for row in array], dtype=np.float64)
161
+
162
+ @property
163
+ def dimension(self) -> int:
164
+ """The number of variables."""
165
+ return int(self.net.Dimension)
166
+
167
+ @property
168
+ def type(self) -> MultivariateDistributionType:
169
+ """The distribution type (``Type``)."""
170
+ return MultivariateDistributionType(str(self.net.Type))
171
+
172
+ @property
173
+ def display_name(self) -> str:
174
+ """The Numerics display name."""
175
+ return str(self.net.DisplayName)
176
+
177
+ @property
178
+ def short_display_name(self) -> str:
179
+ """The Numerics short display name."""
180
+ return str(self.net.ShortDisplayName)
181
+
182
+ @property
183
+ def parameters_valid(self) -> bool:
184
+ """Whether the current parameters are valid (``ParametersValid``)."""
185
+ return bool(self.net.ParametersValid)
186
+
187
+ def pdf(self, x: Any) -> Any:
188
+ """Return the density (``PDF(double[])``).
189
+
190
+ Parameters
191
+ ----------
192
+ x : array_like
193
+ One point of length d, or an array of shape (n, d).
194
+
195
+ Returns
196
+ -------
197
+ float or numpy.ndarray
198
+ The density at the point, or one per row.
199
+ """
200
+ return self._point("PDF", x)
201
+
202
+ def log_pdf(self, x: Any) -> Any:
203
+ """Return the log-density (``LogPDF(double[])``).
204
+
205
+ Parameters
206
+ ----------
207
+ x : array_like
208
+ One point of length d, or an array of shape (n, d).
209
+
210
+ Returns
211
+ -------
212
+ float or numpy.ndarray
213
+ The log-density at the point, or one per row.
214
+ """
215
+ return self._point("LogPDF", x)
216
+
217
+ def cdf(self, x: Any) -> Any:
218
+ """Return the joint CDF P(X ≤ x) (``CDF(double[])``).
219
+
220
+ Parameters
221
+ ----------
222
+ x : array_like
223
+ One point of length d, or an array of shape (n, d).
224
+
225
+ Returns
226
+ -------
227
+ float or numpy.ndarray
228
+ The joint non-exceedance probability at the point, or one per row.
229
+ """
230
+ return self._point("CDF", x)
231
+
232
+ def log_cdf(self, x: Any) -> Any:
233
+ """Return the log of the joint CDF (``LogCDF(double[])``).
234
+
235
+ Parameters
236
+ ----------
237
+ x : array_like
238
+ One point of length d, or an array of shape (n, d).
239
+
240
+ Returns
241
+ -------
242
+ float or numpy.ndarray
243
+ The log joint non-exceedance probability.
244
+ """
245
+ return self._point("LogCDF", x)
246
+
247
+ def ccdf(self, x: Any) -> Any:
248
+ """Return the complement of the joint CDF, 1 − P(X ≤ x) (``CCDF(double[])``).
249
+
250
+ Parameters
251
+ ----------
252
+ x : array_like
253
+ One point of length d, or an array of shape (n, d).
254
+
255
+ Returns
256
+ -------
257
+ float or numpy.ndarray
258
+ One minus the joint non-exceedance probability.
259
+ """
260
+ return self._point("CCDF", x)
261
+
262
+ def log_ccdf(self, x: Any) -> Any:
263
+ """Return the log of the complement of the joint CDF (``LogCCDF(double[])``).
264
+
265
+ Parameters
266
+ ----------
267
+ x : array_like
268
+ One point of length d, or an array of shape (n, d).
269
+
270
+ Returns
271
+ -------
272
+ float or numpy.ndarray
273
+ The log of one minus the joint non-exceedance probability.
274
+ """
275
+ return self._point("LogCCDF", x)
276
+
277
+ def clone(self) -> MultivariateDistribution:
278
+ """Return a deep copy through ``Clone``.
279
+
280
+ Returns
281
+ -------
282
+ MultivariateDistribution
283
+ A wrapper of the cloned .NET object, of the matching Python class.
284
+ """
285
+ return wrap(self.net.Clone())
286
+
287
+
288
+ class MultivariateNormal(MultivariateDistribution):
289
+ """Multivariate normal distribution (``Numerics.Distributions.MultivariateNormal``).
290
+
291
+ Parameters
292
+ ----------
293
+ mean : array_like or int
294
+ Mean vector μ of length d, or the dimension d for a zero mean.
295
+ covariance : array_like, optional
296
+ Covariance matrix Σ of shape (d, d); default the identity.
297
+ decomposition : str, optional
298
+ Factorization of Σ (``DecompositionMethod`` member name, for example ``"Cholesky"``);
299
+ default the Numerics default.
300
+
301
+ Attributes
302
+ ----------
303
+ net : Numerics.Distributions.MultivariateNormal
304
+ The wrapped .NET object.
305
+ """
306
+
307
+ def __init__(self, mean: Any, covariance: Any = None, decomposition: str | None = None) -> None:
308
+ nd = _nd()
309
+ is_dimension = isinstance(mean, (int, np.integer))
310
+ args: list[Any] = [int(mean) if is_dimension else to_net_doubles(mean)]
311
+ if covariance is not None:
312
+ if is_dimension:
313
+ raise ValueError("A covariance matrix needs a mean vector, not a dimension.")
314
+ args.append(to_net_matrix(covariance))
315
+ if decomposition is not None:
316
+ linear_algebra = namespace("Numerics.Mathematics.LinearAlgebra")
317
+ args.append(net_enum(linear_algebra.DecompositionMethod, decomposition))
318
+ MultivariateDistribution.__init__(self, nd.MultivariateNormal(*args))
319
+
320
+ @classmethod
321
+ def univariate(cls, mu: float, sigma: float) -> MultivariateNormal:
322
+ """Build the one-dimensional normal N(μ, σ²) (``MultivariateNormal.Univariate``).
323
+
324
+ Parameters
325
+ ----------
326
+ mu : float
327
+ Mean.
328
+ sigma : float
329
+ Standard deviation.
330
+
331
+ Returns
332
+ -------
333
+ MultivariateNormal
334
+ The distribution.
335
+ """
336
+ return cls.from_net(_nd().MultivariateNormal.Univariate(float(mu), float(sigma)))
337
+
338
+ @classmethod
339
+ def bivariate(cls, mu1: float, mu2: float, sigma1: float, sigma2: float, rho: float) -> MultivariateNormal:
340
+ """Build a bivariate normal from means, standard deviations and correlation (``Bivariate``).
341
+
342
+ Parameters
343
+ ----------
344
+ mu1 : float
345
+ Mean of the first variable.
346
+ mu2 : float
347
+ Mean of the second variable.
348
+ sigma1 : float
349
+ Standard deviation of the first variable.
350
+ sigma2 : float
351
+ Standard deviation of the second variable.
352
+ rho : float
353
+ Correlation.
354
+
355
+ Returns
356
+ -------
357
+ MultivariateNormal
358
+ The distribution.
359
+ """
360
+ return cls.from_net(_nd().MultivariateNormal.Bivariate(float(mu1), float(mu2), float(sigma1), float(sigma2),
361
+ float(rho)))
362
+
363
+ @staticmethod
364
+ def default_mvnuni_seed() -> int:
365
+ """Return the seed of the default CDF generator (``DefaultMVNUNISeed``).
366
+
367
+ Returns
368
+ -------
369
+ int
370
+ The seed every new instance's ``mvnuni`` starts from.
371
+ """
372
+ return int(_nd().MultivariateNormal.DefaultMVNUNISeed)
373
+
374
+ @property
375
+ def mvnuni(self) -> Any:
376
+ """The generator of the randomized lattice rule behind the CDF above two dimensions (``MVNUNI``).
377
+
378
+ Reads as a :class:`pynumerics.sampling.MersenneTwister` when it is one. Accepts a generator or
379
+ an integer seed (a new Numerics ``MersenneTwister``).
380
+ """
381
+ return _wrap_generator(self.net.MVNUNI)
382
+
383
+ @mvnuni.setter
384
+ def mvnuni(self, value: Any) -> None:
385
+ self.net.MVNUNI = _seeded_generator(value)
386
+
387
+ @property
388
+ def max_evaluations(self) -> int:
389
+ """The maximum number of integrand evaluations of the CDF above two dimensions; settable."""
390
+ return int(self.net.MaxEvaluations)
391
+
392
+ @max_evaluations.setter
393
+ def max_evaluations(self, value: int) -> None:
394
+ self.net.MaxEvaluations = int(value)
395
+
396
+ @property
397
+ def absolute_error(self) -> float:
398
+ """The absolute error tolerance of the CDF above two dimensions; settable."""
399
+ return float(self.net.AbsoluteError)
400
+
401
+ @absolute_error.setter
402
+ def absolute_error(self, value: float) -> None:
403
+ self.net.AbsoluteError = float(value)
404
+
405
+ @property
406
+ def relative_error(self) -> float:
407
+ """The relative error tolerance of the CDF above two dimensions; settable."""
408
+ return float(self.net.RelativeError)
409
+
410
+ @relative_error.setter
411
+ def relative_error(self, value: float) -> None:
412
+ self.net.RelativeError = float(value)
413
+
414
+ @property
415
+ def mean(self) -> np.ndarray:
416
+ """The mean vector."""
417
+ return to_numpy_doubles(self.net.Mean)
418
+
419
+ @property
420
+ def median(self) -> np.ndarray:
421
+ """The median vector (equal to the mean)."""
422
+ return to_numpy_doubles(self.net.Median)
423
+
424
+ @property
425
+ def mode(self) -> np.ndarray:
426
+ """The mode vector (equal to the mean)."""
427
+ return to_numpy_doubles(self.net.Mode)
428
+
429
+ @property
430
+ def variance(self) -> np.ndarray:
431
+ """The marginal variances (the diagonal of Σ)."""
432
+ return to_numpy_doubles(self.net.Variance)
433
+
434
+ @property
435
+ def standard_deviation(self) -> np.ndarray:
436
+ """The marginal standard deviations."""
437
+ return to_numpy_doubles(self.net.StandardDeviation)
438
+
439
+ @property
440
+ def covariance(self) -> np.ndarray:
441
+ """The covariance matrix."""
442
+ return to_numpy_matrix(self.net.Covariance)
443
+
444
+ @property
445
+ def decomposition(self) -> str:
446
+ """The factorization of Σ in use (``DecompositionMethod`` member name)."""
447
+ return str(self.net.Decomposition)
448
+
449
+ @property
450
+ def is_positive_definite(self) -> bool:
451
+ """Whether Σ is positive definite."""
452
+ return bool(self.net.IsPositiveDefinite)
453
+
454
+ @property
455
+ def is_density_valid(self) -> bool:
456
+ """Whether the density is defined (Σ is non-singular)."""
457
+ return bool(self.net.IsDensityValid)
458
+
459
+ def set_parameters(self, mean: Any, covariance: Any) -> None:
460
+ """Replace the mean and covariance (``SetParameters``).
461
+
462
+ Parameters
463
+ ----------
464
+ mean : array_like
465
+ Mean vector of length d.
466
+ covariance : array_like
467
+ Covariance matrix of shape (d, d).
468
+ """
469
+ self.net.SetParameters(to_net_doubles(mean), to_net_matrix(covariance))
470
+
471
+ def set_mean(self, mean: Any) -> None:
472
+ """Replace the mean and keep the covariance and its factorization (``SetMean``).
473
+
474
+ Parameters
475
+ ----------
476
+ mean : array_like
477
+ Mean vector of length d.
478
+ """
479
+ self.net.SetMean(to_net_doubles(mean))
480
+
481
+ def validate_parameters(self, mean: Any, covariance: Any) -> str | None:
482
+ """Return why a mean and covariance are invalid, or None when they are valid.
483
+
484
+ Parameters
485
+ ----------
486
+ mean : array_like
487
+ Mean vector.
488
+ covariance : array_like
489
+ Covariance matrix.
490
+
491
+ Returns
492
+ -------
493
+ str or None
494
+ The Numerics message of ``ValidateParameters(mean, covariance, false)``, or None.
495
+ """
496
+ return _message(self.net.ValidateParameters(to_net_doubles(mean), to_net_matrix(covariance), False))
497
+
498
+ def try_set_parameters(self, mean: Any, covariance: Any) -> bool:
499
+ """Set the parameters when they are valid (``TrySetParameters``).
500
+
501
+ Parameters
502
+ ----------
503
+ mean : array_like
504
+ Mean vector.
505
+ covariance : array_like
506
+ Covariance matrix.
507
+
508
+ Returns
509
+ -------
510
+ bool
511
+ True when the parameters were set; the distribution is unchanged otherwise.
512
+ """
513
+ return bool(self.net.TrySetParameters(to_net_doubles(mean), to_net_matrix(covariance)))
514
+
515
+ def try_set_covariance(self, covariance: Any) -> bool:
516
+ """Set the covariance when it is valid, keeping the mean (``TrySetCovariance``).
517
+
518
+ Parameters
519
+ ----------
520
+ covariance : array_like
521
+ Covariance matrix of shape (d, d).
522
+
523
+ Returns
524
+ -------
525
+ bool
526
+ True when the covariance was set.
527
+ """
528
+ return bool(self.net.TrySetCovariance(to_net_matrix(covariance)))
529
+
530
+ def marginal(self, *indices: int) -> MultivariateNormal:
531
+ """Return the marginal distribution of a subset of the variables (``Marginal``).
532
+
533
+ Parameters
534
+ ----------
535
+ *indices : int
536
+ Zero-based variable indices to keep, in the order wanted.
537
+
538
+ Returns
539
+ -------
540
+ MultivariateNormal
541
+ The marginal normal.
542
+ """
543
+ return MultivariateNormal.from_net(self.net.Marginal(to_net_ints(list(indices))))
544
+
545
+ def conditional(self, observed_indices: Any, observed_values: Any) -> MultivariateNormal:
546
+ """Return the distribution of the other variables given observed values (``Conditional``).
547
+
548
+ Parameters
549
+ ----------
550
+ observed_indices : array_like of int
551
+ Zero-based indices of the observed variables.
552
+ observed_values : array_like
553
+ Their observed values.
554
+
555
+ Returns
556
+ -------
557
+ MultivariateNormal
558
+ The conditional normal of the unobserved variables, in index order.
559
+ """
560
+ return MultivariateNormal.from_net(self.net.Conditional(to_net_ints(observed_indices),
561
+ to_net_doubles(observed_values)))
562
+
563
+ def independent_normal(self, index: int) -> Any:
564
+ """Return the univariate normal marginal of one variable (``IndependentNormal``).
565
+
566
+ Parameters
567
+ ----------
568
+ index : int
569
+ Zero-based variable index.
570
+
571
+ Returns
572
+ -------
573
+ pynumerics.distributions.Normal
574
+ The marginal.
575
+ """
576
+ from pynumerics.distributions import wrap as wrap_univariate # noqa: PLC0415 - imported on use
577
+
578
+ return wrap_univariate(self.net.IndependentNormal(int(index)))
579
+
580
+ def mahalanobis(self, x: Any) -> float:
581
+ """Return the squared Mahalanobis distance (x − μ)ᵀ Σ⁻¹ (x − μ) (``Mahalanobis``).
582
+
583
+ Parameters
584
+ ----------
585
+ x : array_like
586
+ Point of length d.
587
+
588
+ Returns
589
+ -------
590
+ float
591
+ The squared distance.
592
+ """
593
+ return float(self.net.Mahalanobis(to_net_doubles(x)))
594
+
595
+ def interval(self, lower: Any, upper: Any) -> float:
596
+ """Return the probability of the box lower ≤ X ≤ upper (``Interval``).
597
+
598
+ Parameters
599
+ ----------
600
+ lower : array_like
601
+ Lower limits of length d.
602
+ upper : array_like
603
+ Upper limits of length d.
604
+
605
+ Returns
606
+ -------
607
+ float
608
+ The box probability.
609
+ """
610
+ return float(self.net.Interval(to_net_doubles(lower), to_net_doubles(upper)))
611
+
612
+ def inverse_cdf(self, probabilities: Any) -> np.ndarray:
613
+ """Map d marginal probabilities to a correlated point (``InverseCDF(double[])``).
614
+
615
+ Parameters
616
+ ----------
617
+ probabilities : array_like
618
+ Probabilities of length d; each maps to a standard normal deviate, and the
619
+ deviates are correlated as A·z + μ.
620
+
621
+ Returns
622
+ -------
623
+ numpy.ndarray
624
+ The point of length d.
625
+ """
626
+ return to_numpy_doubles(self.net.InverseCDF(to_net_doubles(probabilities)))
627
+
628
+ def mvndst(self, lower: Any, upper: Any, infin: Any, correl: Any, max_points: int, absolute_error: float,
629
+ relative_error: float) -> tuple[float, float, int]:
630
+ """Integrate a standard multivariate normal over a box with Genz's MVNDST (``MVNDST``).
631
+
632
+ Parameters
633
+ ----------
634
+ lower : array_like
635
+ Lower limits of length N.
636
+ upper : array_like
637
+ Upper limits of length N.
638
+ infin : array_like of int
639
+ Limit flags: negative for (−∞, ∞), 0 for (−∞, upper], 1 for [lower, ∞), 2 for [lower, upper].
640
+ correl : array_like
641
+ Strict lower triangle of the correlation matrix by rows, N(N − 1)/2 values.
642
+ max_points : int
643
+ Maximum number of integrand evaluations.
644
+ absolute_error : float
645
+ Absolute error tolerance.
646
+ relative_error : float
647
+ Relative error tolerance.
648
+
649
+ Returns
650
+ -------
651
+ error : float
652
+ Estimated absolute error (99% confidence).
653
+ value : float
654
+ Estimated probability.
655
+ inform : int
656
+ 0 for normal completion, 1 when ``max_points`` was reached first, 2 for N outside 1 to 500.
657
+
658
+ Notes
659
+ -----
660
+ The lattice shifts come from this instance's :attr:`mvnuni`, which the call advances.
661
+ """
662
+ low = to_net_doubles(lower)
663
+ error, value, inform = self.net.MVNDST(len(low), low, to_net_doubles(upper), to_net_ints(infin),
664
+ to_net_doubles(correl), int(max_points), float(absolute_error),
665
+ float(relative_error), 0.0, 0.0, 0)
666
+ return float(error), float(value), int(inform)
667
+
668
+ @staticmethod
669
+ def bivariate_cdf(z1: float, z2: float, r: float) -> float:
670
+ """Return the standard bivariate normal upper orthant P(X > z1, Y > z2) (``BivariateCDF``).
671
+
672
+ Parameters
673
+ ----------
674
+ z1 : float
675
+ Lower limit of the first variable.
676
+ z2 : float
677
+ Lower limit of the second variable.
678
+ r : float
679
+ Correlation.
680
+
681
+ Returns
682
+ -------
683
+ float
684
+ The probability (Drezner and Wesolowsky, as modified by Genz).
685
+
686
+ Notes
687
+ -----
688
+ Numerics follows Genz's BVN convention, an upper orthant despite the name; the joint CDF
689
+ P(X ≤ a, Y ≤ b) is ``bivariate_cdf(-a, -b, r)``, which is how Numerics calls it.
690
+ """
691
+ return float(_nd().MultivariateNormal.BivariateCDF(float(z1), float(z2), float(r)))
692
+
693
+ @staticmethod
694
+ def bivnor(ah: float, ak: float, r: float) -> float:
695
+ """Return the standard bivariate normal upper orthant P(X ≥ ah, Y ≥ ak) (``bivnor``).
696
+
697
+ Parameters
698
+ ----------
699
+ ah : float
700
+ Lower limit of the first variable.
701
+ ak : float
702
+ Lower limit of the second variable.
703
+ r : float
704
+ Correlation.
705
+
706
+ Returns
707
+ -------
708
+ float
709
+ The probability (Donnelly, ACM Algorithm 462).
710
+ """
711
+ return float(_nd().MultivariateNormal.bivnor(float(ah), float(ak), float(r)))
712
+
713
+ @staticmethod
714
+ def bvu(sh: float, sk: float, r: float) -> float:
715
+ """Return the standard bivariate normal upper orthant P(X > sh, Y > sk) (``BVU``).
716
+
717
+ Parameters
718
+ ----------
719
+ sh : float
720
+ Lower limit of the first variable.
721
+ sk : float
722
+ Lower limit of the second variable.
723
+ r : float
724
+ Correlation.
725
+
726
+ Returns
727
+ -------
728
+ float
729
+ The probability (Genz).
730
+ """
731
+ return float(_nd().MultivariateNormal.BVU(float(sh), float(sk), float(r)))
732
+
733
+ @staticmethod
734
+ def mvnphi(z: float) -> float:
735
+ """Return the standard normal CDF used by MVNDST (``MVNPHI``).
736
+
737
+ Parameters
738
+ ----------
739
+ z : float
740
+ The z-score.
741
+
742
+ Returns
743
+ -------
744
+ float
745
+ Φ(z).
746
+ """
747
+ return float(_nd().MultivariateNormal.MVNPHI(float(z)))
748
+
749
+ @staticmethod
750
+ def phinvs(p: float) -> float:
751
+ """Return the standard normal quantile used by MVNDST (``PHINVS``, algorithm AS 241).
752
+
753
+ Parameters
754
+ ----------
755
+ p : float
756
+ Lower tail probability.
757
+
758
+ Returns
759
+ -------
760
+ float
761
+ Φ⁻¹(p).
762
+ """
763
+ return float(_nd().MultivariateNormal.PHINVS(float(p)))
764
+
765
+ def generate_random_values(self, sample_size: int, seed: Any = -1) -> np.ndarray:
766
+ """Draw a Monte Carlo sample (``GenerateRandomValues``).
767
+
768
+ Parameters
769
+ ----------
770
+ sample_size : int
771
+ Number of points.
772
+ seed : int or MersenneTwister, optional
773
+ Seed (zero or negative seeds from the clock, not reproducible) or a generator.
774
+
775
+ Returns
776
+ -------
777
+ numpy.ndarray
778
+ Array of shape ``(sample_size, d)``.
779
+ """
780
+ return _draw(self.net.GenerateRandomValues, sample_size, seed)
781
+
782
+ def latin_hypercube_random_values(self, sample_size: int, seed: Any) -> np.ndarray:
783
+ """Draw a Latin hypercube sample (``LatinHypercubeRandomValues``).
784
+
785
+ Parameters
786
+ ----------
787
+ sample_size : int
788
+ Number of points.
789
+ seed : int or MersenneTwister
790
+ Seed or a generator.
791
+
792
+ Returns
793
+ -------
794
+ numpy.ndarray
795
+ Array of shape ``(sample_size, d)``.
796
+ """
797
+ return _draw(self.net.LatinHypercubeRandomValues, sample_size, seed)
798
+
799
+ def stratified_random_values(self, stratification_bins: Any, seed: int) -> np.ndarray:
800
+ """Draw a sample stratified in the first variable (``StratifiedRandomValues``).
801
+
802
+ Parameters
803
+ ----------
804
+ stratification_bins : pynumerics.sampling.StratificationBins
805
+ Probability bins; one point is drawn per bin at its midpoint.
806
+ seed : int
807
+ Seed of the other variables.
808
+
809
+ Returns
810
+ -------
811
+ numpy.ndarray
812
+ Array of shape ``(len(stratification_bins), d)``.
813
+ """
814
+ return to_numpy_matrix(self.net.StratifiedRandomValues(_bins(stratification_bins), int(seed)))
815
+
816
+
817
+ class MultivariateStudentT(MultivariateDistribution):
818
+ """Multivariate Student-t distribution (``Numerics.Distributions.MultivariateStudentT``).
819
+
820
+ Parameters
821
+ ----------
822
+ degrees_of_freedom : float
823
+ Degrees of freedom ν > 0.
824
+ location : array_like or int
825
+ Location vector μ of length d, or the dimension d for a zero location.
826
+ scale_matrix : array_like, optional
827
+ Scale matrix Σ of shape (d, d), positive definite; default the identity. The covariance
828
+ is ν/(ν − 2)·Σ for ν > 2.
829
+
830
+ Attributes
831
+ ----------
832
+ net : Numerics.Distributions.MultivariateStudentT
833
+ The wrapped .NET object.
834
+ """
835
+
836
+ def __init__(self, degrees_of_freedom: float, location: Any, scale_matrix: Any = None) -> None:
837
+ nd = _nd()
838
+ if isinstance(location, (int, np.integer)):
839
+ if scale_matrix is not None:
840
+ raise ValueError("A scale matrix needs a location vector, not a dimension.")
841
+ net = nd.MultivariateStudentT(int(location), float(degrees_of_freedom))
842
+ elif scale_matrix is None:
843
+ net = nd.MultivariateStudentT(float(degrees_of_freedom), to_net_doubles(location))
844
+ else:
845
+ net = nd.MultivariateStudentT(float(degrees_of_freedom), to_net_doubles(location),
846
+ to_net_matrix(scale_matrix))
847
+ MultivariateDistribution.__init__(self, net)
848
+
849
+ @property
850
+ def mvnuni(self) -> Any:
851
+ """The generator of the Genz-Bretz lattice rule of :meth:`cdf` above two dimensions (``MVNUNI``).
852
+
853
+ Reads as a :class:`pynumerics.sampling.MersenneTwister` when it is one. Accepts a generator or
854
+ an integer seed (a new Numerics ``MersenneTwister``).
855
+ """
856
+ return _wrap_generator(self.net.MVNUNI)
857
+
858
+ @mvnuni.setter
859
+ def mvnuni(self, value: Any) -> None:
860
+ self.net.MVNUNI = _seeded_generator(value)
861
+
862
+ @property
863
+ def max_evaluations(self) -> int:
864
+ """The maximum number of lattice-rule evaluations of the CDF above two dimensions; default 25,000."""
865
+ return int(self.net.MaxEvaluations)
866
+
867
+ @max_evaluations.setter
868
+ def max_evaluations(self, value: int) -> None:
869
+ self.net.MaxEvaluations = int(value)
870
+
871
+ @property
872
+ def absolute_error(self) -> float:
873
+ """The absolute error tolerance of the CDF above two dimensions; default 1e-5."""
874
+ return float(self.net.AbsoluteError)
875
+
876
+ @absolute_error.setter
877
+ def absolute_error(self, value: float) -> None:
878
+ self.net.AbsoluteError = float(value)
879
+
880
+ @property
881
+ def relative_error(self) -> float:
882
+ """The relative error tolerance of the CDF above two dimensions; default 0."""
883
+ return float(self.net.RelativeError)
884
+
885
+ @relative_error.setter
886
+ def relative_error(self, value: float) -> None:
887
+ self.net.RelativeError = float(value)
888
+
889
+ @property
890
+ def degrees_of_freedom(self) -> float:
891
+ """The degrees of freedom ν."""
892
+ return float(self.net.DegreesOfFreedom)
893
+
894
+ @property
895
+ def location(self) -> np.ndarray:
896
+ """The location vector μ."""
897
+ return to_numpy_doubles(self.net.Location)
898
+
899
+ @property
900
+ def mean(self) -> np.ndarray:
901
+ """The mean vector (μ for ν > 1)."""
902
+ return to_numpy_doubles(self.net.Mean)
903
+
904
+ @property
905
+ def median(self) -> np.ndarray:
906
+ """The median vector μ."""
907
+ return to_numpy_doubles(self.net.Median)
908
+
909
+ @property
910
+ def mode(self) -> np.ndarray:
911
+ """The mode vector μ."""
912
+ return to_numpy_doubles(self.net.Mode)
913
+
914
+ @property
915
+ def variance(self) -> np.ndarray:
916
+ """The marginal variances ν/(ν − 2)·Σᵢᵢ for ν > 2."""
917
+ return to_numpy_doubles(self.net.Variance)
918
+
919
+ @property
920
+ def standard_deviation(self) -> np.ndarray:
921
+ """The marginal standard deviations."""
922
+ return to_numpy_doubles(self.net.StandardDeviation)
923
+
924
+ @property
925
+ def scale_matrix(self) -> np.ndarray:
926
+ """The scale matrix Σ."""
927
+ return to_numpy_matrix(self.net.ScaleMatrix)
928
+
929
+ @property
930
+ def covariance(self) -> np.ndarray:
931
+ """The covariance matrix ν/(ν − 2)·Σ for ν > 2."""
932
+ return to_numpy_matrix(self.net.Covariance)
933
+
934
+ @property
935
+ def is_positive_definite(self) -> bool:
936
+ """Whether Σ is positive definite."""
937
+ return bool(self.net.IsPositiveDefinite)
938
+
939
+ def set_parameters(self, degrees_of_freedom: float, location: Any, scale_matrix: Any) -> None:
940
+ """Replace the parameters (``SetParameters``).
941
+
942
+ Parameters
943
+ ----------
944
+ degrees_of_freedom : float
945
+ Degrees of freedom ν.
946
+ location : array_like
947
+ Location vector μ.
948
+ scale_matrix : array_like
949
+ Scale matrix Σ.
950
+ """
951
+ self.net.SetParameters(float(degrees_of_freedom), to_net_doubles(location), to_net_matrix(scale_matrix))
952
+
953
+ def validate_parameters(self, degrees_of_freedom: float, location: Any, scale_matrix: Any) -> str | None:
954
+ """Return why a parameter set is invalid, or None when it is valid.
955
+
956
+ Parameters
957
+ ----------
958
+ degrees_of_freedom : float
959
+ Degrees of freedom ν.
960
+ location : array_like
961
+ Location vector μ.
962
+ scale_matrix : array_like
963
+ Scale matrix Σ.
964
+
965
+ Returns
966
+ -------
967
+ str or None
968
+ The Numerics message of ``ValidateParameters(..., false)``, or None.
969
+ """
970
+ return _message(self.net.ValidateParameters(float(degrees_of_freedom), to_net_doubles(location),
971
+ to_net_matrix(scale_matrix), False))
972
+
973
+ def mahalanobis(self, x: Any) -> float:
974
+ """Return the squared Mahalanobis distance (x − μ)ᵀ Σ⁻¹ (x − μ) (``Mahalanobis``).
975
+
976
+ Parameters
977
+ ----------
978
+ x : array_like
979
+ Point of length d.
980
+
981
+ Returns
982
+ -------
983
+ float
984
+ The squared distance.
985
+ """
986
+ return float(self.net.Mahalanobis(to_net_doubles(x)))
987
+
988
+ def inverse_cdf(self, probabilities: Any) -> np.ndarray:
989
+ """Map d + 1 probabilities to a point (``InverseCDF(double[])``).
990
+
991
+ Parameters
992
+ ----------
993
+ probabilities : array_like
994
+ Length d + 1: the first d map to standard normal deviates, the last to the χ²(ν) mixing
995
+ variate W, and the point is μ + L·z·√(ν/W).
996
+
997
+ Returns
998
+ -------
999
+ numpy.ndarray
1000
+ The point of length d.
1001
+ """
1002
+ return to_numpy_doubles(self.net.InverseCDF(to_net_doubles(probabilities)))
1003
+
1004
+ def generate_random_values(self, sample_size: int, seed: Any = -1) -> np.ndarray:
1005
+ """Draw a Monte Carlo sample (``GenerateRandomValues``).
1006
+
1007
+ Parameters
1008
+ ----------
1009
+ sample_size : int
1010
+ Number of points.
1011
+ seed : int or MersenneTwister, optional
1012
+ Seed (zero or negative seeds from the clock, not reproducible) or a generator.
1013
+
1014
+ Returns
1015
+ -------
1016
+ numpy.ndarray
1017
+ Array of shape ``(sample_size, d)``.
1018
+ """
1019
+ return _draw(self.net.GenerateRandomValues, sample_size, seed)
1020
+
1021
+ def latin_hypercube_random_values(self, sample_size: int, seed: int) -> np.ndarray:
1022
+ """Draw a Latin hypercube sample of the normal part (``LatinHypercubeRandomValues``).
1023
+
1024
+ Parameters
1025
+ ----------
1026
+ sample_size : int
1027
+ Number of points.
1028
+ seed : int
1029
+ Seed; the χ² mixing variates use ``seed + 1``.
1030
+
1031
+ Returns
1032
+ -------
1033
+ numpy.ndarray
1034
+ Array of shape ``(sample_size, d)``.
1035
+ """
1036
+ return to_numpy_matrix(self.net.LatinHypercubeRandomValues(int(sample_size), int(seed)))
1037
+
1038
+ def stratified_random_values(self, stratification_bins: Any, seed: int) -> np.ndarray:
1039
+ """Draw a sample stratified in the first variable (``StratifiedRandomValues``).
1040
+
1041
+ Parameters
1042
+ ----------
1043
+ stratification_bins : pynumerics.sampling.StratificationBins
1044
+ Probability bins; one point is drawn per bin at its midpoint.
1045
+ seed : int
1046
+ Seed of the other variables; the χ² mixing variates use ``seed + 1``.
1047
+
1048
+ Returns
1049
+ -------
1050
+ numpy.ndarray
1051
+ Array of shape ``(len(stratification_bins), d)``.
1052
+ """
1053
+ return to_numpy_matrix(self.net.StratifiedRandomValues(_bins(stratification_bins), int(seed)))
1054
+
1055
+
1056
+ class Multinomial(MultivariateDistribution):
1057
+ """Multinomial distribution of category counts (``Numerics.Distributions.Multinomial``).
1058
+
1059
+ Parameters
1060
+ ----------
1061
+ number_of_trials : int
1062
+ Number of trials N ≥ 1.
1063
+ probabilities : array_like
1064
+ Category probabilities p of length k ≥ 2, each in [0, 1], summing to one.
1065
+
1066
+ Attributes
1067
+ ----------
1068
+ net : Numerics.Distributions.Multinomial
1069
+ The wrapped .NET object.
1070
+
1071
+ Notes
1072
+ -----
1073
+ Points are count vectors of length k; ``pdf`` is the probability mass function.
1074
+ """
1075
+
1076
+ def __init__(self, number_of_trials: int, probabilities: Any) -> None:
1077
+ MultivariateDistribution.__init__(self, _nd().Multinomial(int(number_of_trials), to_net_doubles(probabilities)))
1078
+
1079
+ @property
1080
+ def number_of_trials(self) -> int:
1081
+ """The number of trials N."""
1082
+ return int(self.net.NumberOfTrials)
1083
+
1084
+ @property
1085
+ def probabilities(self) -> np.ndarray:
1086
+ """The category probabilities p."""
1087
+ return to_numpy_doubles(self.net.Probabilities)
1088
+
1089
+ @property
1090
+ def mean(self) -> np.ndarray:
1091
+ """The mean counts N·pᵢ."""
1092
+ return to_numpy_doubles(self.net.Mean)
1093
+
1094
+ @property
1095
+ def variance(self) -> np.ndarray:
1096
+ """The count variances N·pᵢ(1 − pᵢ)."""
1097
+ return to_numpy_doubles(self.net.Variance)
1098
+
1099
+ def covariance(self, i: int, j: int) -> float:
1100
+ """Return one covariance entry (``Covariance(int, int)``).
1101
+
1102
+ Parameters
1103
+ ----------
1104
+ i : int
1105
+ Row category index.
1106
+ j : int
1107
+ Column category index.
1108
+
1109
+ Returns
1110
+ -------
1111
+ float
1112
+ Cov(Xᵢ, Xⱼ): N·pᵢ(1 − pᵢ) on the diagonal, −N·pᵢ·pⱼ off it.
1113
+ """
1114
+ return float(self.net.Covariance(int(i), int(j)))
1115
+
1116
+ def log_pmf(self, x: Any) -> Any:
1117
+ """Return the log probability mass (``LogPMF``).
1118
+
1119
+ Parameters
1120
+ ----------
1121
+ x : array_like
1122
+ One count vector of length k, or an array of shape (n, k).
1123
+
1124
+ Returns
1125
+ -------
1126
+ float or numpy.ndarray
1127
+ The log mass; negative infinity off the support.
1128
+ """
1129
+ return self._point("LogPMF", x)
1130
+
1131
+ def generate_random_values(self, sample_size: int, seed: int = -1) -> np.ndarray:
1132
+ """Draw count vectors by sequential binomial sampling (``GenerateRandomValues``).
1133
+
1134
+ Parameters
1135
+ ----------
1136
+ sample_size : int
1137
+ Number of draws.
1138
+ seed : int, optional
1139
+ Seed; zero or negative seeds from the clock (not reproducible).
1140
+
1141
+ Returns
1142
+ -------
1143
+ numpy.ndarray
1144
+ Array of shape ``(sample_size, k)``; each row sums to N.
1145
+ """
1146
+ return to_numpy_matrix(self.net.GenerateRandomValues(int(sample_size), int(seed)))
1147
+
1148
+ @staticmethod
1149
+ def sample(weights: Any, random: Any) -> int:
1150
+ """Draw one category index with probability proportional to its weight (``Multinomial.Sample``).
1151
+
1152
+ Parameters
1153
+ ----------
1154
+ weights : array_like
1155
+ Non-negative weights, at least one positive; normalization is not required.
1156
+ random : MersenneTwister
1157
+ The generator (a :class:`pynumerics.sampling.MersenneTwister` or a .NET ``System.Random``);
1158
+ one draw is consumed.
1159
+
1160
+ Returns
1161
+ -------
1162
+ int
1163
+ The zero-based category index; a draw on a boundary selects the lower category.
1164
+ """
1165
+ return int(_nd().Multinomial.Sample(to_net_doubles(weights), _generator(random)))
1166
+
1167
+ @staticmethod
1168
+ def categorical_indices(cumulative_weights: Any, uniforms: Any, inclusive: bool = False) -> Any:
1169
+ """Map uniforms to category indices through a cumulative weight table (``CategoricalIndices``).
1170
+
1171
+ Parameters
1172
+ ----------
1173
+ cumulative_weights : array_like
1174
+ Non-decreasing finite cumulative weights c of length n ≥ 1; normalization is not required.
1175
+ uniforms : array_like
1176
+ Variates on the scale of ``cumulative_weights``, any shape, no NaN.
1177
+ inclusive : bool, optional
1178
+ False (the default) selects min{k : c[k] > u}; True selects min{k : c[k] ≥ u}, the
1179
+ inverse-transform count min{k : F(k) ≥ u} of a discrete CDF table F. Both clip to n - 1.
1180
+
1181
+ Returns
1182
+ -------
1183
+ int or numpy.ndarray
1184
+ One zero-based index per variate (int32, the shape of ``uniforms``), equal to
1185
+ ``numpy.clip(numpy.searchsorted(c, u, side="right"), 0, n - 1)``, or with ``side="left"``
1186
+ when ``inclusive`` is True.
1187
+
1188
+ Notes
1189
+ -----
1190
+ The two forms differ only for a variate equal to a table entry. The whole array crosses into
1191
+ Numerics once, and the indices cross back once.
1192
+ """
1193
+ member = cached_overload("Multinomial.CategoricalIndices(double[], double[], bool)", _categorical_indices)
1194
+ array = np.asarray(uniforms, dtype=np.float64)
1195
+ indices = to_numpy_ints(member(to_net_doubles(cumulative_weights), _flat_doubles(array), bool(inclusive)))
1196
+ return int(indices[0]) if array.ndim == 0 else indices.reshape(array.shape)
1197
+
1198
+
1199
+ class BivariateEmpirical(MultivariateDistribution):
1200
+ """Bivariate distribution interpolated from a table of joint non-exceedance probabilities.
1201
+
1202
+ Wraps ``Numerics.Distributions.BivariateEmpirical``: the CDF is bilinear interpolation of
1203
+ ``probability_values`` on the grid ``x1_values`` by ``x2_values``, in the chosen transforms.
1204
+
1205
+ Parameters
1206
+ ----------
1207
+ x1_values : array_like, optional
1208
+ Ascending grid of the first variable. Omit all three tables for an empty distribution
1209
+ to fill later with :meth:`set_parameters`.
1210
+ x2_values : array_like, optional
1211
+ Ascending grid of the second variable.
1212
+ probability_values : array_like, optional
1213
+ Joint probabilities of shape ``(len(x1_values), len(x2_values))``.
1214
+ x1_transform : Transform or str, optional
1215
+ Interpolation transform of the first variable; default none.
1216
+ x2_transform : Transform or str, optional
1217
+ Interpolation transform of the second variable; default none.
1218
+ probability_transform : Transform or str, optional
1219
+ Interpolation transform of the probabilities; default none.
1220
+
1221
+ Attributes
1222
+ ----------
1223
+ net : Numerics.Distributions.BivariateEmpirical
1224
+ The wrapped .NET object.
1225
+ """
1226
+
1227
+ def __init__(self, x1_values: Any = None, x2_values: Any = None, probability_values: Any = None,
1228
+ x1_transform: Transform | str = Transform.NONE, x2_transform: Transform | str = Transform.NONE,
1229
+ probability_transform: Transform | str = Transform.NONE) -> None:
1230
+ nd = _nd()
1231
+ kind = namespace("Numerics.Data").Transform
1232
+ transforms = (net_enum(kind, x1_transform), net_enum(kind, x2_transform), net_enum(kind, probability_transform))
1233
+ if x1_values is None and x2_values is None and probability_values is None:
1234
+ net = nd.BivariateEmpirical(*transforms)
1235
+ else:
1236
+ net = nd.BivariateEmpirical(to_net_doubles(x1_values), to_net_doubles(x2_values),
1237
+ to_net_matrix(probability_values), *transforms)
1238
+ MultivariateDistribution.__init__(self, net)
1239
+
1240
+ def _bind(self) -> None:
1241
+ """Bind the ``(double, double)`` overloads of ``PDF`` and ``CDF`` once."""
1242
+ import System # noqa: PLC0415 - the runtime is running (self.net exists)
1243
+
1244
+ self._pair = {name: getattr(self.net, name).Overloads[System.Double, System.Double] for name in ("PDF", "CDF")}
1245
+
1246
+ def _pairs(self, name: str, x1: Any, x2: Any) -> Any:
1247
+ """Evaluate a point member at a point, or at broadcast (x1, x2) pairs with the scalar overload."""
1248
+ if x2 is None:
1249
+ return self._point(name, x1)
1250
+ member = self._pair[name]
1251
+ a, b = np.broadcast_arrays(np.asarray(x1, dtype=np.float64), np.asarray(x2, dtype=np.float64))
1252
+ if a.ndim == 0:
1253
+ return float(member(float(a), float(b)))
1254
+ out = np.array([float(member(float(p), float(q))) for p, q in zip(a.ravel(), b.ravel(), strict=True)])
1255
+ return out.reshape(a.shape)
1256
+
1257
+ @property
1258
+ def x1_values(self) -> np.ndarray:
1259
+ """The grid of the first variable."""
1260
+ return to_numpy_doubles(self.net.X1Values)
1261
+
1262
+ @property
1263
+ def x2_values(self) -> np.ndarray:
1264
+ """The grid of the second variable."""
1265
+ return to_numpy_doubles(self.net.X2Values)
1266
+
1267
+ @property
1268
+ def probability_values(self) -> np.ndarray:
1269
+ """The table of joint non-exceedance probabilities."""
1270
+ return to_numpy_matrix(self.net.ProbabilityValues)
1271
+
1272
+ @property
1273
+ def x1_transform(self) -> Transform:
1274
+ """The interpolation transform of the first variable; settable."""
1275
+ return Transform(str(self.net.X1Transform))
1276
+
1277
+ @x1_transform.setter
1278
+ def x1_transform(self, value: Transform | str) -> None:
1279
+ self.net.X1Transform = net_enum(namespace("Numerics.Data").Transform, value)
1280
+
1281
+ @property
1282
+ def x2_transform(self) -> Transform:
1283
+ """The interpolation transform of the second variable; settable."""
1284
+ return Transform(str(self.net.X2Transform))
1285
+
1286
+ @x2_transform.setter
1287
+ def x2_transform(self, value: Transform | str) -> None:
1288
+ self.net.X2Transform = net_enum(namespace("Numerics.Data").Transform, value)
1289
+
1290
+ @property
1291
+ def probability_transform(self) -> Transform:
1292
+ """The interpolation transform of the probabilities; settable."""
1293
+ return Transform(str(self.net.ProbabilityTransform))
1294
+
1295
+ @probability_transform.setter
1296
+ def probability_transform(self, value: Transform | str) -> None:
1297
+ self.net.ProbabilityTransform = net_enum(namespace("Numerics.Data").Transform, value)
1298
+
1299
+ def set_parameters(self, x1_values: Any, x2_values: Any, probability_values: Any) -> None:
1300
+ """Replace the grids and the probability table (``SetParameters``).
1301
+
1302
+ Parameters
1303
+ ----------
1304
+ x1_values : array_like
1305
+ Grid of the first variable.
1306
+ x2_values : array_like
1307
+ Grid of the second variable.
1308
+ probability_values : array_like
1309
+ Joint probabilities of shape ``(len(x1_values), len(x2_values))``.
1310
+ """
1311
+ self.net.SetParameters(to_net_doubles(x1_values), to_net_doubles(x2_values), to_net_matrix(probability_values))
1312
+
1313
+ def validate_parameters(self, x1_values: Any, x2_values: Any, probability_values: Any) -> str | None:
1314
+ """Return why a grid and table are invalid, or None when they are valid.
1315
+
1316
+ Parameters
1317
+ ----------
1318
+ x1_values : array_like
1319
+ Grid of the first variable.
1320
+ x2_values : array_like
1321
+ Grid of the second variable.
1322
+ probability_values : array_like
1323
+ Joint probabilities.
1324
+
1325
+ Returns
1326
+ -------
1327
+ str or None
1328
+ The Numerics message of ``ValidateParameters(..., false)``, or None.
1329
+ """
1330
+ return _message(self.net.ValidateParameters(to_net_doubles(x1_values), to_net_doubles(x2_values),
1331
+ to_net_matrix(probability_values), False))
1332
+
1333
+ def pdf(self, x1: Any, x2: Any = None) -> Any:
1334
+ """Return the density, which Numerics does not define for the interpolated surface (NaN).
1335
+
1336
+ Parameters
1337
+ ----------
1338
+ x1 : float or array_like
1339
+ The first variable, or one point (x1, x2) when ``x2`` is omitted.
1340
+ x2 : float or array_like, optional
1341
+ The second variable, broadcast with ``x1``.
1342
+
1343
+ Returns
1344
+ -------
1345
+ float or numpy.ndarray
1346
+ NaN for every point.
1347
+ """
1348
+ return self._pairs("PDF", x1, x2)
1349
+
1350
+ def cdf(self, x1: Any, x2: Any = None) -> Any:
1351
+ """Return the interpolated joint CDF P(X1 ≤ x1, X2 ≤ x2) (``CDF(double, double)``).
1352
+
1353
+ Parameters
1354
+ ----------
1355
+ x1 : float or array_like
1356
+ The first variable, or one point (x1, x2) when ``x2`` is omitted.
1357
+ x2 : float or array_like, optional
1358
+ The second variable, broadcast with ``x1``.
1359
+
1360
+ Returns
1361
+ -------
1362
+ float or numpy.ndarray
1363
+ Joint non-exceedance probabilities.
1364
+ """
1365
+ return self._pairs("CDF", x1, x2)
1366
+
1367
+
1368
+ class Dirichlet(MultivariateDistribution):
1369
+ """Dirichlet distribution on the probability simplex (``Numerics.Distributions.Dirichlet``).
1370
+
1371
+ Parameters
1372
+ ----------
1373
+ alpha : array_like
1374
+ Concentration parameters α of length K ≥ 2, each positive and finite. Numerics refuses
1375
+ anything else with ``ArgumentOutOfRangeException`` (``ValueError`` inside
1376
+ :func:`pynumerics.errors.net_errors`).
1377
+
1378
+ Attributes
1379
+ ----------
1380
+ net : Numerics.Distributions.Dirichlet
1381
+ The wrapped .NET object.
1382
+
1383
+ Notes
1384
+ -----
1385
+ Every value comes from Numerics; nothing is computed in Python. Draws use the gamma
1386
+ representation inside ``Dirichlet.GenerateRandomValues``: K independent
1387
+ Gamma(αᵢ, 1) inverse-CDF variates from one seeded Mersenne Twister, normalized to sum
1388
+ to one. The sample crosses back as one ``double[,]`` block copy, so a draw is bit for bit
1389
+ the ``double[,]`` the .NET call returns. The density is zero (log-density negative
1390
+ infinity) off the simplex.
1391
+ """
1392
+
1393
+ def __init__(self, alpha: Any) -> None:
1394
+ MultivariateDistribution.__init__(self, _nd().Dirichlet(to_net_doubles(alpha)))
1395
+
1396
+ @classmethod
1397
+ def symmetric(cls, dimension: int, alpha: float) -> Dirichlet:
1398
+ """Build the symmetric Dirichlet with every αᵢ = ``alpha`` (``Dirichlet(int, double)``).
1399
+
1400
+ Parameters
1401
+ ----------
1402
+ dimension : int
1403
+ Number of components K ≥ 2.
1404
+ alpha : float
1405
+ Common positive concentration parameter.
1406
+
1407
+ Returns
1408
+ -------
1409
+ Dirichlet
1410
+ The wrapped distribution.
1411
+ """
1412
+ return cls.from_net(_nd().Dirichlet(int(dimension), float(alpha)))
1413
+
1414
+ def _bind(self) -> None:
1415
+ """Bind ``GenerateRandomValues(int, int)`` once, so calls skip overload resolution."""
1416
+ import System # noqa: PLC0415 - the runtime is started by the constructor
1417
+
1418
+ self._generate = self.net.GenerateRandomValues.Overloads[System.Int32, System.Int32]
1419
+
1420
+ @property
1421
+ def alpha(self) -> np.ndarray:
1422
+ """The concentration parameters α (a copy)."""
1423
+ return to_numpy_doubles(self.net.Alpha)
1424
+
1425
+ @property
1426
+ def alpha_sum(self) -> float:
1427
+ """The concentration sum α₀ = Σαᵢ."""
1428
+ return float(self.net.AlphaSum)
1429
+
1430
+ @property
1431
+ def mean(self) -> np.ndarray:
1432
+ """The mean vector αᵢ/α₀."""
1433
+ return to_numpy_doubles(self.net.Mean)
1434
+
1435
+ @property
1436
+ def variance(self) -> np.ndarray:
1437
+ """The marginal variances αᵢ(α₀ − αᵢ)/(α₀²(α₀ + 1))."""
1438
+ return to_numpy_doubles(self.net.Variance)
1439
+
1440
+ @property
1441
+ def mode(self) -> np.ndarray:
1442
+ """The mode (αᵢ − 1)/(α₀ − K); Numerics refuses it unless every αᵢ > 1."""
1443
+ return to_numpy_doubles(self.net.Mode)
1444
+
1445
+ def covariance(self, i: int, j: int) -> float:
1446
+ """Return one covariance entry (``Covariance(int, int)``).
1447
+
1448
+ Parameters
1449
+ ----------
1450
+ i : int
1451
+ Row component index.
1452
+ j : int
1453
+ Column component index.
1454
+
1455
+ Returns
1456
+ -------
1457
+ float
1458
+ Cov(Xᵢ, Xⱼ).
1459
+ """
1460
+ return float(self.net.Covariance(int(i), int(j)))
1461
+
1462
+ def covariance_matrix(self) -> np.ndarray:
1463
+ """Return the full covariance matrix (``CovarianceMatrix()``).
1464
+
1465
+ Returns
1466
+ -------
1467
+ numpy.ndarray
1468
+ Array of shape ``(K, K)``.
1469
+ """
1470
+ return to_numpy_matrix(self.net.CovarianceMatrix())
1471
+
1472
+ def generate_random_values(self, sample_size: int, seed: int = -1) -> np.ndarray:
1473
+ """Draw a Monte Carlo sample (``GenerateRandomValues(int, int)``).
1474
+
1475
+ Parameters
1476
+ ----------
1477
+ sample_size : int
1478
+ Number of points; zero returns an empty ``(0, K)`` array.
1479
+ seed : int, optional
1480
+ Mersenne Twister seed; zero or negative seeds from the clock (not reproducible).
1481
+
1482
+ Returns
1483
+ -------
1484
+ numpy.ndarray
1485
+ Array of shape ``(sample_size, K)``; each row lies on the simplex.
1486
+ """
1487
+ return to_numpy_matrix(self._generate(int(sample_size), int(seed)))
1488
+
1489
+ @staticmethod
1490
+ def log_multivariate_beta(alpha: Any) -> float:
1491
+ """Return log B(α) = Σ log Γ(αᵢ) − log Γ(Σαᵢ) (``Dirichlet.LogMultivariateBeta``).
1492
+
1493
+ Parameters
1494
+ ----------
1495
+ alpha : array_like
1496
+ Concentration parameters.
1497
+
1498
+ Returns
1499
+ -------
1500
+ float
1501
+ The log of the multivariate beta function.
1502
+ """
1503
+ return float(_nd().Dirichlet.LogMultivariateBeta(to_net_doubles(alpha)))
1504
+
1505
+
1506
+ _CLASSES: dict[str, type[MultivariateDistribution]] = {
1507
+ "MultivariateNormal": MultivariateNormal,
1508
+ "MultivariateStudentT": MultivariateStudentT,
1509
+ "Multinomial": Multinomial,
1510
+ "BivariateEmpirical": BivariateEmpirical,
1511
+ "Dirichlet": Dirichlet,
1512
+ }
1513
+
1514
+
1515
+ def wrap(net: Any) -> MultivariateDistribution:
1516
+ """Wrap a .NET multivariate distribution in the matching Python class.
1517
+
1518
+ Parameters
1519
+ ----------
1520
+ net : Numerics.Distributions.MultivariateDistribution or IMultivariateDistribution
1521
+ The .NET object.
1522
+
1523
+ Returns
1524
+ -------
1525
+ MultivariateDistribution
1526
+ The specific wrapper class when one exists, otherwise the generic wrapper; the .NET object
1527
+ is shared, not copied.
1528
+ """
1529
+ concrete = unwrap(net)
1530
+ cls = _CLASSES.get(str(concrete.GetType().Name), MultivariateDistribution)
1531
+ return cls.from_net(concrete)
1532
+
1533
+
1534
+ __all__ = [
1535
+ "BivariateEmpirical", "Dirichlet", "Multinomial", "MultivariateDistribution", "MultivariateDistributionType",
1536
+ "MultivariateNormal", "MultivariateStudentT", "wrap",
1537
+ ]
1538
+
1539
+ translate_public(globals())