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 +25 -0
- crival-0.2.0/PKG-INFO +133 -0
- crival-0.2.0/README.md +121 -0
- crival-0.2.0/pyproject.toml +21 -0
- crival-0.2.0/setup.cfg +4 -0
- crival-0.2.0/setup.py +27 -0
- crival-0.2.0/src/crival.egg-info/PKG-INFO +133 -0
- crival-0.2.0/src/crival.egg-info/SOURCES.txt +11 -0
- crival-0.2.0/src/crival.egg-info/dependency_links.txt +1 -0
- crival-0.2.0/src/crival.egg-info/requires.txt +1 -0
- crival-0.2.0/src/crival.egg-info/top_level.txt +1 -0
- crival-0.2.0/src/ival/__init__.py +577 -0
- crival-0.2.0/tests/test_ival.py +193 -0
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
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
|
+
|
|
@@ -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])
|