crival 0.2.0__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.
crival-0.2.0/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.
crival-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: crival
3
+ Version: 0.2.0
4
+ Summary: Tight IEEE 1788.1 interval arithmetic over NumPy arrays, on crnumerics' libival (CORE-MATH's correctly rounded functions)
5
+ License: MIT
6
+ Project-URL: Source, https://github.com/anun333/crnumerics/tree/main/ival
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: numpy
11
+ Dynamic: license-file
12
+
13
+ # ival for Python
14
+
15
+ Interval arithmetic over NumPy arrays: every result is the tightest
16
+ interval of doubles around the exact one (IEEE 1788.1). It calls
17
+ crnumerics' C library `libival`, whose functions are CORE-MATH's,
18
+ correctly rounded.
19
+
20
+ ```python
21
+ import ival, numpy as np
22
+
23
+ x = ival.Interval(1, 2)
24
+ ival.exp(x) * x + 1 # Interval(3.718281828459045, 15.7781121978613)
25
+ ival.Interval(0.1) + 0.2 # Interval(0.3, 0.30000000000000004): the exact sum inside
26
+ ival.Interval.from_text("[0.1]") # Interval(0.09999999999999999, 0.1): the decimal 0.1 inside
27
+ X = ival.Interval(np.linspace(0, 1, 10**6), np.linspace(0, 1, 10**6) + 1e-3)
28
+ ival.sin(X) # a million intervals in one call
29
+ ```
30
+
31
+ ## Installing
32
+
33
+ On Linux x86-64 and aarch64 (glibc 2.26 or later), the wheels on PyPI
34
+ carry the library, so nothing else needs installing. The package is
35
+ `crival` on PyPI; the module is `ival`:
36
+
37
+ ```sh
38
+ pip install crival
39
+ python3 -c 'import ival; print(ival.exp(ival.Interval(1)))'
40
+ ```
41
+
42
+ Elsewhere, or to use a libival you installed, install `libival` first,
43
+ from the root of crnumerics, then the package:
44
+
45
+ ```sh
46
+ make install-ival PREFIX=$HOME/.local # or /usr/local; installs libival.so.0 and ival.pc
47
+ pip install ./ival/python # numpy is the one dependency
48
+ ```
49
+
50
+ `ival/python/build-wheel.sh` builds a wheel that carries the library,
51
+ for the glibc it was built on and newer. The wheels on PyPI are built in
52
+ the manylinux_2_28 containers by `.github/workflows/wheels.yml`.
53
+
54
+ The binding finds the library through `IVAL_LIBRARY` (a path), then a
55
+ copy inside the package (a wheel's), then the system's linker paths, then `pkg-config --variable=libdir ival` (for a
56
+ prefix like `$HOME/.local`, `PKG_CONFIG_PATH` must include its
57
+ `lib/pkgconfig`).
58
+ `ival.library_path` says which it loaded, and `ival.version()` the
59
+ library's version. It works with libival 0.1.0 and later; 0.2.0 fixes sinRev, cosRev
60
+ and tanRev, which in 0.1.0 could miss a set. Python 3.9 or
61
+ later; tested on Linux x86-64 and aarch64.
62
+
63
+ ## What there is
64
+
65
+ - **`Interval(lo, hi)`**, or `Interval(a)` for the smallest interval
66
+ holding `a`. One interval holds two Python floats; an array of them two
67
+ float64 arrays, broadcast as NumPy does. The empty interval is
68
+ `[nan, nan]`; `Interval(lo, hi)` is empty unless `lo <= hi`
69
+ (1788's numsToInterval). `Interval.from_text("[0.1, 0.2]")` reads 1788's
70
+ text forms, a string or an array of strings, rounding each decimal
71
+ outward.
72
+ - **Numbers that are not doubles round outward.** A Python int beyond
73
+ 2^53, a `Fraction`, a `Decimal`, a long double, an int64 array: each
74
+ becomes the two doubles around it, never the nearest one, as an end,
75
+ as an operand (`x + Fraction(1, 3)`) or as a member to test
76
+ (`2**53 + 1 in x`). A float is taken as the double it is, so `0.1` is
77
+ already rounded: give decimals as text or `Decimal`.
78
+ - **Arithmetic:** `+ - * /`, unary `-`, `abs`, `**` (an integer power
79
+ is `pown`, tight; any other `pow`), `&` (intersection), `|` (hull).
80
+ - **40 functions:** `exp exp2 exp10 expm1 log log2 log10 log1p sqrt cbrt
81
+ rsqrt sin cos tan sinpi cospi tanpi asin acos atan asinpi acospi atanpi
82
+ sinh cosh tanh asinh acosh atanh erf erfc tgamma sqr recip sign ceil
83
+ floor trunc round roundeven`, and `atan2 hypot pow min max cancelminus
84
+ cancelplus pown rootn fma`.
85
+ - **The accurate mode:** `acc_exp`, `acc_sin`, ... (the first 32 above,
86
+ and `acc_atan2`, `acc_hypot`, `acc_pow`) are at most one ulp wider than
87
+ the tight result, and faster on arrays, when the environment variable
88
+ `IVAL_CRMVEC` names crmvec's `libmvec.so.1` (x86-64 with AVX2 and FMA).
89
+ Without it they give the tight result.
90
+ - **Reverse operations** (the tightest interval around
91
+ `{x in X : f(x) in C}`, X the whole line if omitted): `sqrrev absrev
92
+ coshrev sinrev cosrev tanrev pownrev mulrev powrev1 powrev2`.
93
+ - **Measures and tests:** `.inf .sup .mid .wid .rad .mag .mig`,
94
+ `is_empty() is_entire() is_singleton()`, `subset interior disjoint
95
+ equal`, `contains(X, v)` and `v in X`.
96
+
97
+ The rounding mode is the library's business: it sets what it needs and
98
+ restores the caller's, so nothing here changes how the rest of a program
99
+ rounds. A call releases the GIL, and the library is thread-safe.
100
+
101
+ ## Speed, and pyinterval
102
+
103
+ pyinterval (on CRlibm) was Python's correctly rounded interval library.
104
+ Its last release was in March 2017, and `pip install pyinterval` fails on
105
+ Python 3.12: its crlibm imports `distutils.command.upload`, which Python
106
+ 3.12 lacks and setuptools 75 dropped. Built with setuptools 74 and no
107
+ build isolation, it and this binding were run on the same inputs by
108
+ `bench/pyinterval_compare.py`, on an AMD Ryzen 5 PRO 5650U, 2026-10-09:
109
+
110
+ - **Bounds:** on 2,000 points each of exp, log, sin, cos, tan, atan,
111
+ sqrt, sinh, cosh, expm1 and log1p, both tight and the same, bound for
112
+ bound. pyinterval's tanh is a quotient of sinh and cosh, every bound
113
+ rounded outward: valid, but not tight on any of the 2,000 points, up to
114
+ 4 doubles too wide.
115
+ - **Time per interval,** for intervals of width 0.5 (and 1, for `b`):
116
+
117
+ | | pyinterval | ival, one at a time | ival, an array of 20,000 |
118
+ |---|---:|---:|---:|
119
+ | exp | 11.0 µs | 1.8 µs | 25 ns |
120
+ | sin | 11.7 µs | 1.8 µs | 141 ns |
121
+ | atan | 10.8 µs | 1.7 µs | 31 ns |
122
+ | `a*b + 2` | 22.2 µs | 4.5 µs | 20 ns |
123
+
124
+ One interval at a time costs about a microsecond of ctypes per call;
125
+ arrays pay it once per call.
126
+
127
+ ## Tests
128
+
129
+ `make ival-python-check` at the root of crnumerics runs `tests/` against
130
+ the library just built (it needs pytest and mpmath): bounds against
131
+ mpmath at 300 bits and exact fractions, arrays against one interval at
132
+ a time, outward rounding of numbers that are not doubles, the reverse
133
+ operations and the accurate mode.
crival-0.2.0/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # ival for Python
2
+
3
+ Interval arithmetic over NumPy arrays: every result is the tightest
4
+ interval of doubles around the exact one (IEEE 1788.1). It calls
5
+ crnumerics' C library `libival`, whose functions are CORE-MATH's,
6
+ correctly rounded.
7
+
8
+ ```python
9
+ import ival, numpy as np
10
+
11
+ x = ival.Interval(1, 2)
12
+ ival.exp(x) * x + 1 # Interval(3.718281828459045, 15.7781121978613)
13
+ ival.Interval(0.1) + 0.2 # Interval(0.3, 0.30000000000000004): the exact sum inside
14
+ ival.Interval.from_text("[0.1]") # Interval(0.09999999999999999, 0.1): the decimal 0.1 inside
15
+ X = ival.Interval(np.linspace(0, 1, 10**6), np.linspace(0, 1, 10**6) + 1e-3)
16
+ ival.sin(X) # a million intervals in one call
17
+ ```
18
+
19
+ ## Installing
20
+
21
+ On Linux x86-64 and aarch64 (glibc 2.26 or later), the wheels on PyPI
22
+ carry the library, so nothing else needs installing. The package is
23
+ `crival` on PyPI; the module is `ival`:
24
+
25
+ ```sh
26
+ pip install crival
27
+ python3 -c 'import ival; print(ival.exp(ival.Interval(1)))'
28
+ ```
29
+
30
+ Elsewhere, or to use a libival you installed, install `libival` first,
31
+ from the root of crnumerics, then the package:
32
+
33
+ ```sh
34
+ make install-ival PREFIX=$HOME/.local # or /usr/local; installs libival.so.0 and ival.pc
35
+ pip install ./ival/python # numpy is the one dependency
36
+ ```
37
+
38
+ `ival/python/build-wheel.sh` builds a wheel that carries the library,
39
+ for the glibc it was built on and newer. The wheels on PyPI are built in
40
+ the manylinux_2_28 containers by `.github/workflows/wheels.yml`.
41
+
42
+ The binding finds the library through `IVAL_LIBRARY` (a path), then a
43
+ copy inside the package (a wheel's), then the system's linker paths, then `pkg-config --variable=libdir ival` (for a
44
+ prefix like `$HOME/.local`, `PKG_CONFIG_PATH` must include its
45
+ `lib/pkgconfig`).
46
+ `ival.library_path` says which it loaded, and `ival.version()` the
47
+ library's version. It works with libival 0.1.0 and later; 0.2.0 fixes sinRev, cosRev
48
+ and tanRev, which in 0.1.0 could miss a set. Python 3.9 or
49
+ later; tested on Linux x86-64 and aarch64.
50
+
51
+ ## What there is
52
+
53
+ - **`Interval(lo, hi)`**, or `Interval(a)` for the smallest interval
54
+ holding `a`. One interval holds two Python floats; an array of them two
55
+ float64 arrays, broadcast as NumPy does. The empty interval is
56
+ `[nan, nan]`; `Interval(lo, hi)` is empty unless `lo <= hi`
57
+ (1788's numsToInterval). `Interval.from_text("[0.1, 0.2]")` reads 1788's
58
+ text forms, a string or an array of strings, rounding each decimal
59
+ outward.
60
+ - **Numbers that are not doubles round outward.** A Python int beyond
61
+ 2^53, a `Fraction`, a `Decimal`, a long double, an int64 array: each
62
+ becomes the two doubles around it, never the nearest one, as an end,
63
+ as an operand (`x + Fraction(1, 3)`) or as a member to test
64
+ (`2**53 + 1 in x`). A float is taken as the double it is, so `0.1` is
65
+ already rounded: give decimals as text or `Decimal`.
66
+ - **Arithmetic:** `+ - * /`, unary `-`, `abs`, `**` (an integer power
67
+ is `pown`, tight; any other `pow`), `&` (intersection), `|` (hull).
68
+ - **40 functions:** `exp exp2 exp10 expm1 log log2 log10 log1p sqrt cbrt
69
+ rsqrt sin cos tan sinpi cospi tanpi asin acos atan asinpi acospi atanpi
70
+ sinh cosh tanh asinh acosh atanh erf erfc tgamma sqr recip sign ceil
71
+ floor trunc round roundeven`, and `atan2 hypot pow min max cancelminus
72
+ cancelplus pown rootn fma`.
73
+ - **The accurate mode:** `acc_exp`, `acc_sin`, ... (the first 32 above,
74
+ and `acc_atan2`, `acc_hypot`, `acc_pow`) are at most one ulp wider than
75
+ the tight result, and faster on arrays, when the environment variable
76
+ `IVAL_CRMVEC` names crmvec's `libmvec.so.1` (x86-64 with AVX2 and FMA).
77
+ Without it they give the tight result.
78
+ - **Reverse operations** (the tightest interval around
79
+ `{x in X : f(x) in C}`, X the whole line if omitted): `sqrrev absrev
80
+ coshrev sinrev cosrev tanrev pownrev mulrev powrev1 powrev2`.
81
+ - **Measures and tests:** `.inf .sup .mid .wid .rad .mag .mig`,
82
+ `is_empty() is_entire() is_singleton()`, `subset interior disjoint
83
+ equal`, `contains(X, v)` and `v in X`.
84
+
85
+ The rounding mode is the library's business: it sets what it needs and
86
+ restores the caller's, so nothing here changes how the rest of a program
87
+ rounds. A call releases the GIL, and the library is thread-safe.
88
+
89
+ ## Speed, and pyinterval
90
+
91
+ pyinterval (on CRlibm) was Python's correctly rounded interval library.
92
+ Its last release was in March 2017, and `pip install pyinterval` fails on
93
+ Python 3.12: its crlibm imports `distutils.command.upload`, which Python
94
+ 3.12 lacks and setuptools 75 dropped. Built with setuptools 74 and no
95
+ build isolation, it and this binding were run on the same inputs by
96
+ `bench/pyinterval_compare.py`, on an AMD Ryzen 5 PRO 5650U, 2026-10-09:
97
+
98
+ - **Bounds:** on 2,000 points each of exp, log, sin, cos, tan, atan,
99
+ sqrt, sinh, cosh, expm1 and log1p, both tight and the same, bound for
100
+ bound. pyinterval's tanh is a quotient of sinh and cosh, every bound
101
+ rounded outward: valid, but not tight on any of the 2,000 points, up to
102
+ 4 doubles too wide.
103
+ - **Time per interval,** for intervals of width 0.5 (and 1, for `b`):
104
+
105
+ | | pyinterval | ival, one at a time | ival, an array of 20,000 |
106
+ |---|---:|---:|---:|
107
+ | exp | 11.0 µs | 1.8 µs | 25 ns |
108
+ | sin | 11.7 µs | 1.8 µs | 141 ns |
109
+ | atan | 10.8 µs | 1.7 µs | 31 ns |
110
+ | `a*b + 2` | 22.2 µs | 4.5 µs | 20 ns |
111
+
112
+ One interval at a time costs about a microsecond of ctypes per call;
113
+ arrays pay it once per call.
114
+
115
+ ## Tests
116
+
117
+ `make ival-python-check` at the root of crnumerics runs `tests/` against
118
+ the library just built (it needs pytest and mpmath): bounds against
119
+ mpmath at 300 bits and exact fractions, arrays against one interval at
120
+ a time, outward rounding of numbers that are not doubles, the reverse
121
+ operations and the accurate mode.
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "crival" # on PyPI; the module is ival (import ival)
7
+ version = "0.2.0"
8
+ description = "Tight IEEE 1788.1 interval arithmetic over NumPy arrays, on crnumerics' libival (CORE-MATH's correctly rounded functions)"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9" # math.nextafter
12
+ dependencies = ["numpy"]
13
+
14
+ [project.urls]
15
+ Source = "https://github.com/anun333/crnumerics/tree/main/ival"
16
+
17
+ [tool.setuptools.packages.find]
18
+ where = ["src"]
19
+
20
+ [tool.setuptools.package-data]
21
+ ival = ["libival.so.0", "LICENSE", "LICENSE-CORE-MATH"] # present only in a wheel from build-wheel.sh
crival-0.2.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
crival-0.2.0/setup.py ADDED
@@ -0,0 +1,27 @@
1
+ """Only for a wheel that carries libival.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
+ bundled = os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "src", "ival", "libival.so.0"))
10
+ kw = {}
11
+ if bundled:
12
+ try:
13
+ from setuptools.command.bdist_wheel import bdist_wheel
14
+ except ImportError: # setuptools before 70.1
15
+ from wheel.bdist_wheel import bdist_wheel
16
+
17
+ class BinaryDistribution(Distribution):
18
+ def has_ext_modules(self):
19
+ return True
20
+
21
+ class PlatformWheel(bdist_wheel):
22
+ def get_tag(self):
23
+ return "py3", "none", super().get_tag()[2]
24
+
25
+ kw = {"distclass": BinaryDistribution, "cmdclass": {"bdist_wheel": PlatformWheel}}
26
+
27
+ setup(**kw)
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: crival
3
+ Version: 0.2.0
4
+ Summary: Tight IEEE 1788.1 interval arithmetic over NumPy arrays, on crnumerics' libival (CORE-MATH's correctly rounded functions)
5
+ License: MIT
6
+ Project-URL: Source, https://github.com/anun333/crnumerics/tree/main/ival
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: numpy
11
+ Dynamic: license-file
12
+
13
+ # ival for Python
14
+
15
+ Interval arithmetic over NumPy arrays: every result is the tightest
16
+ interval of doubles around the exact one (IEEE 1788.1). It calls
17
+ crnumerics' C library `libival`, whose functions are CORE-MATH's,
18
+ correctly rounded.
19
+
20
+ ```python
21
+ import ival, numpy as np
22
+
23
+ x = ival.Interval(1, 2)
24
+ ival.exp(x) * x + 1 # Interval(3.718281828459045, 15.7781121978613)
25
+ ival.Interval(0.1) + 0.2 # Interval(0.3, 0.30000000000000004): the exact sum inside
26
+ ival.Interval.from_text("[0.1]") # Interval(0.09999999999999999, 0.1): the decimal 0.1 inside
27
+ X = ival.Interval(np.linspace(0, 1, 10**6), np.linspace(0, 1, 10**6) + 1e-3)
28
+ ival.sin(X) # a million intervals in one call
29
+ ```
30
+
31
+ ## Installing
32
+
33
+ On Linux x86-64 and aarch64 (glibc 2.26 or later), the wheels on PyPI
34
+ carry the library, so nothing else needs installing. The package is
35
+ `crival` on PyPI; the module is `ival`:
36
+
37
+ ```sh
38
+ pip install crival
39
+ python3 -c 'import ival; print(ival.exp(ival.Interval(1)))'
40
+ ```
41
+
42
+ Elsewhere, or to use a libival you installed, install `libival` first,
43
+ from the root of crnumerics, then the package:
44
+
45
+ ```sh
46
+ make install-ival PREFIX=$HOME/.local # or /usr/local; installs libival.so.0 and ival.pc
47
+ pip install ./ival/python # numpy is the one dependency
48
+ ```
49
+
50
+ `ival/python/build-wheel.sh` builds a wheel that carries the library,
51
+ for the glibc it was built on and newer. The wheels on PyPI are built in
52
+ the manylinux_2_28 containers by `.github/workflows/wheels.yml`.
53
+
54
+ The binding finds the library through `IVAL_LIBRARY` (a path), then a
55
+ copy inside the package (a wheel's), then the system's linker paths, then `pkg-config --variable=libdir ival` (for a
56
+ prefix like `$HOME/.local`, `PKG_CONFIG_PATH` must include its
57
+ `lib/pkgconfig`).
58
+ `ival.library_path` says which it loaded, and `ival.version()` the
59
+ library's version. It works with libival 0.1.0 and later; 0.2.0 fixes sinRev, cosRev
60
+ and tanRev, which in 0.1.0 could miss a set. Python 3.9 or
61
+ later; tested on Linux x86-64 and aarch64.
62
+
63
+ ## What there is
64
+
65
+ - **`Interval(lo, hi)`**, or `Interval(a)` for the smallest interval
66
+ holding `a`. One interval holds two Python floats; an array of them two
67
+ float64 arrays, broadcast as NumPy does. The empty interval is
68
+ `[nan, nan]`; `Interval(lo, hi)` is empty unless `lo <= hi`
69
+ (1788's numsToInterval). `Interval.from_text("[0.1, 0.2]")` reads 1788's
70
+ text forms, a string or an array of strings, rounding each decimal
71
+ outward.
72
+ - **Numbers that are not doubles round outward.** A Python int beyond
73
+ 2^53, a `Fraction`, a `Decimal`, a long double, an int64 array: each
74
+ becomes the two doubles around it, never the nearest one, as an end,
75
+ as an operand (`x + Fraction(1, 3)`) or as a member to test
76
+ (`2**53 + 1 in x`). A float is taken as the double it is, so `0.1` is
77
+ already rounded: give decimals as text or `Decimal`.
78
+ - **Arithmetic:** `+ - * /`, unary `-`, `abs`, `**` (an integer power
79
+ is `pown`, tight; any other `pow`), `&` (intersection), `|` (hull).
80
+ - **40 functions:** `exp exp2 exp10 expm1 log log2 log10 log1p sqrt cbrt
81
+ rsqrt sin cos tan sinpi cospi tanpi asin acos atan asinpi acospi atanpi
82
+ sinh cosh tanh asinh acosh atanh erf erfc tgamma sqr recip sign ceil
83
+ floor trunc round roundeven`, and `atan2 hypot pow min max cancelminus
84
+ cancelplus pown rootn fma`.
85
+ - **The accurate mode:** `acc_exp`, `acc_sin`, ... (the first 32 above,
86
+ and `acc_atan2`, `acc_hypot`, `acc_pow`) are at most one ulp wider than
87
+ the tight result, and faster on arrays, when the environment variable
88
+ `IVAL_CRMVEC` names crmvec's `libmvec.so.1` (x86-64 with AVX2 and FMA).
89
+ Without it they give the tight result.
90
+ - **Reverse operations** (the tightest interval around
91
+ `{x in X : f(x) in C}`, X the whole line if omitted): `sqrrev absrev
92
+ coshrev sinrev cosrev tanrev pownrev mulrev powrev1 powrev2`.
93
+ - **Measures and tests:** `.inf .sup .mid .wid .rad .mag .mig`,
94
+ `is_empty() is_entire() is_singleton()`, `subset interior disjoint
95
+ equal`, `contains(X, v)` and `v in X`.
96
+
97
+ The rounding mode is the library's business: it sets what it needs and
98
+ restores the caller's, so nothing here changes how the rest of a program
99
+ rounds. A call releases the GIL, and the library is thread-safe.
100
+
101
+ ## Speed, and pyinterval
102
+
103
+ pyinterval (on CRlibm) was Python's correctly rounded interval library.
104
+ Its last release was in March 2017, and `pip install pyinterval` fails on
105
+ Python 3.12: its crlibm imports `distutils.command.upload`, which Python
106
+ 3.12 lacks and setuptools 75 dropped. Built with setuptools 74 and no
107
+ build isolation, it and this binding were run on the same inputs by
108
+ `bench/pyinterval_compare.py`, on an AMD Ryzen 5 PRO 5650U, 2026-10-09:
109
+
110
+ - **Bounds:** on 2,000 points each of exp, log, sin, cos, tan, atan,
111
+ sqrt, sinh, cosh, expm1 and log1p, both tight and the same, bound for
112
+ bound. pyinterval's tanh is a quotient of sinh and cosh, every bound
113
+ rounded outward: valid, but not tight on any of the 2,000 points, up to
114
+ 4 doubles too wide.
115
+ - **Time per interval,** for intervals of width 0.5 (and 1, for `b`):
116
+
117
+ | | pyinterval | ival, one at a time | ival, an array of 20,000 |
118
+ |---|---:|---:|---:|
119
+ | exp | 11.0 µs | 1.8 µs | 25 ns |
120
+ | sin | 11.7 µs | 1.8 µs | 141 ns |
121
+ | atan | 10.8 µs | 1.7 µs | 31 ns |
122
+ | `a*b + 2` | 22.2 µs | 4.5 µs | 20 ns |
123
+
124
+ One interval at a time costs about a microsecond of ctypes per call;
125
+ arrays pay it once per call.
126
+
127
+ ## Tests
128
+
129
+ `make ival-python-check` at the root of crnumerics runs `tests/` against
130
+ the library just built (it needs pytest and mpmath): bounds against
131
+ mpmath at 300 bits and exact fractions, arrays against one interval at
132
+ a time, outward rounding of numbers that are not doubles, the reverse
133
+ operations and the accurate mode.
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ src/crival.egg-info/PKG-INFO
6
+ src/crival.egg-info/SOURCES.txt
7
+ src/crival.egg-info/dependency_links.txt
8
+ src/crival.egg-info/requires.txt
9
+ src/crival.egg-info/top_level.txt
10
+ src/ival/__init__.py
11
+ tests/test_ival.py
@@ -0,0 +1 @@
1
+ numpy
@@ -0,0 +1 @@
1
+ ival
@@ -0,0 +1,577 @@
1
+ """ival for Python: tight IEEE 1788.1 intervals over NumPy arrays, from crnumerics' C library libival.
2
+
3
+ An Interval holds one interval, as two Python floats, or an array of them, as two NumPy arrays of float64 broadcast
4
+ like NumPy. Every result is the tightest binary64 interval around the exact one: the functions are CORE-MATH's,
5
+ correctly rounded in every rounding mode. The empty interval is [nan, nan].
6
+
7
+ import ival
8
+ x = ival.Interval(1, 2)
9
+ y = ival.exp(x) * x + 1 # [e + 1, 2 e^2 + 1], each end rounded outward
10
+ t = ival.Interval.from_text("[0.1, 0.2]") # decimal ends rounded outward, not to nearest
11
+ s = ival.sin(ival.Interval([0, 1, 2], [0.5, 4, 3])) # three intervals at once
12
+
13
+ A number that is not a double (a large int, a Fraction, a Decimal, a long double) becomes the two doubles around it,
14
+ never the nearest one, wherever it is given: as an end, as an operand, or as a member to test.
15
+
16
+ The library is libival.so.0: found by the environment variable IVAL_LIBRARY, then a copy inside this package (a
17
+ wheel's), then the system's linker paths (crnumerics' `make install`), then pkg-config's libdir for ival.
18
+ """
19
+ import ctypes
20
+ import ctypes.util
21
+ import math
22
+ import numbers
23
+ import os
24
+ import subprocess
25
+ from decimal import Decimal
26
+ from fractions import Fraction
27
+
28
+ import numpy as np
29
+
30
+ __all__ = ["Interval", "version", "library_path"]
31
+
32
+ _N = ctypes.c_size_t
33
+ _c = ctypes.c_double
34
+ _ref = ctypes.byref
35
+ _ONE = ctypes.c_size_t(1)
36
+ _INF = math.inf
37
+ _NAN = math.nan
38
+
39
+
40
+ def _find():
41
+ env = os.environ.get("IVAL_LIBRARY")
42
+ if env:
43
+ return env
44
+ here = os.path.join(os.path.dirname(os.path.abspath(__file__)), "libival.so.0")
45
+ if os.path.exists(here): # a wheel's own copy (build-wheel.sh)
46
+ return here
47
+ for name in ("libival.so.0", "libival.so"):
48
+ try:
49
+ ctypes.CDLL(name)
50
+ return name
51
+ except OSError:
52
+ pass
53
+ found = ctypes.util.find_library("ival")
54
+ if found:
55
+ return found
56
+ try:
57
+ libdir = subprocess.run(["pkg-config", "--variable=libdir", "ival"], capture_output=True, text=True,
58
+ check=True).stdout.strip()
59
+ for name in ("libival.so.0", "libival.so"):
60
+ path = os.path.join(libdir, name)
61
+ if os.path.exists(path):
62
+ return path
63
+ except (OSError, subprocess.CalledProcessError):
64
+ pass
65
+ raise OSError("ival: libival.so not found; install crnumerics' ival (make install) or set IVAL_LIBRARY")
66
+
67
+
68
+ library_path = _find()
69
+ _lib = ctypes.CDLL(library_path)
70
+ _lib.ival_version.restype = ctypes.c_char_p
71
+ _lib.ival_version.argtypes = []
72
+
73
+
74
+ def version():
75
+ """The C library's version (IVAL_VERSION of the ival.h it was built from)."""
76
+ return _lib.ival_version().decode()
77
+
78
+
79
+ def _arr(x): # float64, C-contiguous, keeping a 0-d array 0-d (np.ascontiguousarray makes it 1-d)
80
+ return np.require(x, dtype=np.float64, requirements="C")
81
+
82
+
83
+ def _p(a): # the array's address (the array stays alive through the call: the caller holds it)
84
+ return ctypes.c_void_p(a.ctypes.data)
85
+
86
+
87
+ # ---- numbers to doubles, rounded outward
88
+ def _round(v, up):
89
+ """the real number v as a double: v itself when it is one, else the double just above it (up) or below it"""
90
+ if type(v) is float:
91
+ return v
92
+ if isinstance(v, np.integer):
93
+ v = int(v) # NumPy compares an int64 with a float in float64, inexactly
94
+ elif isinstance(v, (np.bool_, bool)) or not isinstance(v, (numbers.Real, Decimal)):
95
+ raise TypeError(f"ival: an interval's end must be a real number, not {type(v).__name__}")
96
+ try:
97
+ f = float(v) # to nearest
98
+ except OverflowError: # an int or a Fraction beyond the largest double
99
+ f = _INF if v > 0 else -_INF
100
+ if f != f or f == v: # NaN, or exact (the comparisons are exact for int, Fraction, Decimal, long double)
101
+ return f
102
+ if up:
103
+ return f if f > v else math.nextafter(f, _INF)
104
+ return f if f < v else math.nextafter(f, -_INF)
105
+
106
+
107
+ def _exact(v): # v as a Fraction; None for an infinity or NaN
108
+ if isinstance(v, np.integer):
109
+ v = int(v)
110
+ try:
111
+ return Fraction(*v.as_integer_ratio())
112
+ except (OverflowError, ValueError, AttributeError):
113
+ return None
114
+
115
+
116
+ def _ends(x, up):
117
+ """an array of numbers as float64, each rounded outward (up or down) when it is not a double; and a mask of
118
+ the ones rounded (None when none can be)"""
119
+ a = np.asarray(x)
120
+ k, size = a.dtype.kind, a.dtype.itemsize
121
+ if a.dtype == np.float64 or (k in "iuf" and size <= 4):
122
+ return _arr(a), None # exact in float64
123
+ if k == "f": # a long double: compared with its rounding exactly
124
+ f = a.astype(np.float64)
125
+ moved = f < a if up else f > a
126
+ return _arr(np.where(moved, np.nextafter(f, _INF if up else -_INF), f)), moved
127
+ if k in "iu":
128
+ f = a.astype(np.float64)
129
+ moved = (a > 2**53) | (a < -2**53) if k == "i" else a > 2**53
130
+ if moved.any():
131
+ f[moved] = [_round(int(v), up) for v in a[moved]]
132
+ return _arr(f), moved
133
+ if k == "O":
134
+ f = np.array([_round(v, up) for v in a.ravel()], dtype=np.float64).reshape(a.shape)
135
+ return _arr(f), np.array([type(v) is not float for v in a.ravel()], dtype=bool).reshape(a.shape)
136
+ if k == "b":
137
+ raise TypeError("ival: an interval's end must be a real number, not a bool")
138
+ raise TypeError(f"ival: an interval's end must be a real number, not {a.dtype}"
139
+ + (" (text goes through Interval.from_text)" if k in "US" else ""))
140
+
141
+
142
+ def _same(*arrs):
143
+ """the arrays broadcast to one shape, as C float64 arrays; when they are arrays of one shape and floats (an
144
+ array and a number), the arrays pass as they are and each float is filled out, cheaper than broadcasting"""
145
+ s = next((a.shape for a in arrs if type(a) is np.ndarray), None)
146
+ if s is not None and all(type(a) is float or (type(a) is np.ndarray and a.shape == s) for a in arrs):
147
+ return [np.full(s, a) if type(a) is float else _arr(a) for a in arrs]
148
+ return [_arr(a) for a in np.broadcast_arrays(*arrs)]
149
+
150
+
151
+ def _mk(lo, hi):
152
+ obj = _new(Interval)
153
+ obj.lo, obj.hi = lo, hi
154
+ return obj
155
+
156
+
157
+ def _wrap(lo, hi): # an array result; one interval is kept as two floats
158
+ return _mk(float(lo), float(hi)) if lo.ndim == 0 else _mk(lo, hi)
159
+
160
+
161
+ def _one(lo, hi):
162
+ """[lo, hi] for two doubles, as ival_nums makes it: empty unless lo <= hi, lo != +inf, hi != -inf; zeros +0"""
163
+ if lo <= hi and lo != _INF and hi != -_INF:
164
+ return _mk(0.0 if lo == 0 else lo, 0.0 if hi == 0 else hi)
165
+ return _mk(_NAN, _NAN)
166
+
167
+
168
+ class Interval:
169
+ """One interval, or a NumPy-shaped array of them: lo and hi are two floats, or two float64 arrays of one shape;
170
+ [nan, nan] is empty. Interval(a) is the smallest interval holding the number a (the point [a, a] when a is a
171
+ double); Interval(lo, hi) is the smallest holding [lo, hi], empty unless lo <= hi, lo != +inf and hi != -inf
172
+ (1788's numsToInterval). Decimal ends are best given as text (from_text) or as Decimal: 0.1 as a float is
173
+ already rounded to nearest."""
174
+
175
+ __slots__ = ("lo", "hi")
176
+ __array_priority__ = 1000 # so that ndarray + Interval comes here
177
+
178
+ def __init__(self, lo, hi=None):
179
+ point = hi is None
180
+ if point:
181
+ hi = lo
182
+ if type(lo) is int and -2**53 <= lo <= 2**53: # exact as a double
183
+ lo = float(lo)
184
+ if type(hi) is int and -2**53 <= hi <= 2**53:
185
+ hi = float(hi)
186
+ if type(lo) is float and type(hi) is float: # as ival_nums does it, without the call
187
+ if lo <= hi and lo != _INF and hi != -_INF:
188
+ self.lo, self.hi = 0.0 if lo == 0 else lo, 0.0 if hi == 0 else hi
189
+ else:
190
+ self.lo = self.hi = _NAN
191
+ return
192
+ if not (isinstance(lo, np.ndarray) or isinstance(hi, np.ndarray)) and np.ndim(lo) == 0 and np.ndim(hi) == 0:
193
+ l, h = _round(lo, False), _round(hi, True)
194
+ if (type(lo) is not float or type(hi) is not float) and not point:
195
+ ql, qh = _exact(lo), _exact(hi) # rounded: lo > hi is decided on the numbers themselves
196
+ if ql is not None and qh is not None and ql > qh:
197
+ l = h = _NAN
198
+ r = _one(l, h)
199
+ self.lo, self.hi = r.lo, r.hi
200
+ return
201
+ a, b = np.broadcast_arrays(np.asarray(lo), np.asarray(hi))
202
+ lo_d, ml = _ends(a, False)
203
+ hi_d, mh = _ends(b, True)
204
+ if (ml is not None or mh is not None) and not point:
205
+ moved = (ml if ml is not None else False) | (mh if mh is not None else False)
206
+ lo_d, hi_d = lo_d.copy(), hi_d.copy()
207
+ for k in np.flatnonzero(moved):
208
+ ql, qh = _exact(a.flat[k]), _exact(b.flat[k])
209
+ if ql is not None and qh is not None and ql > qh:
210
+ lo_d.flat[k] = hi_d.flat[k] = _NAN
211
+ out_lo, out_hi, st = np.empty_like(lo_d), np.empty_like(lo_d), np.empty(lo_d.shape, dtype=np.uint8)
212
+ _lib.ival_nums(_p(lo_d), _p(hi_d), _p(out_lo), _p(out_hi), _p(st), lo_d.size)
213
+ if out_lo.ndim == 0:
214
+ out_lo, out_hi = float(out_lo), float(out_hi)
215
+ self.lo, self.hi = out_lo, out_hi
216
+
217
+ @classmethod
218
+ def from_text(cls, s):
219
+ """1788's textToInterval: "[0.1, 0.2]", "[1]", "0.1" (a point), "[entire]", "[empty]", "1.5?3" ...; each
220
+ decimal end rounded outward. s may be a string or an array of strings."""
221
+ strs = np.asarray(s, dtype=object)
222
+ flat = [str(t).encode() for t in strs.ravel()]
223
+ n = len(flat)
224
+ arr = (ctypes.c_char_p * n)(*flat)
225
+ lo, hi, st = np.empty(n), np.empty(n), np.empty(n, dtype=np.uint8)
226
+ _lib.ival_text(arr, _p(lo), _p(hi), _p(st), n)
227
+ return _wrap(lo.reshape(strs.shape), hi.reshape(strs.shape))
228
+
229
+ @classmethod
230
+ def empty(cls, shape=()):
231
+ return _mk(_NAN, _NAN) if shape == () else _mk(np.full(shape, np.nan), np.full(shape, np.nan))
232
+
233
+ @classmethod
234
+ def entire(cls, shape=()):
235
+ return _mk(-_INF, _INF) if shape == () else _mk(np.full(shape, -np.inf), np.full(shape, np.inf))
236
+
237
+ # ---- shape and access
238
+ @property
239
+ def shape(self):
240
+ return np.shape(self.lo)
241
+
242
+ def __len__(self):
243
+ return len(self.lo)
244
+
245
+ def __getitem__(self, k):
246
+ return _wrap(_arr(self.lo[k]), _arr(self.hi[k]))
247
+
248
+ def __iter__(self):
249
+ for k in range(len(self)):
250
+ yield self[k]
251
+
252
+ def __repr__(self):
253
+ if type(self.lo) is float:
254
+ return f"Interval({self.lo!r}, {self.hi!r})" if self.lo == self.lo else "Interval.empty()"
255
+ return f"Interval(lo={self.lo!r}, hi={self.hi!r})"
256
+
257
+ # ---- arithmetic (tight; a number is the smallest interval holding it)
258
+ def __add__(self, o): return _bin(_lib.ival_add, self, o)
259
+ def __radd__(self, o): return _bin(_lib.ival_add, o, self)
260
+ def __sub__(self, o): return _bin(_lib.ival_sub, self, o)
261
+ def __rsub__(self, o): return _bin(_lib.ival_sub, o, self)
262
+ def __mul__(self, o): return _bin(_lib.ival_mul, self, o)
263
+ def __rmul__(self, o): return _bin(_lib.ival_mul, o, self)
264
+ def __truediv__(self, o): return _bin(_lib.ival_div, self, o)
265
+ def __rtruediv__(self, o): return _bin(_lib.ival_div, o, self)
266
+ def __neg__(self): return _un(_lib.ival_neg, self)
267
+ def __pos__(self): return self
268
+ def __abs__(self): return _un(_lib.ival_abs, self)
269
+
270
+ def __pow__(self, p):
271
+ if isinstance(p, (int, np.integer)) or (isinstance(p, np.ndarray) and p.dtype.kind in "iu"):
272
+ return pown(self, p)
273
+ return pow(self, p)
274
+
275
+ # ---- the 1788 measures and tests
276
+ @property
277
+ def inf(self): return _num(_lib.ival_inf, self)
278
+ @property
279
+ def sup(self): return _num(_lib.ival_sup, self)
280
+ @property
281
+ def mid(self): return _num(_lib.ival_mid, self)
282
+ @property
283
+ def wid(self): return _num(_lib.ival_wid, self)
284
+ @property
285
+ def rad(self): return _num(_lib.ival_rad, self)
286
+ @property
287
+ def mag(self): return _num(_lib.ival_mag, self)
288
+ @property
289
+ def mig(self): return _num(_lib.ival_mig, self)
290
+
291
+ def is_empty(self): return _bool1(_lib.ival_isempty, self)
292
+ def is_entire(self): return _bool1(_lib.ival_isentire, self)
293
+ def is_singleton(self): return _bool1(_lib.ival_issingleton, self)
294
+
295
+ def __contains__(self, x):
296
+ """x in X: the number x lies in X (in every interval of an array)"""
297
+ r = contains(self, x)
298
+ return bool(r) if np.ndim(r) == 0 else bool(np.all(r))
299
+
300
+ def subset(self, o): return _bool2(_lib.ival_subset, self, o)
301
+ def interior(self, o): return _bool2(_lib.ival_interior, self, o)
302
+ def disjoint(self, o): return _bool2(_lib.ival_disjoint, self, o)
303
+ def equal(self, o): return _bool2(_lib.ival_equal, self, o)
304
+
305
+ def __and__(self, o): return _bin(_lib.ival_intersect, self, o)
306
+ def __or__(self, o): return _bin(_lib.ival_hull, self, o)
307
+
308
+
309
+ _new = object.__new__
310
+
311
+
312
+ def _as_interval(x):
313
+ if isinstance(x, Interval):
314
+ return x
315
+ if type(x) is int and -2**53 <= x <= 2**53:
316
+ x = float(x)
317
+ if type(x) is float: # a double is the point [x, x]; an infinity or NaN the empty interval
318
+ return _mk(0.0 if x == 0 else x, 0.0 if x == 0 else x) if -_INF < x < _INF else _mk(_NAN, _NAN)
319
+ return Interval(x)
320
+
321
+
322
+ # Each operation below has two paths: one interval (two floats, passed by reference, with no NumPy: a call costs
323
+ # about a microsecond) and arrays (broadcast, one call over all of them).
324
+ def _out(shape):
325
+ return np.empty(shape), np.empty(shape)
326
+
327
+
328
+ def _un(f, x):
329
+ x = _as_interval(x)
330
+ if type(x.lo) is float:
331
+ zl, zh = _c(), _c()
332
+ f(_ref(_c(x.lo)), _ref(_c(x.hi)), _ref(zl), _ref(zh), _ONE)
333
+ return _mk(zl.value, zh.value)
334
+ lo, hi = _arr(x.lo), _arr(x.hi)
335
+ zl, zh = _out(lo.shape)
336
+ f(_p(lo), _p(hi), _p(zl), _p(zh), _N(lo.size))
337
+ return _mk(zl, zh)
338
+
339
+
340
+ def _bin(f, x, y):
341
+ x, y = _as_interval(x), _as_interval(y)
342
+ if type(x.lo) is float and type(y.lo) is float:
343
+ zl, zh = _c(), _c()
344
+ f(_ref(_c(x.lo)), _ref(_c(x.hi)), _ref(_c(y.lo)), _ref(_c(y.hi)), _ref(zl), _ref(zh), _ONE)
345
+ return _mk(zl.value, zh.value)
346
+ al, ah, bl, bh = _same(x.lo, x.hi, y.lo, y.hi)
347
+ zl, zh = _out(al.shape)
348
+ f(_p(al), _p(ah), _p(bl), _p(bh), _p(zl), _p(zh), _N(al.size))
349
+ return _wrap(zl, zh)
350
+
351
+
352
+ def _num(f, x):
353
+ if type(x.lo) is float:
354
+ y = _c()
355
+ f(_ref(_c(x.lo)), _ref(_c(x.hi)), _ref(y), _ONE)
356
+ return y.value
357
+ lo, hi = _arr(x.lo), _arr(x.hi)
358
+ y = np.empty(lo.shape)
359
+ f(_p(lo), _p(hi), _p(y), _N(lo.size))
360
+ return y
361
+
362
+
363
+ def _bool1(f, x):
364
+ if type(x.lo) is float:
365
+ r = ctypes.c_ubyte()
366
+ f(_ref(_c(x.lo)), _ref(_c(x.hi)), _ref(r), _ONE)
367
+ return bool(r.value)
368
+ lo, hi = _arr(x.lo), _arr(x.hi)
369
+ r = np.empty(lo.shape, dtype=np.uint8)
370
+ f(_p(lo), _p(hi), _p(r), _N(lo.size))
371
+ return r.astype(bool)
372
+
373
+
374
+ def _bool2(f, x, y):
375
+ x, y = _as_interval(x), _as_interval(y)
376
+ if type(x.lo) is float and type(y.lo) is float:
377
+ r = ctypes.c_ubyte()
378
+ f(_ref(_c(x.lo)), _ref(_c(x.hi)), _ref(_c(y.lo)), _ref(_c(y.hi)), _ref(r), _ONE)
379
+ return bool(r.value)
380
+ al, ah, bl, bh = _same(x.lo, x.hi, y.lo, y.hi)
381
+ r = np.empty(al.shape, dtype=np.uint8)
382
+ f(_p(al), _p(ah), _p(bl), _p(bh), _p(r), _N(al.size))
383
+ return r.astype(bool) if r.ndim else bool(r)
384
+
385
+
386
+ def contains(x, v):
387
+ """1788's isMember: whether the real number v lies in X, elementwise (an infinity never does)"""
388
+ x = _as_interval(x)
389
+ if np.ndim(v) == 0 and type(x.lo) is float:
390
+ if type(v) is not float: # compared exactly, not rounded
391
+ q = _exact(v)
392
+ return q is not None and x.lo <= q <= x.hi
393
+ r = ctypes.c_ubyte()
394
+ _lib.ival_ismember(_ref(_c(v)), _ref(_c(x.lo)), _ref(_c(x.hi)), _ref(r), _ONE)
395
+ return bool(r.value)
396
+ a = np.asarray(v)
397
+ if not (a.dtype == np.float64 or (a.dtype.kind in "iuf" and a.dtype.itemsize <= 4)):
398
+ va, lo, hi = np.broadcast_arrays(a, x.lo, x.hi)
399
+ r = np.array([contains(_mk(float(l), float(h)), w) for w, l, h in zip(va.ravel(), lo.ravel(), hi.ravel())])
400
+ return r.reshape(va.shape) if va.ndim else bool(r[0])
401
+ v, lo, hi = (_arr(b) for b in np.broadcast_arrays(a, x.lo, x.hi))
402
+ r = np.empty(v.shape, dtype=np.uint8)
403
+ _lib.ival_ismember(_p(v), _p(lo), _p(hi), _p(r), _N(v.size))
404
+ return r.astype(bool) if r.ndim else bool(r)
405
+
406
+
407
+ # ---- the functions, each the tightest interval around f over X (within f's domain)
408
+ _UNARY = ["acos", "acosh", "acospi", "asin", "asinh", "asinpi", "atan", "atanh", "atanpi", "cbrt", "cos", "cosh",
409
+ "cospi", "erf", "erfc", "exp", "exp10", "exp2", "expm1", "log", "log10", "log1p", "log2", "rsqrt", "sin",
410
+ "sinh", "sinpi", "sqrt", "tan", "tanh", "tanpi", "tgamma", "sqr", "recip", "sign", "ceil", "floor",
411
+ "trunc", "round", "roundeven"]
412
+ _BINARY = ["atan2", "hypot", "pow", "min", "max", "cancelminus", "cancelplus"]
413
+
414
+
415
+ def _make_unary(name):
416
+ cf = getattr(_lib, "ival_" + name)
417
+
418
+ def f(x):
419
+ return _un(cf, x)
420
+ f.__name__ = name
421
+ f.__doc__ = f"ival_{name}: the tightest interval around {name}(x) for x in X, elementwise"
422
+ return f
423
+
424
+
425
+ def _make_binary(name):
426
+ cf = getattr(_lib, "ival_" + name)
427
+
428
+ def f(x, y):
429
+ return _bin(cf, x, y)
430
+ f.__name__ = name
431
+ f.__doc__ = f"ival_{name}: the tightest interval around {name}(x, y) for x in X, y in Y, elementwise"
432
+ return f
433
+
434
+
435
+ for _n in _UNARY:
436
+ globals()[_n] = _make_unary(_n)
437
+ __all__.append(_n)
438
+ for _n in _BINARY:
439
+ globals()[_n] = _make_binary(_n)
440
+ __all__.append(_n)
441
+ for _n in _UNARY[:32]: # the 32 functions (ival-list.h) have the accurate mode: one ulp at most, at vector speed
442
+ globals()["acc_" + _n] = _make_unary("acc_" + _n)
443
+ __all__.append("acc_" + _n)
444
+ for _n in ("atan2", "hypot", "pow"):
445
+ globals()["acc_" + _n] = _make_binary("acc_" + _n)
446
+ __all__.append("acc_" + _n)
447
+
448
+
449
+ def _int1(p): # an integer exponent or root as a C int
450
+ if type(p) is int and -2**31 <= p < 2**31:
451
+ return p
452
+ if isinstance(p, (np.bool_, bool)) or not isinstance(p, (numbers.Integral, np.integer)):
453
+ if not (isinstance(p, (numbers.Real, np.floating)) and float(p).is_integer()):
454
+ raise TypeError(f"ival: the power must be an integer, not {p!r}")
455
+ p = int(p)
456
+ if not -2**31 <= p < 2**31:
457
+ raise OverflowError(f"ival: the power {p} is beyond a C int")
458
+ return p
459
+
460
+
461
+ def _ints(p): # an array of them
462
+ a = np.asarray(p)
463
+ if a.dtype.kind not in "iu" and not (a.dtype.kind == "f" and np.all(a == np.floor(a))):
464
+ raise TypeError(f"ival: the powers must be integers, not {a.dtype}")
465
+ if a.size and (a.min() < -2**31 or a.max() >= 2**31):
466
+ raise OverflowError("ival: a power beyond a C int")
467
+ return a.astype(np.intc)
468
+
469
+
470
+ def _pow_int(f, x, p):
471
+ x = _as_interval(x)
472
+ if type(x.lo) is float and (type(p) is int or np.ndim(p) == 0):
473
+ zl, zh = _c(), _c()
474
+ f(_ref(_c(x.lo)), _ref(_c(x.hi)), _ref(ctypes.c_int(_int1(p))), _ref(zl), _ref(zh), _ONE)
475
+ return _mk(zl.value, zh.value)
476
+ lo, hi, pp = np.broadcast_arrays(x.lo, x.hi, _ints(p))
477
+ lo, hi = _arr(lo), _arr(hi)
478
+ pp = np.require(pp, dtype=np.intc, requirements="C")
479
+ zl, zh = _out(lo.shape)
480
+ f(_p(lo), _p(hi), _p(pp), _p(zl), _p(zh), _N(lo.size))
481
+ return _wrap(zl, zh)
482
+
483
+
484
+ def pown(x, p):
485
+ """x^p for integer p, elementwise (1788's pown): tight; a negative power of [0, 0] is empty"""
486
+ return _pow_int(_lib.ival_pown, x, p)
487
+
488
+
489
+ def rootn(x, q):
490
+ """the real q-th root, elementwise (1788.1's rootn)"""
491
+ return _pow_int(_lib.ival_rootn, x, q)
492
+
493
+
494
+ def fma(a, b, c):
495
+ """a * b + c with one rounding of each end, elementwise"""
496
+ a, b, c = _as_interval(a), _as_interval(b), _as_interval(c)
497
+ if type(a.lo) is float and type(b.lo) is float and type(c.lo) is float:
498
+ zl, zh = _c(), _c()
499
+ _lib.ival_fma(*(_ref(_c(v)) for v in (a.lo, a.hi, b.lo, b.hi, c.lo, c.hi)), _ref(zl), _ref(zh), _ONE)
500
+ return _mk(zl.value, zh.value)
501
+ arrs = _same(a.lo, a.hi, b.lo, b.hi, c.lo, c.hi)
502
+ zl, zh = _out(arrs[0].shape)
503
+ _lib.ival_fma(*(_p(v) for v in arrs), _p(zl), _p(zh), _N(arrs[0].size))
504
+ return _wrap(zl, zh)
505
+
506
+
507
+ # ---- reverse operations: the tightest interval around {x in X : f(x) in C}
508
+ def _make_rev(name):
509
+ cf = getattr(_lib, "ival_" + name)
510
+
511
+ def f(c, x=None):
512
+ c = _as_interval(c)
513
+ x = _mk(-_INF, _INF) if x is None else _as_interval(x)
514
+ if type(c.lo) is float and type(x.lo) is float:
515
+ zl, zh = _c(), _c()
516
+ cf(_ref(_c(c.lo)), _ref(_c(c.hi)), _ref(_c(x.lo)), _ref(_c(x.hi)), _ref(zl), _ref(zh), _ONE)
517
+ return _mk(zl.value, zh.value)
518
+ cl, ch, xl, xh = _same(c.lo, c.hi, x.lo, x.hi)
519
+ zl, zh = _out(cl.shape)
520
+ cf(_p(cl), _p(ch), _p(xl), _p(xh), _p(zl), _p(zh), _N(cl.size))
521
+ return _wrap(zl, zh)
522
+ f.__name__ = name
523
+ f.__doc__ = f"ival_{name}(C, X): the tightest interval around {{x in X : f(x) in C}} (X the whole line if omitted)"
524
+ return f
525
+
526
+
527
+ for _n in ("sqrrev", "absrev", "coshrev", "sinrev", "cosrev", "tanrev"):
528
+ globals()[_n] = _make_rev(_n)
529
+ __all__.append(_n)
530
+
531
+
532
+ def pownrev(c, p, x=None):
533
+ """the tightest interval around {x in X : x^p in C}"""
534
+ c = _as_interval(c)
535
+ x = _mk(-_INF, _INF) if x is None else _as_interval(x)
536
+ if type(c.lo) is float and type(x.lo) is float and (type(p) is int or np.ndim(p) == 0):
537
+ zl, zh = _c(), _c()
538
+ _lib.ival_pownrev(_ref(_c(c.lo)), _ref(_c(c.hi)), _ref(_c(x.lo)), _ref(_c(x.hi)),
539
+ _ref(ctypes.c_int(_int1(p))), _ref(zl), _ref(zh), _ONE)
540
+ return _mk(zl.value, zh.value)
541
+ cl, ch, xl, xh, pp = np.broadcast_arrays(c.lo, c.hi, x.lo, x.hi, _ints(p))
542
+ cl, ch, xl, xh = _arr(cl), _arr(ch), _arr(xl), _arr(xh)
543
+ pp = np.require(pp, dtype=np.intc, requirements="C")
544
+ zl, zh = _out(cl.shape)
545
+ _lib.ival_pownrev(_p(cl), _p(ch), _p(xl), _p(xh), _p(pp), _p(zl), _p(zh), _N(cl.size))
546
+ return _wrap(zl, zh)
547
+
548
+
549
+ def _rev3(f, b, c, x):
550
+ b, c = _as_interval(b), _as_interval(c)
551
+ x = _mk(-_INF, _INF) if x is None else _as_interval(x)
552
+ if type(b.lo) is float and type(c.lo) is float and type(x.lo) is float:
553
+ zl, zh = _c(), _c()
554
+ f(*(_ref(_c(v)) for v in (b.lo, b.hi, c.lo, c.hi, x.lo, x.hi)), _ref(zl), _ref(zh), _ONE)
555
+ return _mk(zl.value, zh.value)
556
+ arrs = _same(b.lo, b.hi, c.lo, c.hi, x.lo, x.hi)
557
+ zl, zh = _out(arrs[0].shape)
558
+ f(*(_p(v) for v in arrs), _p(zl), _p(zh), _N(arrs[0].size))
559
+ return _wrap(zl, zh)
560
+
561
+
562
+ def mulrev(b, c, x=None):
563
+ """the tightest interval around {x in X : x b in C for some b in B}"""
564
+ return _rev3(_lib.ival_mulrev, b, c, x)
565
+
566
+
567
+ def powrev1(b, c, x=None):
568
+ """the tightest interval around {x in X : x^y in C for some y in B}"""
569
+ return _rev3(_lib.ival_powrev1, b, c, x)
570
+
571
+
572
+ def powrev2(a, c, y=None):
573
+ """the tightest interval around {y in Y : x^y in C for some x in A}"""
574
+ return _rev3(_lib.ival_powrev2, a, c, y)
575
+
576
+
577
+ __all__ += ["pown", "rootn", "fma", "contains", "pownrev", "mulrev", "powrev1", "powrev2"]
@@ -0,0 +1,193 @@
1
+ """Tests of ival's Python binding: results tight against mpmath (300 bits) and exact fractions, arrays elementwise
2
+ the same as one interval at a time, the constructors, empty intervals, the reverse operations and the accurate mode.
3
+ Run with libival on the linker's path or IVAL_LIBRARY naming it: python3 -m pytest ival/python/tests"""
4
+ import math
5
+ import random
6
+ from fractions import Fraction
7
+
8
+ import mpmath
9
+ import numpy as np
10
+ import pytest
11
+
12
+ import ival
13
+
14
+ mpmath.mp.prec = 300
15
+ rng = random.Random(20261009)
16
+
17
+
18
+ def adjacent(lo, hi):
19
+ """lo and hi the same double or neighbours: the tightest interval around a value not a double"""
20
+ return lo == hi or math.nextafter(lo, math.inf) == hi
21
+
22
+
23
+ def brackets(lo, hi, exact):
24
+ return mpmath.mpf(lo) <= exact <= mpmath.mpf(hi)
25
+
26
+
27
+ def test_version():
28
+ assert isinstance(ival.version(), str) and ival.version().count(".") == 2
29
+
30
+
31
+ FUNCS = [("exp", mpmath.exp, lambda: rng.uniform(-700, 700)), ("log", mpmath.log, lambda: math.exp(rng.uniform(-700, 700))),
32
+ ("sin", mpmath.sin, lambda: rng.uniform(-1e6, 1e6)), ("cos", mpmath.cos, lambda: rng.uniform(-1e6, 1e6)),
33
+ ("tan", mpmath.tan, lambda: rng.uniform(-100, 100)), ("atan", mpmath.atan, lambda: rng.uniform(-1e3, 1e3)),
34
+ ("sinh", mpmath.sinh, lambda: rng.uniform(-700, 700)), ("tanh", mpmath.tanh, lambda: rng.uniform(-20, 20)),
35
+ ("cbrt", lambda x: mpmath.sign(x) * mpmath.cbrt(abs(x)), lambda: rng.uniform(-1e9, 1e9)), # the real root
36
+ ("exp2", lambda x: mpmath.power(2, x), lambda: rng.uniform(-1000, 1000)),
37
+ ("log1p", mpmath.log1p, lambda: rng.uniform(-0.999, 1e6)), ("erf", mpmath.erf, lambda: rng.uniform(-6, 6)),
38
+ ("asinh", mpmath.asinh, lambda: rng.uniform(-1e6, 1e6)), ("sqrt", mpmath.sqrt, lambda: rng.uniform(0, 1e300))]
39
+
40
+
41
+ @pytest.mark.parametrize("name,ref,gen", FUNCS, ids=[f[0] for f in FUNCS])
42
+ def test_points_tight(name, ref, gen):
43
+ """f of a point [x, x]: the exact f(x) inside, the ends adjacent (each the exact value rounded down and up)"""
44
+ xs = np.array([gen() for _ in range(300)])
45
+ r = getattr(ival, name)(ival.Interval(xs))
46
+ for x, lo, hi in zip(xs, r.lo, r.hi):
47
+ exact = ref(mpmath.mpf(float(x)))
48
+ assert brackets(lo, hi, exact), (name, x, lo, hi)
49
+ assert adjacent(lo, hi), (name, x, lo, hi)
50
+
51
+
52
+ def test_arith_exact():
53
+ """the four operations against exact fractions: inside, and the ends adjacent"""
54
+ for _ in range(2000):
55
+ a, b = rng.uniform(-1e6, 1e6), rng.uniform(-1e6, 1e6)
56
+ for op, ex in (("__add__", Fraction(a) + Fraction(b)), ("__sub__", Fraction(a) - Fraction(b)),
57
+ ("__mul__", Fraction(a) * Fraction(b)), ("__truediv__", Fraction(a) / Fraction(b))):
58
+ r = getattr(ival.Interval(a), op)(b)
59
+ assert Fraction(float(r.lo)) <= ex <= Fraction(float(r.hi)), (op, a, b)
60
+ assert adjacent(float(r.lo), float(r.hi)), (op, a, b)
61
+
62
+
63
+ def test_point_one_plus_point_two():
64
+ r = ival.Interval(0.1) + 0.2
65
+ assert float(r.lo) == 0.3 and float(r.hi) == math.nextafter(0.3, 1)
66
+
67
+
68
+ def test_text_rounds_outward():
69
+ t = ival.Interval.from_text("[0.1, 0.2]")
70
+ assert Fraction(float(t.lo)) < Fraction("0.1") and adjacent(float(t.lo), 0.1)
71
+ assert Fraction(float(t.hi)) > Fraction("0.2") or float(t.hi) == 0.2
72
+ assert Fraction("0.2") <= Fraction(float(t.hi)) and adjacent(0.2, float(t.hi)) or float(t.hi) == 0.2
73
+ e = ival.Interval.from_text(["[empty]", "[entire]", "[1, 2]"])
74
+ assert e.shape == (3,) and e.is_empty()[0] and e.is_entire()[1] and float(e.lo[2]) == 1.0
75
+
76
+
77
+ def test_arrays_match_one_at_a_time():
78
+ lo = np.array([rng.uniform(-10, 10) for _ in range(500)])
79
+ hi = lo + np.array([rng.uniform(0, 3) for _ in range(500)])
80
+ X = ival.Interval(lo, hi)
81
+ for f in (ival.sin, ival.exp, ival.atan, ival.sqr, ival.cosh):
82
+ R = f(X)
83
+ for k in range(0, 500, 37):
84
+ r = f(ival.Interval(lo[k], hi[k]))
85
+ assert (float(r.lo), float(r.hi)) == (R.lo[k], R.hi[k])
86
+ Y = X * 2 + 1 # broadcasting a number
87
+ assert Y.shape == (500,)
88
+ Z = X + ival.Interval(lo[:1], hi[:1]) # a one-interval array broadcast
89
+ assert Z.shape == (500,)
90
+
91
+
92
+ def test_empty():
93
+ e = ival.Interval(2, 1)
94
+ assert e.is_empty()
95
+ for r in (e + 1, ival.exp(e), e * ival.Interval(1, 2), ival.sqrt(ival.Interval(-3, -1))):
96
+ assert r.is_empty()
97
+ assert not ival.Interval(0, 1).is_empty()
98
+
99
+
100
+ def test_domain_and_poles():
101
+ assert float(ival.log(ival.Interval(-1, 1)).lo) == -math.inf
102
+ t = ival.tan(ival.Interval(1, 2)) # across pi/2
103
+ assert float(t.lo) == -math.inf and float(t.hi) == math.inf
104
+ assert float((ival.Interval(-2, 3) ** 2).lo) == 0.0 # an even power straddling 0
105
+
106
+
107
+ def test_pown_tight():
108
+ for _ in range(300):
109
+ x, p = rng.uniform(-50, 50), rng.choice([2, 3, 4, 5, 7, 11, -1, -2, -3])
110
+ r = ival.pown(ival.Interval(x), p)
111
+ exact = mpmath.power(mpmath.mpf(x), p)
112
+ assert brackets(float(r.lo), float(r.hi), exact) and adjacent(float(r.lo), float(r.hi)), (x, p)
113
+
114
+
115
+ def test_reverse():
116
+ r = ival.sqrrev(ival.Interval(4, 9))
117
+ assert (float(r.lo), float(r.hi)) == (-3.0, 3.0)
118
+ r = ival.sqrrev(ival.Interval(4, 9), ival.Interval(0, 10))
119
+ assert (float(r.lo), float(r.hi)) == (2.0, 3.0)
120
+ r = ival.powrev2(ival.Interval(0.25, 0.5), ival.Interval(2, math.inf)) # ITF1788.jl#8's case
121
+ assert (float(r.lo), float(r.hi)) == (-math.inf, -0.5)
122
+ r = ival.mulrev(ival.Interval(2, 4), ival.Interval(8, 8))
123
+ assert (float(r.lo), float(r.hi)) == (2.0, 4.0)
124
+
125
+
126
+ def test_accurate_mode():
127
+ """one ulp at most outside the tight result (equal to it without crmvec)"""
128
+ xs = np.array([rng.uniform(-50, 50) for _ in range(400)])
129
+ X = ival.Interval(xs, xs + 0.5)
130
+ for name in ("exp", "sin", "log1p", "atan"):
131
+ T, A = getattr(ival, name)(X), getattr(ival, "acc_" + name)(X)
132
+ ok = ~np.isnan(T.lo)
133
+ assert np.all(A.lo[ok] <= T.lo[ok]) and np.all(A.hi[ok] >= T.hi[ok])
134
+ assert np.all(np.nextafter(T.lo[ok], -np.inf) <= A.lo[ok]) and np.all(A.hi[ok] <= np.nextafter(T.hi[ok], np.inf))
135
+
136
+
137
+ def test_measures_and_membership():
138
+ x = ival.Interval(1, 3)
139
+ assert x.mid == 2.0 and x.wid == 2.0 and x.rad == 1.0 and x.mag == 3.0 and x.mig == 1.0
140
+ assert 2.5 in x and 3.5 not in x
141
+ assert ival.Interval(1.5, 2).subset(x) and not x.subset(ival.Interval(1.5, 2))
142
+ assert float((x & ival.Interval(2, 5)).lo) == 2.0 and float((x | ival.Interval(5, 6)).hi) == 6.0
143
+
144
+
145
+ def _ok(r, exact): # the interval r contains the exact number (a Fraction), and its ends are adjacent doubles
146
+ return Fraction(float(r.lo)) <= exact <= Fraction(float(r.hi)) and adjacent(float(r.lo), float(r.hi))
147
+
148
+
149
+ def test_numbers_round_outward():
150
+ """a number that is not a double becomes the two doubles around it, not the nearest one"""
151
+ from decimal import Decimal
152
+ for v in (2**53 + 1, -(2**60) - 3, 10**20 + 7, Decimal("0.1"), Fraction(1, 3)):
153
+ assert _ok(ival.Interval(v), Fraction(v)), v
154
+ r = ival.Interval(Fraction(1, 3), 2**53 + 1)
155
+ assert Fraction(float(r.lo)) <= Fraction(1, 3) and Fraction(float(r.hi)) >= 2**53 + 1
156
+ assert ival.Interval(2**53 + 3, 2**53 + 1).is_empty() # lo > hi though both round to 2^53 + 2
157
+ big = ival.Interval(10**400) # beyond the largest double
158
+ assert float(big.lo) == np.finfo(np.float64).max and float(big.hi) == math.inf
159
+ a = np.array([2**53 + 1, 2**62 + 1, -(2**63) + 1, 5], dtype=np.int64)
160
+ r = ival.Interval(a)
161
+ for k, v in enumerate(a.tolist()):
162
+ assert Fraction(float(r.lo[k])) <= v <= Fraction(float(r.hi[k])), v
163
+ if np.finfo(np.longdouble).nmant > 52: # x86's 80-bit long double
164
+ x = np.longdouble(1) / 3
165
+ assert _ok(ival.Interval(x), Fraction(*x.as_integer_ratio()))
166
+ r = ival.Interval(np.array([x, -x]))
167
+ assert _ok(r[1], -Fraction(*x.as_integer_ratio()))
168
+ assert (float(ival.Interval(0.5).lo), float(ival.Interval(7).hi)) == (0.5, 7.0) # exact numbers stay points
169
+ r = ival.Interval(1, 2) + Fraction(1, 3) # an operand too
170
+ assert Fraction(r.lo) <= Fraction(4, 3) and Fraction(r.hi) >= Fraction(7, 3)
171
+ assert ival.contains(ival.Interval(0, 2**53), 2**53 + 1) is False and (2**53 + 1) in ival.Interval(2**53 + 1)
172
+ assert not ival.contains(ival.Interval(0, 1), np.array([2**60 + 1])).any()
173
+
174
+
175
+ def test_one_at_a_time_matches_arrays():
176
+ """an Interval of one interval (Python floats) gives the bounds of the array path, through every kind of call"""
177
+ xs = [rng.uniform(-3, 3) for _ in range(200)]
178
+ X = ival.Interval(np.array(xs), np.array(xs) + 0.25)
179
+ one = [ival.Interval(x, x + 0.25) for x in xs]
180
+ assert all(type(o.lo) is float for o in one)
181
+ calls = [(lambda A: ival.exp(A)), (lambda A: A * A - 1 / A), (lambda A: ival.pown(A, 3)), (lambda A: A ** -2),
182
+ (lambda A: ival.atan2(A, A + 1)), (lambda A: ival.sqrrev(A, ival.Interval(0, 4))),
183
+ (lambda A: ival.fma(A, A, 1)), (lambda A: ival.powrev1(ival.Interval(2, 3), A + 4)),
184
+ (lambda A: ival.rootn(A, 3)), (lambda A: A & ival.Interval(0, 1))]
185
+ for f in calls:
186
+ R = f(X)
187
+ for k in range(0, 200, 7):
188
+ r = f(one[k])
189
+ assert type(r.lo) is float
190
+ assert np.array_equal([r.lo, r.hi], [R.lo[k], R.hi[k]], equal_nan=True), k
191
+ assert [o.mid for o in one[:5]] == list(X.mid[:5]) and [o.is_empty() for o in one[:5]] == [False] * 5
192
+ assert ival.Interval(1, 2).subset(ival.Interval(0, 3)) is True and ival.contains(ival.Interval(1, 2), 1.5) is True
193
+ assert type(X[3].lo) is float and X[3].equal(one[3])