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 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`).
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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
+ 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]