crdistr 0.4.1__tar.gz
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.
- crdistr-0.4.1/LICENSE +25 -0
- crdistr-0.4.1/PKG-INFO +44 -0
- crdistr-0.4.1/README.md +32 -0
- crdistr-0.4.1/pyproject.toml +21 -0
- crdistr-0.4.1/setup.cfg +4 -0
- crdistr-0.4.1/setup.py +32 -0
- crdistr-0.4.1/src/crdistr/__init__.py +207 -0
- crdistr-0.4.1/src/crdistr.egg-info/PKG-INFO +44 -0
- crdistr-0.4.1/src/crdistr.egg-info/SOURCES.txt +11 -0
- crdistr-0.4.1/src/crdistr.egg-info/dependency_links.txt +1 -0
- crdistr-0.4.1/src/crdistr.egg-info/requires.txt +1 -0
- crdistr-0.4.1/src/crdistr.egg-info/top_level.txt +1 -0
- crdistr-0.4.1/tests/test_crdistr.py +183 -0
crdistr-0.4.1/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 anun333
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
The CORE-MATH source files included here (the .c and .h files carrying their
|
|
24
|
+
own copyright notices, and the tables copied into crmvec-*-tab.h) are
|
|
25
|
+
distributed under their own MIT license and copyright notices.
|
crdistr-0.4.1/PKG-INFO
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crdistr
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: erfinv, erfcinv and probit (the inverse normal distribution) over NumPy arrays, correctly rounded in binary32, binary16 and bfloat16, on crnumerics' libcrdist
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Source, https://github.com/anun333/crnumerics/tree/main/dist
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# crdistr: crdist for Python
|
|
14
|
+
|
|
15
|
+
The normal distribution over NumPy arrays, from crnumerics' C library `libcrdist`: `erfinv`, `erfcinv`, `probit`
|
|
16
|
+
(the standard normal quantile: R's `qnorm`, SciPy's `ndtri`), `ndtr`, `log_ndtr`, `normpdf` and `logit`, correctly
|
|
17
|
+
rounded to nearest in binary32, binary16 and bfloat16, every input of each checked against MPFR on x86-64 and
|
|
18
|
+
aarch64; and samplers on a Philox4x32-10 stream. The results are the same bits on every machine, in any
|
|
19
|
+
floating-point environment.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import numpy as np, crdistr
|
|
23
|
+
crdistr.probit(np.float32(0.975)) # 1.959964, the float32 nearest the exact quantile
|
|
24
|
+
u = np.random.default_rng(1).random(10, dtype=np.float32)
|
|
25
|
+
z = crdistr.probit(u) # normal draws: the same everywhere for the same u
|
|
26
|
+
crdistr.erfinv(np.array([0.1, 0.5], np.float16)) # binary16 in, binary16 out, rounded once
|
|
27
|
+
crdistr.log_ndtr(np.float32(-40)) # log Phi(-40) = -804.6084, where Phi underflows binary64
|
|
28
|
+
s = crdistr.Stream(seed=42) # Philox4x32-10, (seed, substream, index)
|
|
29
|
+
g = s.gamma(1000, shape=2.5) # the same bits on every machine
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Also `ndtr` (Phi), `normpdf` and `logit`, correctly rounded like the inverses; and `Stream`'s `uniform`,
|
|
33
|
+
`normal`, `exponential`, `gamma`, `beta`, `chisquared`, `student_t` and `words`, whose bits are fixed by the
|
|
34
|
+
specification in `crdist.h` (the tests check the binding against its recorded fingerprints).
|
|
35
|
+
|
|
36
|
+
The format follows the input. float16 arrays use binary16, and ml_dtypes' `bfloat16` arrays use bfloat16; for
|
|
37
|
+
bfloat16 bit patterns given as `uint16`, use `erfinv_bf16`, `erfcinv_bf16` and `probit_bf16`. float32 uses
|
|
38
|
+
binary32. Anything else is converted to float32 first, rounded to nearest, so a float64 0.1 becomes float32's 0.1
|
|
39
|
+
before the function sees it.
|
|
40
|
+
|
|
41
|
+
The library is `libcrdist.so.0`, from crnumerics' `make install` (or `CRDIST_LIBRARY` naming it). The tests
|
|
42
|
+
(`make crdist-python-check` at crnumerics' root) compare results with mpmath, rounded once to each format.
|
|
43
|
+
Linux only for now. `pip install crdistr`, then `import crdistr`: the name is crdistr on PyPI and as a module,
|
|
44
|
+
"crdist" on PyPI being an unrelated project (the C library is still crdist: `libcrdist`, `crdist.h`).
|
crdistr-0.4.1/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# crdistr: crdist for Python
|
|
2
|
+
|
|
3
|
+
The normal distribution over NumPy arrays, from crnumerics' C library `libcrdist`: `erfinv`, `erfcinv`, `probit`
|
|
4
|
+
(the standard normal quantile: R's `qnorm`, SciPy's `ndtri`), `ndtr`, `log_ndtr`, `normpdf` and `logit`, correctly
|
|
5
|
+
rounded to nearest in binary32, binary16 and bfloat16, every input of each checked against MPFR on x86-64 and
|
|
6
|
+
aarch64; and samplers on a Philox4x32-10 stream. The results are the same bits on every machine, in any
|
|
7
|
+
floating-point environment.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import numpy as np, crdistr
|
|
11
|
+
crdistr.probit(np.float32(0.975)) # 1.959964, the float32 nearest the exact quantile
|
|
12
|
+
u = np.random.default_rng(1).random(10, dtype=np.float32)
|
|
13
|
+
z = crdistr.probit(u) # normal draws: the same everywhere for the same u
|
|
14
|
+
crdistr.erfinv(np.array([0.1, 0.5], np.float16)) # binary16 in, binary16 out, rounded once
|
|
15
|
+
crdistr.log_ndtr(np.float32(-40)) # log Phi(-40) = -804.6084, where Phi underflows binary64
|
|
16
|
+
s = crdistr.Stream(seed=42) # Philox4x32-10, (seed, substream, index)
|
|
17
|
+
g = s.gamma(1000, shape=2.5) # the same bits on every machine
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Also `ndtr` (Phi), `normpdf` and `logit`, correctly rounded like the inverses; and `Stream`'s `uniform`,
|
|
21
|
+
`normal`, `exponential`, `gamma`, `beta`, `chisquared`, `student_t` and `words`, whose bits are fixed by the
|
|
22
|
+
specification in `crdist.h` (the tests check the binding against its recorded fingerprints).
|
|
23
|
+
|
|
24
|
+
The format follows the input. float16 arrays use binary16, and ml_dtypes' `bfloat16` arrays use bfloat16; for
|
|
25
|
+
bfloat16 bit patterns given as `uint16`, use `erfinv_bf16`, `erfcinv_bf16` and `probit_bf16`. float32 uses
|
|
26
|
+
binary32. Anything else is converted to float32 first, rounded to nearest, so a float64 0.1 becomes float32's 0.1
|
|
27
|
+
before the function sees it.
|
|
28
|
+
|
|
29
|
+
The library is `libcrdist.so.0`, from crnumerics' `make install` (or `CRDIST_LIBRARY` naming it). The tests
|
|
30
|
+
(`make crdist-python-check` at crnumerics' root) compare results with mpmath, rounded once to each format.
|
|
31
|
+
Linux only for now. `pip install crdistr`, then `import crdistr`: the name is crdistr on PyPI and as a module,
|
|
32
|
+
"crdist" on PyPI being an unrelated project (the C library is still crdist: `libcrdist`, `crdist.h`).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "crdistr" # on PyPI and as the module (import crdistr); "crdist" there is an unrelated project
|
|
7
|
+
version = "0.4.1"
|
|
8
|
+
description = "erfinv, erfcinv and probit (the inverse normal distribution) over NumPy arrays, correctly rounded in binary32, binary16 and bfloat16, on crnumerics' libcrdist"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = ["numpy"]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Source = "https://github.com/anun333/crnumerics/tree/main/dist"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["src"]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.package-data]
|
|
21
|
+
crdistr = ["libcrdist.so.0", "LICENSE", "LICENSE-CORE-MATH"] # present only in a wheel from build-wheel.sh
|
crdistr-0.4.1/setup.cfg
ADDED
crdistr-0.4.1/setup.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Only for a wheel that carries libcrdist.so.0 (build-wheel.sh): it is for one platform, and the package is Python
|
|
2
|
+
and ctypes, so any Python 3 can use it. The distribution reports binaries, which files the package as platlib,
|
|
3
|
+
and the tag says py3-none-<platform>."""
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from setuptools import setup
|
|
7
|
+
from setuptools.dist import Distribution
|
|
8
|
+
|
|
9
|
+
here = os.path.join(os.path.dirname(os.path.abspath(__file__)), "src", "crdistr")
|
|
10
|
+
bundled = any(os.path.exists(os.path.join(here, n)) for n in ("libcrdist.so.0",))
|
|
11
|
+
kw = {}
|
|
12
|
+
if bundled:
|
|
13
|
+
try:
|
|
14
|
+
from setuptools.command.bdist_wheel import bdist_wheel
|
|
15
|
+
except ImportError: # setuptools before 70.1
|
|
16
|
+
from wheel.bdist_wheel import bdist_wheel
|
|
17
|
+
|
|
18
|
+
class BinaryDistribution(Distribution):
|
|
19
|
+
def has_ext_modules(self):
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
class PlatformWheel(bdist_wheel):
|
|
23
|
+
def get_tag(self):
|
|
24
|
+
plat = super().get_tag()[2]
|
|
25
|
+
if plat.startswith("macosx_"): # the library's minimum, not the build machine's macOS
|
|
26
|
+
target = os.environ.get("MACOSX_DEPLOYMENT_TARGET", "11.0").replace(".", "_")
|
|
27
|
+
plat = "macosx_" + target + "_" + plat.split("_", 3)[3]
|
|
28
|
+
return "py3", "none", plat
|
|
29
|
+
|
|
30
|
+
kw = {"distclass": BinaryDistribution, "cmdclass": {"bdist_wheel": PlatformWheel}}
|
|
31
|
+
|
|
32
|
+
setup(**kw)
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""crdistr, crdist for Python: erfinv, erfcinv and probit (the standard normal quantile) over NumPy arrays, correctly rounded
|
|
2
|
+
to nearest in binary32, binary16 and bfloat16, from crnumerics' C library libcrdist. Every input of every function
|
|
3
|
+
in each format was checked against MPFR; the results are the same bits on every machine, in any floating-point
|
|
4
|
+
environment.
|
|
5
|
+
|
|
6
|
+
import numpy as np, crdistr
|
|
7
|
+
crdistr.probit(np.float32(0.975)) # 1.959964 (R's qnorm, SciPy's ndtri), the float32 nearest
|
|
8
|
+
crdistr.erfinv(np.array([0.1, 0.5], np.float32)) # elementwise
|
|
9
|
+
crdistr.probit(np.float16(0.9)) # binary16 in, binary16 out, rounded once
|
|
10
|
+
u = np.random.default_rng(1).random(10, dtype=np.float32)
|
|
11
|
+
z = crdistr.probit(u) # normal draws, the same everywhere for the same u
|
|
12
|
+
crdistr.log_ndtr(np.float32(-40)) # log Phi(-40) = -804.6084, where Phi underflows
|
|
13
|
+
s = crdistr.Stream(seed=42) # Philox4x32-10: the same draws on every machine
|
|
14
|
+
g = s.gamma(1000, shape=2.5)
|
|
15
|
+
|
|
16
|
+
The format follows the input: float16 arrays use binary16, ml_dtypes' bfloat16 arrays use bfloat16 (or pass bfloat16
|
|
17
|
+
bit patterns as uint16 to erfinv_bf16, erfcinv_bf16 and probit_bf16), float32 uses binary32, and anything else is
|
|
18
|
+
converted to float32 first (rounded to nearest), so a float64 0.1 becomes float32's 0.1 before the function sees it.
|
|
19
|
+
|
|
20
|
+
The library is libcrdist.so.0: found by the environment variable CRDIST_LIBRARY, then a copy inside this package
|
|
21
|
+
(a wheel's), then the system's linker paths (crnumerics' `make install`), then pkg-config's libdir for crdist.
|
|
22
|
+
"""
|
|
23
|
+
import ctypes
|
|
24
|
+
import ctypes.util
|
|
25
|
+
import os
|
|
26
|
+
import subprocess
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
__all__ = ["erfinv", "erfcinv", "probit", "ndtr", "log_ndtr", "normpdf", "logit", "erfinv_bf16", "erfcinv_bf16",
|
|
31
|
+
"probit_bf16", "Stream", "version", "library_path"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _find():
|
|
35
|
+
env = os.environ.get("CRDIST_LIBRARY")
|
|
36
|
+
if env:
|
|
37
|
+
return env
|
|
38
|
+
here = os.path.join(os.path.dirname(os.path.abspath(__file__)), "libcrdist.so.0") # a wheel's own copy
|
|
39
|
+
if os.path.exists(here):
|
|
40
|
+
return here
|
|
41
|
+
for name in ("libcrdist.so.0", "libcrdist.so"):
|
|
42
|
+
try:
|
|
43
|
+
ctypes.CDLL(name)
|
|
44
|
+
return name
|
|
45
|
+
except OSError:
|
|
46
|
+
pass
|
|
47
|
+
found = ctypes.util.find_library("crdist")
|
|
48
|
+
if found:
|
|
49
|
+
return found
|
|
50
|
+
try:
|
|
51
|
+
libdir = subprocess.run(["pkg-config", "--variable=libdir", "crdist"], capture_output=True, text=True,
|
|
52
|
+
check=True).stdout.strip()
|
|
53
|
+
for name in ("libcrdist.so.0", "libcrdist.so"):
|
|
54
|
+
path = os.path.join(libdir, name)
|
|
55
|
+
if os.path.exists(path):
|
|
56
|
+
return path
|
|
57
|
+
except (OSError, subprocess.CalledProcessError):
|
|
58
|
+
pass
|
|
59
|
+
raise OSError("crdistr: libcrdist.so not found; install crnumerics (make install) or set CRDIST_LIBRARY")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
library_path = _find()
|
|
63
|
+
_lib = ctypes.CDLL(library_path)
|
|
64
|
+
_lib.crdist_version.restype = ctypes.c_char_p
|
|
65
|
+
_lib.crdist_version.argtypes = []
|
|
66
|
+
_SIG = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
|
|
67
|
+
for _f in ("erfinv", "erfcinv", "probit", "ndtr", "log_ndtr", "normpdf", "logit"):
|
|
68
|
+
for _name in ("crdist_%sf" % _f, "crdist_%s_f16" % _f, "crdist_%s_bf16" % _f):
|
|
69
|
+
getattr(_lib, _name).restype = None
|
|
70
|
+
getattr(_lib, _name).argtypes = _SIG
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def version():
|
|
74
|
+
"""The C library's version (CRDIST_VERSION of the crdist.h it was built from)."""
|
|
75
|
+
return _lib.crdist_version().decode()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _call(name, x, dtype):
|
|
79
|
+
a = np.require(x, dtype=dtype, requirements="C") # keeps a 0-d array 0-d
|
|
80
|
+
y = np.empty_like(a)
|
|
81
|
+
getattr(_lib, name)(ctypes.c_void_p(y.ctypes.data), ctypes.c_void_p(a.ctypes.data), a.size)
|
|
82
|
+
return y if y.ndim else y[()]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _dispatch(f, x):
|
|
86
|
+
dt = getattr(x, "dtype", None)
|
|
87
|
+
if dt == np.float16:
|
|
88
|
+
return _call("crdist_%s_f16" % f, x, np.float16)
|
|
89
|
+
if dt is not None and dt.name == "bfloat16": # ml_dtypes' type: two bytes, bfloat16's bits
|
|
90
|
+
a = np.require(x, requirements="C")
|
|
91
|
+
return _call("crdist_%s_bf16" % f, a.view(np.uint16), np.uint16).view(dt)
|
|
92
|
+
return _call("crdist_%sf" % f, x, np.float32)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def erfinv(x):
|
|
96
|
+
"""The y with erf(y) = x, correctly rounded (-1 <= x <= 1; erfinv(+-1) = +-inf, NaN outside)."""
|
|
97
|
+
return _dispatch("erfinv", x)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def erfcinv(x):
|
|
101
|
+
"""The y with erfc(y) = x, correctly rounded (0 <= x <= 2; erfcinv(0) = inf, erfcinv(2) = -inf, NaN outside)."""
|
|
102
|
+
return _dispatch("erfcinv", x)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def probit(p):
|
|
106
|
+
"""The y with Phi(y) = p, Phi the standard normal distribution function, correctly rounded (0 <= p <= 1;
|
|
107
|
+
probit(0) = -inf, probit(1) = inf, NaN outside): R's qnorm, SciPy's ndtri."""
|
|
108
|
+
return _dispatch("probit", p)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def ndtr(x):
|
|
112
|
+
"""Phi(x), the standard normal distribution function, correctly rounded (SciPy's ndtr, R's pnorm); the
|
|
113
|
+
survival function is ndtr(-x)."""
|
|
114
|
+
return _dispatch("ndtr", x)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def log_ndtr(x):
|
|
118
|
+
"""log Phi(x), correctly rounded, also where Phi underflows (SciPy's log_ndtr)."""
|
|
119
|
+
return _dispatch("log_ndtr", x)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def normpdf(x):
|
|
123
|
+
"""The standard normal density e^(-x^2/2)/sqrt(2 pi), correctly rounded."""
|
|
124
|
+
return _dispatch("normpdf", x)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def logit(p):
|
|
128
|
+
"""log(p/(1 - p)), correctly rounded (0 <= p <= 1; logit(0) = -inf, logit(1) = inf, NaN outside)."""
|
|
129
|
+
return _dispatch("logit", p)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def erfinv_bf16(bits):
|
|
133
|
+
"""erfinv on bfloat16 numbers given as their bit patterns (uint16), returning bit patterns."""
|
|
134
|
+
return _call("crdist_erfinv_bf16", bits, np.uint16)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def erfcinv_bf16(bits):
|
|
138
|
+
"""erfcinv on bfloat16 bit patterns (uint16)."""
|
|
139
|
+
return _call("crdist_erfcinv_bf16", bits, np.uint16)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def probit_bf16(bits):
|
|
143
|
+
"""probit on bfloat16 bit patterns (uint16)."""
|
|
144
|
+
return _call("crdist_probit_bf16", bits, np.uint16)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class _CStream(ctypes.Structure):
|
|
148
|
+
_fields_ = [("seed", ctypes.c_uint64), ("substream", ctypes.c_uint64), ("index", ctypes.c_uint64)]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
for _name in ("crdist_uniformf", "crdist_normalf", "crdist_exponentialf"):
|
|
152
|
+
getattr(_lib, _name).argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(_CStream)]
|
|
153
|
+
_lib.crdist_words.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(_CStream)]
|
|
154
|
+
for _name in ("crdist_gammaf", "crdist_chisquaredf", "crdist_student_tf"):
|
|
155
|
+
getattr(_lib, _name).argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_double, ctypes.POINTER(_CStream)]
|
|
156
|
+
_lib.crdist_betaf.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_double, ctypes.c_double, ctypes.POINTER(_CStream)]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Stream:
|
|
160
|
+
"""A Philox4x32-10 stream (seed, substream), read from index on: the same draws on every machine (crdist.h's
|
|
161
|
+
specification; each sampler's bits are fixed by it). Each call takes the words it needs and moves index past
|
|
162
|
+
them; streams with different substreams never overlap.
|
|
163
|
+
|
|
164
|
+
s = crdistr.Stream(seed=42)
|
|
165
|
+
z = s.normal(1000) # float32
|
|
166
|
+
g = s.gamma(1000, shape=2.5)
|
|
167
|
+
crdistr.Stream(42).normal(1000) # the same z again
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(self, seed, substream=0, index=0):
|
|
171
|
+
self._s = _CStream(seed, substream, index)
|
|
172
|
+
|
|
173
|
+
seed = property(lambda self: self._s.seed)
|
|
174
|
+
substream = property(lambda self: self._s.substream)
|
|
175
|
+
index = property(lambda self: self._s.index)
|
|
176
|
+
|
|
177
|
+
def _run(self, name, n, *args, dtype=np.float32):
|
|
178
|
+
y = np.empty(int(n), dtype)
|
|
179
|
+
getattr(_lib, name)(ctypes.c_void_p(y.ctypes.data), y.size, *args, ctypes.byref(self._s))
|
|
180
|
+
return y
|
|
181
|
+
|
|
182
|
+
def words(self, n):
|
|
183
|
+
"""the stream's next n 32-bit words"""
|
|
184
|
+
return self._run("crdist_words", n, dtype=np.uint32)
|
|
185
|
+
|
|
186
|
+
def uniform(self, n):
|
|
187
|
+
"""(2k + 1) 2^-24 for k the word's top 23 bits: 2^23 values in (0, 1), symmetric about 1/2"""
|
|
188
|
+
return self._run("crdist_uniformf", n)
|
|
189
|
+
|
|
190
|
+
def normal(self, n):
|
|
191
|
+
"""standard normal: probit of a binary32 in (0, 1/2], the sign from the word's top bit"""
|
|
192
|
+
return self._run("crdist_normalf", n)
|
|
193
|
+
|
|
194
|
+
def exponential(self, n):
|
|
195
|
+
return self._run("crdist_exponentialf", n)
|
|
196
|
+
|
|
197
|
+
def gamma(self, n, shape):
|
|
198
|
+
return self._run("crdist_gammaf", n, float(shape))
|
|
199
|
+
|
|
200
|
+
def beta(self, n, a, b):
|
|
201
|
+
return self._run("crdist_betaf", n, float(a), float(b))
|
|
202
|
+
|
|
203
|
+
def chisquared(self, n, k):
|
|
204
|
+
return self._run("crdist_chisquaredf", n, float(k))
|
|
205
|
+
|
|
206
|
+
def student_t(self, n, nu):
|
|
207
|
+
return self._run("crdist_student_tf", n, float(nu))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crdistr
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: erfinv, erfcinv and probit (the inverse normal distribution) over NumPy arrays, correctly rounded in binary32, binary16 and bfloat16, on crnumerics' libcrdist
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Source, https://github.com/anun333/crnumerics/tree/main/dist
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
# crdistr: crdist for Python
|
|
14
|
+
|
|
15
|
+
The normal distribution over NumPy arrays, from crnumerics' C library `libcrdist`: `erfinv`, `erfcinv`, `probit`
|
|
16
|
+
(the standard normal quantile: R's `qnorm`, SciPy's `ndtri`), `ndtr`, `log_ndtr`, `normpdf` and `logit`, correctly
|
|
17
|
+
rounded to nearest in binary32, binary16 and bfloat16, every input of each checked against MPFR on x86-64 and
|
|
18
|
+
aarch64; and samplers on a Philox4x32-10 stream. The results are the same bits on every machine, in any
|
|
19
|
+
floating-point environment.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import numpy as np, crdistr
|
|
23
|
+
crdistr.probit(np.float32(0.975)) # 1.959964, the float32 nearest the exact quantile
|
|
24
|
+
u = np.random.default_rng(1).random(10, dtype=np.float32)
|
|
25
|
+
z = crdistr.probit(u) # normal draws: the same everywhere for the same u
|
|
26
|
+
crdistr.erfinv(np.array([0.1, 0.5], np.float16)) # binary16 in, binary16 out, rounded once
|
|
27
|
+
crdistr.log_ndtr(np.float32(-40)) # log Phi(-40) = -804.6084, where Phi underflows binary64
|
|
28
|
+
s = crdistr.Stream(seed=42) # Philox4x32-10, (seed, substream, index)
|
|
29
|
+
g = s.gamma(1000, shape=2.5) # the same bits on every machine
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Also `ndtr` (Phi), `normpdf` and `logit`, correctly rounded like the inverses; and `Stream`'s `uniform`,
|
|
33
|
+
`normal`, `exponential`, `gamma`, `beta`, `chisquared`, `student_t` and `words`, whose bits are fixed by the
|
|
34
|
+
specification in `crdist.h` (the tests check the binding against its recorded fingerprints).
|
|
35
|
+
|
|
36
|
+
The format follows the input. float16 arrays use binary16, and ml_dtypes' `bfloat16` arrays use bfloat16; for
|
|
37
|
+
bfloat16 bit patterns given as `uint16`, use `erfinv_bf16`, `erfcinv_bf16` and `probit_bf16`. float32 uses
|
|
38
|
+
binary32. Anything else is converted to float32 first, rounded to nearest, so a float64 0.1 becomes float32's 0.1
|
|
39
|
+
before the function sees it.
|
|
40
|
+
|
|
41
|
+
The library is `libcrdist.so.0`, from crnumerics' `make install` (or `CRDIST_LIBRARY` naming it). The tests
|
|
42
|
+
(`make crdist-python-check` at crnumerics' root) compare results with mpmath, rounded once to each format.
|
|
43
|
+
Linux only for now. `pip install crdistr`, then `import crdistr`: the name is crdistr on PyPI and as a module,
|
|
44
|
+
"crdist" on PyPI being an unrelated project (the C library is still crdist: `libcrdist`, `crdist.h`).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
src/crdistr/__init__.py
|
|
6
|
+
src/crdistr.egg-info/PKG-INFO
|
|
7
|
+
src/crdistr.egg-info/SOURCES.txt
|
|
8
|
+
src/crdistr.egg-info/dependency_links.txt
|
|
9
|
+
src/crdistr.egg-info/requires.txt
|
|
10
|
+
src/crdistr.egg-info/top_level.txt
|
|
11
|
+
tests/test_crdistr.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
crdistr
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Tests of crdistr, crdist's Python binding: results correctly rounded against mpmath (300 bits, then rounded once to the
|
|
2
|
+
format), the format following the input's dtype, shapes, scalars and non-contiguous arrays, the special values,
|
|
3
|
+
and the floating-point state left as it was. crdist-check proves the C library on every input; these test that
|
|
4
|
+
the binding hands the right inputs to the right functions and the results back. Run with libcrdist on the
|
|
5
|
+
linker's path or CRDIST_LIBRARY naming it: python3 -m pytest dist/python/tests"""
|
|
6
|
+
import random
|
|
7
|
+
|
|
8
|
+
import mpmath
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
import crdistr
|
|
13
|
+
|
|
14
|
+
mpmath.mp.prec = 300
|
|
15
|
+
rng = random.Random(20261013)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def exact(f, x):
|
|
19
|
+
x = mpmath.mpf(float(x))
|
|
20
|
+
if f == "erfinv":
|
|
21
|
+
return mpmath.erfinv(x)
|
|
22
|
+
if f == "erfcinv":
|
|
23
|
+
return mpmath.erfinv(1 - x)
|
|
24
|
+
return mpmath.sqrt(2) * mpmath.erfinv(2 * x - 1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def rounded(v, bits):
|
|
28
|
+
"""v rounded once to `bits` significant bits, to nearest (normal numbers only)"""
|
|
29
|
+
with mpmath.workprec(bits):
|
|
30
|
+
return float(+v)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def draw(f, dtype, n, lo_exp):
|
|
34
|
+
"""n numbers of the dtype inside f's domain, away from its ends, with |y| in the format's normal range"""
|
|
35
|
+
out = []
|
|
36
|
+
top = 1.0 if f != "erfcinv" else 2.0
|
|
37
|
+
while len(out) < n:
|
|
38
|
+
x = dtype(rng.uniform(0, top) if rng.random() < 0.5 else 2.0 ** rng.uniform(lo_exp, -1))
|
|
39
|
+
if 0 < x < top and x != 1 and not (f == "probit" and x == 0.5) and abs(exact(f, x)) > 2.0 ** -13:
|
|
40
|
+
out.append(x)
|
|
41
|
+
return np.array(out, dtype)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize("f", ["erfinv", "erfcinv", "probit"])
|
|
45
|
+
def test_float32_correctly_rounded(f):
|
|
46
|
+
x = draw(f, np.float32, 400, -100)
|
|
47
|
+
y = getattr(crdistr, f)(x)
|
|
48
|
+
assert y.dtype == np.float32
|
|
49
|
+
for a, b in zip(x, y):
|
|
50
|
+
assert float(b) == rounded(exact(f, a), 24), (f, float(a))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.parametrize("f", ["erfinv", "erfcinv", "probit"])
|
|
54
|
+
def test_float16_correctly_rounded(f):
|
|
55
|
+
x = draw(f, np.float16, 300, -13)
|
|
56
|
+
y = getattr(crdistr, f)(x)
|
|
57
|
+
assert y.dtype == np.float16
|
|
58
|
+
for a, b in zip(x, y):
|
|
59
|
+
assert float(b) == rounded(exact(f, a), 11), (f, float(a))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_bfloat16_bits():
|
|
63
|
+
# 0.5 is 0x3f00; erfinv(0.5) = 0.4769..., bfloat16's nearest 0.4765625 = 0x3ef4
|
|
64
|
+
assert int(crdistr.erfinv_bf16(np.uint16(0x3f00))) == 0x3ef4
|
|
65
|
+
bits = np.array([0x3f00, 0x3f40, 0x0001], np.uint16) # 0.5, 0.75, the smallest subnormal
|
|
66
|
+
y = crdistr.probit_bf16(bits)
|
|
67
|
+
assert y.dtype == np.uint16 and y.shape == (3,)
|
|
68
|
+
f32 = (y.astype(np.uint32) << 16).view(np.float32)
|
|
69
|
+
assert f32[0] == 0 and 0.67 < f32[1] < 0.68 and f32[2] < -13
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_bfloat16_dtype():
|
|
73
|
+
ml = pytest.importorskip("ml_dtypes")
|
|
74
|
+
x = np.array([0.5, 0.25], ml.bfloat16)
|
|
75
|
+
y = crdistr.erfinv(x)
|
|
76
|
+
assert y.dtype == ml.bfloat16
|
|
77
|
+
assert int(y.view(np.uint16)[0]) == 0x3ef4
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_special_values():
|
|
81
|
+
inf, nan = np.inf, np.nan
|
|
82
|
+
e = crdistr.erfinv(np.array([-1, 1, 0, -0.0, 2, nan], np.float32))
|
|
83
|
+
assert e[0] == -inf and e[1] == inf and e[2] == 0 and np.signbit(e[3]) and np.isnan(e[4]) and np.isnan(e[5])
|
|
84
|
+
c = crdistr.erfcinv(np.array([0, 2, 1, -1, 3], np.float32))
|
|
85
|
+
assert c[0] == inf and c[1] == -inf and c[2] == 0 and not np.signbit(c[2]) and np.isnan(c[3]) and np.isnan(c[4])
|
|
86
|
+
p = crdistr.probit(np.array([0, 1, 0.5, -0.5, 1.5], np.float32))
|
|
87
|
+
assert p[0] == -inf and p[1] == inf and p[2] == 0 and np.isnan(p[3]) and np.isnan(p[4])
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_shapes_scalars_and_conversion():
|
|
91
|
+
x = np.linspace(0.05, 0.95, 12, dtype=np.float32).reshape(3, 4)
|
|
92
|
+
y = crdistr.probit(x)
|
|
93
|
+
assert y.shape == (3, 4) and y.dtype == np.float32
|
|
94
|
+
assert np.array_equal(crdistr.probit(x.T), y.T) # non-contiguous: elementwise the same
|
|
95
|
+
assert np.array_equal(crdistr.probit(x[:, ::2]), y[:, ::2])
|
|
96
|
+
s = crdistr.probit(np.float32(0.975))
|
|
97
|
+
assert np.ndim(s) == 0 and float(s) == rounded(exact("probit", np.float32(0.975)), 24)
|
|
98
|
+
# a float64 or a list becomes float32 first (rounded to nearest), and the result is float32
|
|
99
|
+
assert crdistr.probit([0.975]).dtype == np.float32
|
|
100
|
+
assert crdistr.probit(np.float64(0.975)) == s
|
|
101
|
+
assert crdistr.erfinv(0.5) == crdistr.erfinv(np.float32(0.5))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_environment_left_alone():
|
|
105
|
+
# the library sets round-to-nearest for itself and restores the caller's state; NumPy's error state is Python's
|
|
106
|
+
before = np.geterr()
|
|
107
|
+
crdistr.erfinv(np.array([0.5, 2.0], np.float32))
|
|
108
|
+
assert np.geterr() == before
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_version():
|
|
112
|
+
assert crdistr.version().count(".") == 2
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def exact_fn(f, x):
|
|
116
|
+
x = mpmath.mpf(float(x))
|
|
117
|
+
if f == "ndtr":
|
|
118
|
+
return mpmath.ncdf(x)
|
|
119
|
+
if f == "log_ndtr":
|
|
120
|
+
return mpmath.log(mpmath.ncdf(x))
|
|
121
|
+
if f == "normpdf":
|
|
122
|
+
return mpmath.npdf(x)
|
|
123
|
+
return mpmath.log(x / (1 - x))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.mark.parametrize("f", ["ndtr", "log_ndtr", "normpdf", "logit"])
|
|
127
|
+
def test_functions_of_x_correctly_rounded(f):
|
|
128
|
+
out = []
|
|
129
|
+
while len(out) < 300:
|
|
130
|
+
if f == "logit":
|
|
131
|
+
x = np.float32(rng.uniform(0, 1) if rng.random() < 0.5 else 2.0 ** rng.uniform(-60, -1))
|
|
132
|
+
ok = 0 < x < 1 and x != 0.5
|
|
133
|
+
else:
|
|
134
|
+
x = np.float32(rng.uniform(-14, 6) if rng.random() < 0.7 else -(10 ** rng.uniform(1, 30)))
|
|
135
|
+
ok = abs(float(exact_fn(f, x))) > 2.0 ** -120 if f != "log_ndtr" else x < 5
|
|
136
|
+
if ok:
|
|
137
|
+
out.append(x)
|
|
138
|
+
x = np.array(out, np.float32)
|
|
139
|
+
y = getattr(crdistr, f)(x)
|
|
140
|
+
assert y.dtype == np.float32
|
|
141
|
+
for a, b in zip(x, y):
|
|
142
|
+
v = exact_fn(f, a)
|
|
143
|
+
if abs(v) > 3e38:
|
|
144
|
+
assert np.isinf(b), (f, float(a))
|
|
145
|
+
else:
|
|
146
|
+
assert float(b) == rounded(v, 24), (f, float(a))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_log_ndtr_far():
|
|
150
|
+
assert float(crdistr.log_ndtr(np.float32(-40))) == rounded(mpmath.log(mpmath.ncdf(-40)), 24)
|
|
151
|
+
assert crdistr.log_ndtr(np.float32(-1e20)) == -np.inf # -5e39, past binary32
|
|
152
|
+
assert crdistr.ndtr(np.float32(-40)) == 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def fnv(y):
|
|
156
|
+
h = 0xcbf29ce484222325
|
|
157
|
+
for u in np.asarray(y, np.float32).view(np.uint32).tolist():
|
|
158
|
+
for b in range(4):
|
|
159
|
+
h ^= (u >> (8 * b)) & 0xff
|
|
160
|
+
h = (h * 0x100000001b3) % 2 ** 64
|
|
161
|
+
return h
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_stream_fingerprints():
|
|
165
|
+
# crdist-check's recorded fingerprints (seed 1, substream 0, index 0, 4096 draws): the binding gives the C bits
|
|
166
|
+
assert fnv(crdistr.Stream(1).normal(4096)) == 0xe0b080312afb39d4
|
|
167
|
+
assert fnv(crdistr.Stream(1).gamma(4096, 2.5)) == 0xc6a5cb829535e89e
|
|
168
|
+
assert fnv(crdistr.Stream(1).beta(4096, 0.01, 0.02)) == 0xa972487992e10f5b
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_stream_index_and_substreams():
|
|
172
|
+
s = crdistr.Stream(7, substream=3)
|
|
173
|
+
a = s.words(10)
|
|
174
|
+
assert s.index == 10 and a.dtype == np.uint32
|
|
175
|
+
assert np.array_equal(crdistr.Stream(7, 3, 4).words(6), a[4:]) # cut by index
|
|
176
|
+
assert not np.array_equal(crdistr.Stream(7, 4).words(10), a) # another substream
|
|
177
|
+
t = crdistr.Stream(9)
|
|
178
|
+
t.normal(5)
|
|
179
|
+
assert t.index == 5 # a normal takes one word
|
|
180
|
+
g = crdistr.Stream(9).gamma(3, -1.0)
|
|
181
|
+
assert np.isnan(g).all()
|
|
182
|
+
# Random123's known answer for Philox4x32-10 at counter 0, key 0
|
|
183
|
+
assert crdistr.Stream(0).words(4).tolist() == [0x6627e8d5, 0xe169c58d, 0xbc57ac4c, 0x9b00dbd8]
|