certivl 0.1.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.
- certivl-0.1.0/LICENSE +21 -0
- certivl-0.1.0/PKG-INFO +114 -0
- certivl-0.1.0/README.md +90 -0
- certivl-0.1.0/pyproject.toml +43 -0
- certivl-0.1.0/setup.cfg +4 -0
- certivl-0.1.0/src/certivl/__init__.py +38 -0
- certivl-0.1.0/src/certivl/exact.py +444 -0
- certivl-0.1.0/src/certivl.egg-info/PKG-INFO +114 -0
- certivl-0.1.0/src/certivl.egg-info/SOURCES.txt +11 -0
- certivl-0.1.0/src/certivl.egg-info/dependency_links.txt +1 -0
- certivl-0.1.0/src/certivl.egg-info/requires.txt +1 -0
- certivl-0.1.0/src/certivl.egg-info/top_level.txt +1 -0
- certivl-0.1.0/tests/test_exact.py +170 -0
certivl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vincent Gonzalez
|
|
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.
|
certivl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: certivl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Exact rational and certified interval arithmetic: enclosures that turn a computed inequality into a proof.
|
|
5
|
+
Author-email: Vincent Gonzalez <vincegonzalez@me.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vince-gonzalez/certivl
|
|
8
|
+
Project-URL: Source, https://github.com/vince-gonzalez/certivl
|
|
9
|
+
Keywords: interval arithmetic,certified computation,computer-assisted proof,exact arithmetic,rational arithmetic,validated numerics,rigorous numerics
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: mpmath>=1.3
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# certivl
|
|
26
|
+
|
|
27
|
+
Exact rational and certified interval arithmetic. Every quantity is a
|
|
28
|
+
`Fraction` or an `Ivl` — a closed interval with exact rational endpoints
|
|
29
|
+
guaranteed to contain the true value — and every operation rounds outward, so
|
|
30
|
+
containment survives composition.
|
|
31
|
+
|
|
32
|
+
That is the whole point: **if `x.hi < 0` then the true value is negative.** Not
|
|
33
|
+
probably, not to within tolerance. A computed inequality becomes a proof.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
pip install certivl
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from certivl import Ivl, pi_ivl, sqrt_ivl
|
|
41
|
+
|
|
42
|
+
p = pi_ivl()
|
|
43
|
+
print(float(p.hi - p.lo)) # 2e-80
|
|
44
|
+
|
|
45
|
+
x = Ivl(3, 3) * sqrt_ivl(2) - p # 3*sqrt(2) - pi
|
|
46
|
+
assert x.lo > Ivl(11, 10).lo # proved > 1.1, not estimated
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## What is in it
|
|
50
|
+
|
|
51
|
+
`Ivl` with the arithmetic operators, and validated enclosures for
|
|
52
|
+
`sqrt`, `sin`, `cos`, `tan`, `sec`, `asin`, `atan`, `pi`, `sqrt2`, `sqrt3`,
|
|
53
|
+
`deg`, `abs`, integer `isqrt`, plus `isolate_root` for certified root
|
|
54
|
+
bracketing of an integer polynomial by bisection with exact sign evaluation.
|
|
55
|
+
|
|
56
|
+
The algebraic half runs on the standard library alone — `Ivl` arithmetic,
|
|
57
|
+
`sqrt`, `isqrt`, `sqrt2`, `sqrt3`, `abs`, `isolate_root`. The transcendental
|
|
58
|
+
enclosures — `pi`, `sin`, `cos`, `tan`, `sec`, `asin`, `atan`, `deg` — are built
|
|
59
|
+
from `mpmath`'s validated interval type, widened outward, so `mpmath` is a
|
|
60
|
+
dependency rather than an extra.
|
|
61
|
+
|
|
62
|
+
## Why not mpmath, Arb, or python-flint
|
|
63
|
+
|
|
64
|
+
Those are faster and more general, and if you want validated numerics at scale
|
|
65
|
+
you should use them. This exists for a narrower job: **plane geometry where the
|
|
66
|
+
answer has to be a proof and the constants are algebraic.** Exact `Fraction`
|
|
67
|
+
endpoints throughout, no binary float anywhere on the path, and denominators
|
|
68
|
+
sized so the final interval widths are irrelevant to the conclusion rather than
|
|
69
|
+
tuned to it.
|
|
70
|
+
|
|
71
|
+
## Where it came from
|
|
72
|
+
|
|
73
|
+
This is the kernel underneath four deposited papers on certified computation for
|
|
74
|
+
classical plane-covering problems — the Lebesgue universal covering ladder,
|
|
75
|
+
opaque sets for the unit disc, and Fejes Tóth's point-goalie problem. It
|
|
76
|
+
certified Pál, Sprague and Hansen's published areas to the digits their authors
|
|
77
|
+
quoted, and adjudicated a disagreement between a published table and its
|
|
78
|
+
author's own write-up.
|
|
79
|
+
|
|
80
|
+
## The bug that explains the design
|
|
81
|
+
|
|
82
|
+
An earlier version converted `mpf` values by re-creating them in the ambient
|
|
83
|
+
mpmath context before reading their tuple. `mp.prec` in a fresh process is 53,
|
|
84
|
+
so the *first* conversions of a run were silently rounded to double precision —
|
|
85
|
+
a one-sided error near 1e-17, inside intervals padded to 1e-80.
|
|
86
|
+
|
|
87
|
+
Nothing caught it for weeks. It surfaced through a cross-check between two
|
|
88
|
+
independently computed results that should have summed to zero and instead
|
|
89
|
+
missed by **8.1e-19** — a discrepancy only visible because everything around it
|
|
90
|
+
was exact. The conversion now reads the raw `(sign, man, exp, bc)` tuple, which
|
|
91
|
+
cannot round.
|
|
92
|
+
|
|
93
|
+
That is the argument for exact endpoints in one paragraph: a rounding error
|
|
94
|
+
inside a tolerance is invisible, and a rounding error inside a *proof* is fatal.
|
|
95
|
+
|
|
96
|
+
## Tests
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
python tests/test_exact.py
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Constants are checked against published decimal expansions quoted from
|
|
103
|
+
elsewhere, never generated by this code — a kernel checked against itself is
|
|
104
|
+
checked against nothing. The suite finishes by confirming a value known to lie
|
|
105
|
+
outside its interval is rejected, because a test that has never failed is not
|
|
106
|
+
evidence.
|
|
107
|
+
|
|
108
|
+
Note what is being tested: containment, not accuracy. A wide interval is
|
|
109
|
+
useless and honest; an interval that excludes the true value is a broken proof.
|
|
110
|
+
`Ivl(-2, 3) ** 2` returns `[-6, 9]` rather than `[0, 9]` — loose, and correct.
|
|
111
|
+
|
|
112
|
+
## Licence
|
|
113
|
+
|
|
114
|
+
MIT.
|
certivl-0.1.0/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# certivl
|
|
2
|
+
|
|
3
|
+
Exact rational and certified interval arithmetic. Every quantity is a
|
|
4
|
+
`Fraction` or an `Ivl` — a closed interval with exact rational endpoints
|
|
5
|
+
guaranteed to contain the true value — and every operation rounds outward, so
|
|
6
|
+
containment survives composition.
|
|
7
|
+
|
|
8
|
+
That is the whole point: **if `x.hi < 0` then the true value is negative.** Not
|
|
9
|
+
probably, not to within tolerance. A computed inequality becomes a proof.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
pip install certivl
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from certivl import Ivl, pi_ivl, sqrt_ivl
|
|
17
|
+
|
|
18
|
+
p = pi_ivl()
|
|
19
|
+
print(float(p.hi - p.lo)) # 2e-80
|
|
20
|
+
|
|
21
|
+
x = Ivl(3, 3) * sqrt_ivl(2) - p # 3*sqrt(2) - pi
|
|
22
|
+
assert x.lo > Ivl(11, 10).lo # proved > 1.1, not estimated
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## What is in it
|
|
26
|
+
|
|
27
|
+
`Ivl` with the arithmetic operators, and validated enclosures for
|
|
28
|
+
`sqrt`, `sin`, `cos`, `tan`, `sec`, `asin`, `atan`, `pi`, `sqrt2`, `sqrt3`,
|
|
29
|
+
`deg`, `abs`, integer `isqrt`, plus `isolate_root` for certified root
|
|
30
|
+
bracketing of an integer polynomial by bisection with exact sign evaluation.
|
|
31
|
+
|
|
32
|
+
The algebraic half runs on the standard library alone — `Ivl` arithmetic,
|
|
33
|
+
`sqrt`, `isqrt`, `sqrt2`, `sqrt3`, `abs`, `isolate_root`. The transcendental
|
|
34
|
+
enclosures — `pi`, `sin`, `cos`, `tan`, `sec`, `asin`, `atan`, `deg` — are built
|
|
35
|
+
from `mpmath`'s validated interval type, widened outward, so `mpmath` is a
|
|
36
|
+
dependency rather than an extra.
|
|
37
|
+
|
|
38
|
+
## Why not mpmath, Arb, or python-flint
|
|
39
|
+
|
|
40
|
+
Those are faster and more general, and if you want validated numerics at scale
|
|
41
|
+
you should use them. This exists for a narrower job: **plane geometry where the
|
|
42
|
+
answer has to be a proof and the constants are algebraic.** Exact `Fraction`
|
|
43
|
+
endpoints throughout, no binary float anywhere on the path, and denominators
|
|
44
|
+
sized so the final interval widths are irrelevant to the conclusion rather than
|
|
45
|
+
tuned to it.
|
|
46
|
+
|
|
47
|
+
## Where it came from
|
|
48
|
+
|
|
49
|
+
This is the kernel underneath four deposited papers on certified computation for
|
|
50
|
+
classical plane-covering problems — the Lebesgue universal covering ladder,
|
|
51
|
+
opaque sets for the unit disc, and Fejes Tóth's point-goalie problem. It
|
|
52
|
+
certified Pál, Sprague and Hansen's published areas to the digits their authors
|
|
53
|
+
quoted, and adjudicated a disagreement between a published table and its
|
|
54
|
+
author's own write-up.
|
|
55
|
+
|
|
56
|
+
## The bug that explains the design
|
|
57
|
+
|
|
58
|
+
An earlier version converted `mpf` values by re-creating them in the ambient
|
|
59
|
+
mpmath context before reading their tuple. `mp.prec` in a fresh process is 53,
|
|
60
|
+
so the *first* conversions of a run were silently rounded to double precision —
|
|
61
|
+
a one-sided error near 1e-17, inside intervals padded to 1e-80.
|
|
62
|
+
|
|
63
|
+
Nothing caught it for weeks. It surfaced through a cross-check between two
|
|
64
|
+
independently computed results that should have summed to zero and instead
|
|
65
|
+
missed by **8.1e-19** — a discrepancy only visible because everything around it
|
|
66
|
+
was exact. The conversion now reads the raw `(sign, man, exp, bc)` tuple, which
|
|
67
|
+
cannot round.
|
|
68
|
+
|
|
69
|
+
That is the argument for exact endpoints in one paragraph: a rounding error
|
|
70
|
+
inside a tolerance is invisible, and a rounding error inside a *proof* is fatal.
|
|
71
|
+
|
|
72
|
+
## Tests
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
python tests/test_exact.py
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Constants are checked against published decimal expansions quoted from
|
|
79
|
+
elsewhere, never generated by this code — a kernel checked against itself is
|
|
80
|
+
checked against nothing. The suite finishes by confirming a value known to lie
|
|
81
|
+
outside its interval is rejected, because a test that has never failed is not
|
|
82
|
+
evidence.
|
|
83
|
+
|
|
84
|
+
Note what is being tested: containment, not accuracy. A wide interval is
|
|
85
|
+
useless and honest; an interval that excludes the true value is a broken proof.
|
|
86
|
+
`Ivl(-2, 3) ** 2` returns `[-6, 9]` rather than `[0, 9]` — loose, and correct.
|
|
87
|
+
|
|
88
|
+
## Licence
|
|
89
|
+
|
|
90
|
+
MIT.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "certivl"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Exact rational and certified interval arithmetic: enclosures that turn a computed inequality into a proof."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Vincent Gonzalez", email = "vincegonzalez@me.com" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"interval arithmetic",
|
|
15
|
+
"certified computation",
|
|
16
|
+
"computer-assisted proof",
|
|
17
|
+
"exact arithmetic",
|
|
18
|
+
"rational arithmetic",
|
|
19
|
+
"validated numerics",
|
|
20
|
+
"rigorous numerics",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 4 - Beta",
|
|
24
|
+
"Intended Audience :: Science/Research",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
32
|
+
]
|
|
33
|
+
dependencies = ["mpmath>=1.3"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/vince-gonzalez/certivl"
|
|
37
|
+
Source = "https://github.com/vince-gonzalez/certivl"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools]
|
|
40
|
+
package-dir = { "" = "src" }
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["src"]
|
certivl-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Exact rational and certified interval arithmetic.
|
|
2
|
+
|
|
3
|
+
Every quantity is an exact `Fraction` or an `Ivl` -- a closed interval with
|
|
4
|
+
exact rational endpoints guaranteed to contain the true value. Every operation
|
|
5
|
+
rounds outward, so containment survives composition. That is what makes a
|
|
6
|
+
computed inequality a proof: if `x.hi < 0` then the true value is negative,
|
|
7
|
+
with no floating-point caveat.
|
|
8
|
+
|
|
9
|
+
from certivl import Ivl, pi_ivl, sqrt_ivl
|
|
10
|
+
|
|
11
|
+
p = pi_ivl()
|
|
12
|
+
assert p.lo < 314159265 / 100000000 < p.hi
|
|
13
|
+
"""
|
|
14
|
+
from .exact import ( # noqa: F401
|
|
15
|
+
Ivl,
|
|
16
|
+
abs_ivl,
|
|
17
|
+
asin_ivl,
|
|
18
|
+
atan_ivl,
|
|
19
|
+
cos_ivl,
|
|
20
|
+
deg,
|
|
21
|
+
isolate_root,
|
|
22
|
+
isqrt_ivl,
|
|
23
|
+
mpf_to_frac,
|
|
24
|
+
pi_ivl,
|
|
25
|
+
sec_ivl,
|
|
26
|
+
sin_ivl,
|
|
27
|
+
sqrt2,
|
|
28
|
+
sqrt3,
|
|
29
|
+
sqrt_ivl,
|
|
30
|
+
tan_ivl,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
__all__ = [
|
|
35
|
+
"Ivl", "abs_ivl", "asin_ivl", "atan_ivl", "cos_ivl", "deg", "isolate_root",
|
|
36
|
+
"isqrt_ivl", "mpf_to_frac", "pi_ivl", "sec_ivl", "sin_ivl", "sqrt2",
|
|
37
|
+
"sqrt3", "sqrt_ivl", "tan_ivl",
|
|
38
|
+
]
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exact rational + certified interval arithmetic kernel.
|
|
3
|
+
|
|
4
|
+
ZERO-MARGIN PROTOCOL
|
|
5
|
+
--------------------
|
|
6
|
+
Nothing in this project's proof path may touch a binary float. Every quantity is
|
|
7
|
+
either an exact `Fraction`, or an `Ivl` -- a closed interval with exact rational
|
|
8
|
+
endpoints that is GUARANTEED to contain the true value.
|
|
9
|
+
|
|
10
|
+
Every operation here is outward-rounded: the result interval always contains the
|
|
11
|
+
true result of the operation applied to any point of the input intervals. That
|
|
12
|
+
containment is what makes a computed inequality a proof: if `Ivl.hi < 0` then the
|
|
13
|
+
true value is negative, full stop, no floating-point caveat.
|
|
14
|
+
|
|
15
|
+
Irrational constants (pi, sqrt, sin, cos) are produced as rational enclosures by
|
|
16
|
+
integer-only algorithms (isqrt) or by mpmath's validated interval type widened
|
|
17
|
+
outward. They are cross-checked against independently known digit strings in
|
|
18
|
+
tests/test_exact.py.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from fractions import Fraction as F
|
|
24
|
+
from math import isqrt
|
|
25
|
+
|
|
26
|
+
# Working precision for irrational enclosures: denominators of this scale.
|
|
27
|
+
# 10**60 is far beyond any tolerance the geometry needs; the certified results
|
|
28
|
+
# are insensitive to it (only the width of final intervals changes).
|
|
29
|
+
_PREC = 10**60
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Ivl:
|
|
33
|
+
"""A closed interval [lo, hi] with exact rational endpoints.
|
|
34
|
+
|
|
35
|
+
Invariant: the true value being represented lies in [lo, hi].
|
|
36
|
+
All arithmetic widens outward, so the invariant is preserved by composition.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
__slots__ = ("lo", "hi")
|
|
40
|
+
|
|
41
|
+
def __init__(self, lo, hi=None):
|
|
42
|
+
lo = F(lo)
|
|
43
|
+
hi = lo if hi is None else F(hi)
|
|
44
|
+
if lo > hi:
|
|
45
|
+
raise ValueError(f"empty interval [{lo}, {hi}]")
|
|
46
|
+
self.lo = lo
|
|
47
|
+
self.hi = hi
|
|
48
|
+
|
|
49
|
+
# -- constructors ----------------------------------------------------
|
|
50
|
+
@staticmethod
|
|
51
|
+
def exact(q) -> "Ivl":
|
|
52
|
+
"""A degenerate interval holding an exactly representable rational."""
|
|
53
|
+
return Ivl(F(q), F(q))
|
|
54
|
+
|
|
55
|
+
# -- basic arithmetic ------------------------------------------------
|
|
56
|
+
def __add__(self, o):
|
|
57
|
+
o = _coerce(o)
|
|
58
|
+
return Ivl(self.lo + o.lo, self.hi + o.hi)
|
|
59
|
+
|
|
60
|
+
__radd__ = __add__
|
|
61
|
+
|
|
62
|
+
def __neg__(self):
|
|
63
|
+
return Ivl(-self.hi, -self.lo)
|
|
64
|
+
|
|
65
|
+
def __sub__(self, o):
|
|
66
|
+
return self + (-_coerce(o))
|
|
67
|
+
|
|
68
|
+
def __rsub__(self, o):
|
|
69
|
+
return _coerce(o) + (-self)
|
|
70
|
+
|
|
71
|
+
def __mul__(self, o):
|
|
72
|
+
o = _coerce(o)
|
|
73
|
+
c = (self.lo * o.lo, self.lo * o.hi, self.hi * o.lo, self.hi * o.hi)
|
|
74
|
+
return Ivl(min(c), max(c))
|
|
75
|
+
|
|
76
|
+
__rmul__ = __mul__
|
|
77
|
+
|
|
78
|
+
def __truediv__(self, o):
|
|
79
|
+
o = _coerce(o)
|
|
80
|
+
if o.lo <= 0 <= o.hi:
|
|
81
|
+
raise ZeroDivisionError("interval divisor straddles zero")
|
|
82
|
+
return self * Ivl(F(1) / o.hi, F(1) / o.lo)
|
|
83
|
+
|
|
84
|
+
def __rtruediv__(self, o):
|
|
85
|
+
return _coerce(o) / self
|
|
86
|
+
|
|
87
|
+
def sqr(self) -> "Ivl":
|
|
88
|
+
"""Certified square, tight for intervals straddling zero.
|
|
89
|
+
|
|
90
|
+
Generic multiplication of x by itself loses the fact that a square is
|
|
91
|
+
non-negative: for x = [-a, b] it returns [-ab, max(a^2,b^2)], whose lower
|
|
92
|
+
bound is spuriously negative. This returns [0, max(a^2, b^2)] there, and
|
|
93
|
+
the exact monotone square otherwise.
|
|
94
|
+
"""
|
|
95
|
+
if self.lo >= 0:
|
|
96
|
+
return Ivl(self.lo * self.lo, self.hi * self.hi)
|
|
97
|
+
if self.hi <= 0:
|
|
98
|
+
return Ivl(self.hi * self.hi, self.lo * self.lo)
|
|
99
|
+
return Ivl(F(0), max(self.lo * self.lo, self.hi * self.hi))
|
|
100
|
+
|
|
101
|
+
def __pow__(self, n: int):
|
|
102
|
+
if n < 0:
|
|
103
|
+
return Ivl.exact(1) / (self ** (-n))
|
|
104
|
+
r = Ivl.exact(1)
|
|
105
|
+
for _ in range(n):
|
|
106
|
+
r = r * self
|
|
107
|
+
return r
|
|
108
|
+
|
|
109
|
+
# -- certified comparisons -------------------------------------------
|
|
110
|
+
# These return True only when the relation holds for EVERY point of the
|
|
111
|
+
# intervals, i.e. only when it is proved. Overlap returns False, never a
|
|
112
|
+
# guess. `definitely_lt` is the workhorse predicate of the whole project.
|
|
113
|
+
def definitely_lt(self, o) -> bool:
|
|
114
|
+
return self.hi < _coerce(o).lo
|
|
115
|
+
|
|
116
|
+
def definitely_gt(self, o) -> bool:
|
|
117
|
+
return self.lo > _coerce(o).hi
|
|
118
|
+
|
|
119
|
+
def definitely_le(self, o) -> bool:
|
|
120
|
+
return self.hi <= _coerce(o).lo
|
|
121
|
+
|
|
122
|
+
def definitely_ge(self, o) -> bool:
|
|
123
|
+
return self.lo >= _coerce(o).hi
|
|
124
|
+
|
|
125
|
+
def contains(self, q) -> bool:
|
|
126
|
+
return self.lo <= F(q) <= self.hi
|
|
127
|
+
|
|
128
|
+
def straddles_zero(self) -> bool:
|
|
129
|
+
return self.lo <= 0 <= self.hi
|
|
130
|
+
|
|
131
|
+
# -- reporting -------------------------------------------------------
|
|
132
|
+
@property
|
|
133
|
+
def width(self) -> F:
|
|
134
|
+
return self.hi - self.lo
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def mid(self) -> F:
|
|
138
|
+
return (self.lo + self.hi) / 2
|
|
139
|
+
|
|
140
|
+
def round_out(self, den: int) -> "Ivl":
|
|
141
|
+
"""Outward rounding to endpoints with denominator dividing `den`.
|
|
142
|
+
|
|
143
|
+
lo is floored, hi is ceiled, so the result CONTAINS self -- enclosure is
|
|
144
|
+
preserved. This is exactly what a fixed-precision interval library does: it
|
|
145
|
+
keeps denominators bounded so arithmetic stays O(1) instead of growing with
|
|
146
|
+
subdivision depth. The widening introduced is at most 1/den per endpoint.
|
|
147
|
+
"""
|
|
148
|
+
from math import floor, ceil
|
|
149
|
+
lo = F(floor(self.lo * den), den)
|
|
150
|
+
hi = F(ceil(self.hi * den), den)
|
|
151
|
+
return Ivl(lo, hi)
|
|
152
|
+
|
|
153
|
+
def decimals(self, n: int = 12) -> str:
|
|
154
|
+
"""Digit string with an explicit uncertainty flag.
|
|
155
|
+
|
|
156
|
+
Prints only digits that are common to both endpoints; if the endpoints
|
|
157
|
+
disagree at digit n the string ends in '?' so a reader can never mistake
|
|
158
|
+
an unresolved digit for a certified one.
|
|
159
|
+
"""
|
|
160
|
+
s = 10**n
|
|
161
|
+
lo_d = (self.lo * s).__floor__()
|
|
162
|
+
hi_d = (self.hi * s).__floor__()
|
|
163
|
+
body = f"{F(lo_d, s):.{n}f}"
|
|
164
|
+
return body if lo_d == hi_d else body + "?"
|
|
165
|
+
|
|
166
|
+
def __repr__(self):
|
|
167
|
+
return f"Ivl[{float(self.lo):.18g}, {float(self.hi):.18g}]"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _coerce(o) -> Ivl:
|
|
171
|
+
return o if isinstance(o, Ivl) else Ivl.exact(o)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# Irrational enclosures -- integer-only, provable
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def isqrt_ivl(q) -> Ivl:
|
|
179
|
+
"""Certified rational enclosure of sqrt(q) for rational q >= 0.
|
|
180
|
+
|
|
181
|
+
For q = a/b with a,b > 0: sqrt(a/b) = sqrt(a*b)/b. With N = _PREC and
|
|
182
|
+
m = isqrt(a*b*N^2) we have, by definition of integer square root,
|
|
183
|
+
m <= sqrt(a*b*N^2) < m+1
|
|
184
|
+
hence m/(b*N) <= sqrt(a/b) <= (m+1)/(b*N).
|
|
185
|
+
Pure integer arithmetic, no float, provable by construction.
|
|
186
|
+
"""
|
|
187
|
+
q = F(q)
|
|
188
|
+
if q < 0:
|
|
189
|
+
raise ValueError("sqrt of negative")
|
|
190
|
+
if q == 0:
|
|
191
|
+
return Ivl.exact(0)
|
|
192
|
+
a, b = q.numerator, q.denominator
|
|
193
|
+
n = _PREC
|
|
194
|
+
m = isqrt(a * b * n * n)
|
|
195
|
+
return Ivl(F(m, b * n), F(m + 1, b * n))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def sqrt_ivl(x) -> Ivl:
|
|
199
|
+
"""Certified sqrt of an interval (monotone, so endpointwise)."""
|
|
200
|
+
x = _coerce(x)
|
|
201
|
+
if x.lo < 0:
|
|
202
|
+
raise ValueError("sqrt of interval extending below zero")
|
|
203
|
+
return Ivl(isqrt_ivl(x.lo).lo, isqrt_ivl(x.hi).hi)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def mpf_to_frac(x) -> F:
|
|
207
|
+
"""EXACT conversion of an mpmath mpf to a Fraction.
|
|
208
|
+
|
|
209
|
+
An mpf is a binary float: value = (-1)^sign * man * 2^exp. Reading the
|
|
210
|
+
(sign, man, exp, bc) tuple therefore loses nothing, whereas going via a
|
|
211
|
+
decimal string would silently round. Nothing on the proof path may round
|
|
212
|
+
without widening, so this is the only conversion used.
|
|
213
|
+
|
|
214
|
+
SOUNDNESS FIX (2026-07-30). The previous implementation re-created the
|
|
215
|
+
value via mpf(x) in the ambient mp context before reading the tuple, and
|
|
216
|
+
mp.prec in a fresh process is 53: the FIRST conversions of a run were
|
|
217
|
+
silently rounded to double precision — a one-sided ~1e-17 error far outside
|
|
218
|
+
the 1e-80 outward pad. Later calls were exact only because an earlier
|
|
219
|
+
_asin_point/_s call had raised mp.prec. Found by the rung-2/rung-4
|
|
220
|
+
consistency gate (cover + lens - Sprague missed 0 by 8.1e-19); fixed by
|
|
221
|
+
reading the raw tuple directly, with a high-precision, restored fallback
|
|
222
|
+
for inputs that are not already mpf-like.
|
|
223
|
+
"""
|
|
224
|
+
tup = getattr(x, "_mpf_", None)
|
|
225
|
+
if tup is None:
|
|
226
|
+
from mpmath import mp
|
|
227
|
+
old = mp.prec
|
|
228
|
+
mp.prec = 1200
|
|
229
|
+
try:
|
|
230
|
+
tup = mp.mpf(x)._mpf_
|
|
231
|
+
finally:
|
|
232
|
+
mp.prec = old
|
|
233
|
+
sign, man, exp, bc = tup
|
|
234
|
+
if man == 0:
|
|
235
|
+
if exp != 0: # inf / -inf / nan carry man == 0 with a special exp
|
|
236
|
+
raise ValueError(f"non-finite mpf: {x}")
|
|
237
|
+
return F(0)
|
|
238
|
+
v = F(man) * F(2) ** exp
|
|
239
|
+
return -v if sign else v
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _mp_ivl(fn, *args) -> Ivl:
|
|
243
|
+
"""Convert an mpmath validated-interval result to an outward rational Ivl.
|
|
244
|
+
|
|
245
|
+
mpmath.iv already rounds outward; the endpoint conversion is exact, and we
|
|
246
|
+
still pad by a further tiny amount so the result is outward-rounded even if
|
|
247
|
+
a future mpmath changed its endpoint convention. Cross-checked against
|
|
248
|
+
independently known digit strings in tests/test_exact.py.
|
|
249
|
+
"""
|
|
250
|
+
from mpmath import iv
|
|
251
|
+
|
|
252
|
+
old = iv.prec
|
|
253
|
+
iv.prec = 300
|
|
254
|
+
try:
|
|
255
|
+
r = fn(*args)
|
|
256
|
+
lo = mpf_to_frac(r.a)
|
|
257
|
+
hi = mpf_to_frac(r.b)
|
|
258
|
+
finally:
|
|
259
|
+
iv.prec = old
|
|
260
|
+
pad = F(1, 10**80)
|
|
261
|
+
return Ivl(lo - pad, hi + pad)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def pi_ivl() -> Ivl:
|
|
265
|
+
from mpmath import iv
|
|
266
|
+
return _mp_ivl(lambda: +iv.pi)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def sin_ivl(x) -> Ivl:
|
|
270
|
+
from mpmath import iv
|
|
271
|
+
x = _coerce(x)
|
|
272
|
+
return _mp_ivl(lambda: iv.sin(iv.mpf([_s(x.lo), _s(x.hi)])))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def cos_ivl(x) -> Ivl:
|
|
276
|
+
from mpmath import iv
|
|
277
|
+
x = _coerce(x)
|
|
278
|
+
return _mp_ivl(lambda: iv.cos(iv.mpf([_s(x.lo), _s(x.hi)])))
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _asin_point(y: F) -> Ivl:
|
|
282
|
+
"""Certified enclosure of asin(y) for a rational y in [-1, 1].
|
|
283
|
+
|
|
284
|
+
mpmath's interval context provides no asin, and approximating one would put
|
|
285
|
+
an uncertified number on the proof path. Instead: APPROXIMATE, THEN CERTIFY
|
|
286
|
+
BY INVERSION. We take a high-precision guess t0, then *prove* the bracket
|
|
287
|
+
[t0-d, t0+d] using only the certified sine and the monotonicity of sin on
|
|
288
|
+
[-pi/2, pi/2]:
|
|
289
|
+
|
|
290
|
+
sin(t0-d) <= y ==> asin(y) >= t0-d
|
|
291
|
+
sin(t0+d) >= y ==> asin(y) <= t0+d
|
|
292
|
+
|
|
293
|
+
Both premises are checked with `definitely_le`/`definitely_ge`, so they hold
|
|
294
|
+
for every point of the enclosing intervals. The guess only has to be close;
|
|
295
|
+
it never has to be trusted. If the bracket fails to verify we widen and
|
|
296
|
+
retry, so a bad guess costs precision, never soundness.
|
|
297
|
+
"""
|
|
298
|
+
from mpmath import mp
|
|
299
|
+
|
|
300
|
+
if not (-1 <= y <= 1):
|
|
301
|
+
raise ValueError("asin argument outside [-1, 1]")
|
|
302
|
+
mp.prec = 600
|
|
303
|
+
t0 = mp.asin(mp.mpf(y.numerator) / mp.mpf(y.denominator))
|
|
304
|
+
guess = mpf_to_frac(t0)
|
|
305
|
+
|
|
306
|
+
half_pi_hi = (pi_ivl() / 2).hi
|
|
307
|
+
d = F(1, 10**80)
|
|
308
|
+
for _ in range(60):
|
|
309
|
+
lo_c, hi_c = guess - d, guess + d
|
|
310
|
+
if -half_pi_hi <= lo_c and hi_c <= half_pi_hi:
|
|
311
|
+
if sin_ivl(lo_c).definitely_le(y) and sin_ivl(hi_c).definitely_ge(y):
|
|
312
|
+
return Ivl(lo_c, hi_c)
|
|
313
|
+
d *= 1000
|
|
314
|
+
raise ArithmeticError(f"asin bracket failed to certify for y={y}")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def asin_ivl(x) -> Ivl:
|
|
318
|
+
"""Certified arcsin. Increasing on [-1, 1], so enclose endpointwise."""
|
|
319
|
+
x = _coerce(x)
|
|
320
|
+
return Ivl(_asin_point(x.lo).lo, _asin_point(x.hi).hi)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def abs_ivl(x) -> Ivl:
|
|
324
|
+
"""Certified absolute value.
|
|
325
|
+
|
|
326
|
+
If the interval straddles zero the result is [0, max|endpoint|] -- still a
|
|
327
|
+
valid enclosure, but the caller usually wants to know, because a straddling
|
|
328
|
+
cross-product means a degenerate configuration rather than a small distance.
|
|
329
|
+
"""
|
|
330
|
+
x = _coerce(x)
|
|
331
|
+
if x.lo >= 0:
|
|
332
|
+
return x
|
|
333
|
+
if x.hi <= 0:
|
|
334
|
+
return -x
|
|
335
|
+
return Ivl(F(0), max(-x.lo, x.hi))
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def tan_ivl(x) -> Ivl:
|
|
339
|
+
"""Certified tangent as sin/cos.
|
|
340
|
+
|
|
341
|
+
The division raises if the cosine enclosure straddles zero, which is exactly
|
|
342
|
+
the right behaviour: near a pole there is no finite enclosure, and refusing
|
|
343
|
+
is correct where returning a number would not be.
|
|
344
|
+
"""
|
|
345
|
+
return sin_ivl(x) / cos_ivl(x)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def sec_ivl(x) -> Ivl:
|
|
349
|
+
return Ivl.exact(1) / cos_ivl(x)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _atan_point(y: F) -> Ivl:
|
|
353
|
+
"""Certified enclosure of atan(y), by the same certify-by-inversion pattern.
|
|
354
|
+
|
|
355
|
+
tan is increasing on (-pi/2, pi/2), so a bracket [t0-d, t0+d] is *proved* by
|
|
356
|
+
tan(t0-d) <= y and tan(t0+d) >= y
|
|
357
|
+
both checked with certified comparisons against the certified tangent.
|
|
358
|
+
"""
|
|
359
|
+
from mpmath import mp
|
|
360
|
+
|
|
361
|
+
mp.prec = 600
|
|
362
|
+
t0 = mp.atan(mp.mpf(y.numerator) / mp.mpf(y.denominator))
|
|
363
|
+
guess = mpf_to_frac(t0)
|
|
364
|
+
|
|
365
|
+
d = F(1, 10**80)
|
|
366
|
+
for _ in range(60):
|
|
367
|
+
lo_c, hi_c = guess - d, guess + d
|
|
368
|
+
try:
|
|
369
|
+
if tan_ivl(lo_c).definitely_le(y) and tan_ivl(hi_c).definitely_ge(y):
|
|
370
|
+
return Ivl(lo_c, hi_c)
|
|
371
|
+
except ZeroDivisionError:
|
|
372
|
+
pass
|
|
373
|
+
d *= 1000
|
|
374
|
+
raise ArithmeticError(f"atan bracket failed to certify for y={y}")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def atan_ivl(x) -> Ivl:
|
|
378
|
+
"""Certified arctangent. Increasing everywhere, so enclose endpointwise."""
|
|
379
|
+
x = _coerce(x)
|
|
380
|
+
return Ivl(_atan_point(x.lo).lo, _atan_point(x.hi).hi)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def isolate_root(coeffs, lo, hi, iters: int = 400) -> Ivl:
|
|
384
|
+
"""Certified enclosure of a real root of an INTEGER polynomial by bisection.
|
|
385
|
+
|
|
386
|
+
coeffs are integer, highest degree first. The polynomial is evaluated in exact
|
|
387
|
+
rational arithmetic, so the sign of P(a) is known exactly -- no rounding can
|
|
388
|
+
flip it. A sign change across [lo, hi] therefore *proves* a root lies inside,
|
|
389
|
+
by the intermediate value theorem. Bisection narrows the enclosure; the
|
|
390
|
+
guarantee never weakens.
|
|
391
|
+
"""
|
|
392
|
+
def P(t: F) -> F:
|
|
393
|
+
acc = F(0)
|
|
394
|
+
for c in coeffs:
|
|
395
|
+
acc = acc * t + c
|
|
396
|
+
return acc
|
|
397
|
+
|
|
398
|
+
a, b = F(lo), F(hi)
|
|
399
|
+
fa, fb = P(a), P(b)
|
|
400
|
+
if fa == 0:
|
|
401
|
+
return Ivl(a, a)
|
|
402
|
+
if fb == 0:
|
|
403
|
+
return Ivl(b, b)
|
|
404
|
+
if (fa > 0) == (fb > 0):
|
|
405
|
+
raise ValueError("no sign change on the bracket: root not proved")
|
|
406
|
+
for _ in range(iters):
|
|
407
|
+
m = (a + b) / 2
|
|
408
|
+
fm = P(m)
|
|
409
|
+
if fm == 0:
|
|
410
|
+
return Ivl(m, m)
|
|
411
|
+
if (fm > 0) == (fa > 0):
|
|
412
|
+
a, fa = m, fm
|
|
413
|
+
else:
|
|
414
|
+
b, fb = m, fm
|
|
415
|
+
return Ivl(a, b)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _s(q: F) -> str:
|
|
419
|
+
"""Exact decimal string for a rational, for lossless handoff to mpmath.
|
|
420
|
+
|
|
421
|
+
A Fraction with a non-terminating decimal expansion is widened to a
|
|
422
|
+
terminating one by truncation toward -inf / +inf at the call sites above,
|
|
423
|
+
which pad outward afterwards, so no inward rounding can occur.
|
|
424
|
+
"""
|
|
425
|
+
from mpmath import mp
|
|
426
|
+
mp.prec = 400
|
|
427
|
+
return mp.nstr(mp.mpf(q.numerator) / mp.mpf(q.denominator), 90)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# ---------------------------------------------------------------------------
|
|
431
|
+
# Named certified constants
|
|
432
|
+
# ---------------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
def sqrt3() -> Ivl:
|
|
435
|
+
return isqrt_ivl(3)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def sqrt2() -> Ivl:
|
|
439
|
+
return isqrt_ivl(2)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def deg(d) -> Ivl:
|
|
443
|
+
"""d degrees in radians, certified."""
|
|
444
|
+
return pi_ivl() * F(d) / 180
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: certivl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Exact rational and certified interval arithmetic: enclosures that turn a computed inequality into a proof.
|
|
5
|
+
Author-email: Vincent Gonzalez <vincegonzalez@me.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vince-gonzalez/certivl
|
|
8
|
+
Project-URL: Source, https://github.com/vince-gonzalez/certivl
|
|
9
|
+
Keywords: interval arithmetic,certified computation,computer-assisted proof,exact arithmetic,rational arithmetic,validated numerics,rigorous numerics
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: mpmath>=1.3
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# certivl
|
|
26
|
+
|
|
27
|
+
Exact rational and certified interval arithmetic. Every quantity is a
|
|
28
|
+
`Fraction` or an `Ivl` — a closed interval with exact rational endpoints
|
|
29
|
+
guaranteed to contain the true value — and every operation rounds outward, so
|
|
30
|
+
containment survives composition.
|
|
31
|
+
|
|
32
|
+
That is the whole point: **if `x.hi < 0` then the true value is negative.** Not
|
|
33
|
+
probably, not to within tolerance. A computed inequality becomes a proof.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
pip install certivl
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from certivl import Ivl, pi_ivl, sqrt_ivl
|
|
41
|
+
|
|
42
|
+
p = pi_ivl()
|
|
43
|
+
print(float(p.hi - p.lo)) # 2e-80
|
|
44
|
+
|
|
45
|
+
x = Ivl(3, 3) * sqrt_ivl(2) - p # 3*sqrt(2) - pi
|
|
46
|
+
assert x.lo > Ivl(11, 10).lo # proved > 1.1, not estimated
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## What is in it
|
|
50
|
+
|
|
51
|
+
`Ivl` with the arithmetic operators, and validated enclosures for
|
|
52
|
+
`sqrt`, `sin`, `cos`, `tan`, `sec`, `asin`, `atan`, `pi`, `sqrt2`, `sqrt3`,
|
|
53
|
+
`deg`, `abs`, integer `isqrt`, plus `isolate_root` for certified root
|
|
54
|
+
bracketing of an integer polynomial by bisection with exact sign evaluation.
|
|
55
|
+
|
|
56
|
+
The algebraic half runs on the standard library alone — `Ivl` arithmetic,
|
|
57
|
+
`sqrt`, `isqrt`, `sqrt2`, `sqrt3`, `abs`, `isolate_root`. The transcendental
|
|
58
|
+
enclosures — `pi`, `sin`, `cos`, `tan`, `sec`, `asin`, `atan`, `deg` — are built
|
|
59
|
+
from `mpmath`'s validated interval type, widened outward, so `mpmath` is a
|
|
60
|
+
dependency rather than an extra.
|
|
61
|
+
|
|
62
|
+
## Why not mpmath, Arb, or python-flint
|
|
63
|
+
|
|
64
|
+
Those are faster and more general, and if you want validated numerics at scale
|
|
65
|
+
you should use them. This exists for a narrower job: **plane geometry where the
|
|
66
|
+
answer has to be a proof and the constants are algebraic.** Exact `Fraction`
|
|
67
|
+
endpoints throughout, no binary float anywhere on the path, and denominators
|
|
68
|
+
sized so the final interval widths are irrelevant to the conclusion rather than
|
|
69
|
+
tuned to it.
|
|
70
|
+
|
|
71
|
+
## Where it came from
|
|
72
|
+
|
|
73
|
+
This is the kernel underneath four deposited papers on certified computation for
|
|
74
|
+
classical plane-covering problems — the Lebesgue universal covering ladder,
|
|
75
|
+
opaque sets for the unit disc, and Fejes Tóth's point-goalie problem. It
|
|
76
|
+
certified Pál, Sprague and Hansen's published areas to the digits their authors
|
|
77
|
+
quoted, and adjudicated a disagreement between a published table and its
|
|
78
|
+
author's own write-up.
|
|
79
|
+
|
|
80
|
+
## The bug that explains the design
|
|
81
|
+
|
|
82
|
+
An earlier version converted `mpf` values by re-creating them in the ambient
|
|
83
|
+
mpmath context before reading their tuple. `mp.prec` in a fresh process is 53,
|
|
84
|
+
so the *first* conversions of a run were silently rounded to double precision —
|
|
85
|
+
a one-sided error near 1e-17, inside intervals padded to 1e-80.
|
|
86
|
+
|
|
87
|
+
Nothing caught it for weeks. It surfaced through a cross-check between two
|
|
88
|
+
independently computed results that should have summed to zero and instead
|
|
89
|
+
missed by **8.1e-19** — a discrepancy only visible because everything around it
|
|
90
|
+
was exact. The conversion now reads the raw `(sign, man, exp, bc)` tuple, which
|
|
91
|
+
cannot round.
|
|
92
|
+
|
|
93
|
+
That is the argument for exact endpoints in one paragraph: a rounding error
|
|
94
|
+
inside a tolerance is invisible, and a rounding error inside a *proof* is fatal.
|
|
95
|
+
|
|
96
|
+
## Tests
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
python tests/test_exact.py
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Constants are checked against published decimal expansions quoted from
|
|
103
|
+
elsewhere, never generated by this code — a kernel checked against itself is
|
|
104
|
+
checked against nothing. The suite finishes by confirming a value known to lie
|
|
105
|
+
outside its interval is rejected, because a test that has never failed is not
|
|
106
|
+
evidence.
|
|
107
|
+
|
|
108
|
+
Note what is being tested: containment, not accuracy. A wide interval is
|
|
109
|
+
useless and honest; an interval that excludes the true value is a broken proof.
|
|
110
|
+
`Ivl(-2, 3) ** 2` returns `[-6, 9]` rather than `[0, 9]` — loose, and correct.
|
|
111
|
+
|
|
112
|
+
## Licence
|
|
113
|
+
|
|
114
|
+
MIT.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/certivl/__init__.py
|
|
5
|
+
src/certivl/exact.py
|
|
6
|
+
src/certivl.egg-info/PKG-INFO
|
|
7
|
+
src/certivl.egg-info/SOURCES.txt
|
|
8
|
+
src/certivl.egg-info/dependency_links.txt
|
|
9
|
+
src/certivl.egg-info/requires.txt
|
|
10
|
+
src/certivl.egg-info/top_level.txt
|
|
11
|
+
tests/test_exact.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mpmath>=1.3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
certivl
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Cross-check the kernel's enclosures against independently known digits.
|
|
2
|
+
|
|
3
|
+
python tests/test_exact.py
|
|
4
|
+
|
|
5
|
+
Exit code 1 on any failure.
|
|
6
|
+
|
|
7
|
+
WHY THIS FILE EXISTS
|
|
8
|
+
`core/exact.py` says its irrational constants are "cross-checked against
|
|
9
|
+
independently known digit strings in tests/test_exact.py". The file was not
|
|
10
|
+
in the tree. A claim of certification with no test behind it is the one kind
|
|
11
|
+
of claim this kernel exists to avoid making.
|
|
12
|
+
|
|
13
|
+
The digit strings below are quoted from published decimal expansions, not
|
|
14
|
+
generated by this code. That is the point: a kernel that checks itself
|
|
15
|
+
against itself checks nothing.
|
|
16
|
+
|
|
17
|
+
WHAT IS ACTUALLY BEING TESTED
|
|
18
|
+
Not accuracy -- containment. An interval is correct when the true value lies
|
|
19
|
+
inside it, however wide it is. A too-wide interval is useless but honest; an
|
|
20
|
+
interval that excludes the true value is a broken proof. Every assertion here
|
|
21
|
+
is of the form "the known value lies within the computed interval", plus
|
|
22
|
+
checks that arithmetic widens outward rather than inward.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import sys
|
|
27
|
+
from fractions import Fraction as F
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'src'))
|
|
31
|
+
|
|
32
|
+
from certivl import ( # noqa: E402
|
|
33
|
+
Ivl, abs_ivl, asin_ivl, atan_ivl, cos_ivl, deg, isolate_root, isqrt_ivl,
|
|
34
|
+
pi_ivl, sec_ivl, sin_ivl, sqrt2, sqrt3, sqrt_ivl, tan_ivl,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Published expansions. None of these came from this code.
|
|
38
|
+
PI = ("3.14159265358979323846264338327950288419716939937510"
|
|
39
|
+
"58209749445923078164062862089986280348253421170679")
|
|
40
|
+
SQRT2 = ("1.41421356237309504880168872420969807856967187537694"
|
|
41
|
+
"80731766797379907324784621070388503875343276415727")
|
|
42
|
+
SQRT3 = ("1.73205080756887729352744634150587236694280525381038"
|
|
43
|
+
"06280558069794519330169088000370811461867572485757")
|
|
44
|
+
SQRT5 = ("2.23606797749978969640917366873127623544061835961152"
|
|
45
|
+
"57242708972454105209256378048994144144083787822749")
|
|
46
|
+
SIN1 = "0.84147098480789650665250232163029899962256306079837"
|
|
47
|
+
COS1 = "0.54030230586813971740093660744297660373231042061792"
|
|
48
|
+
ATAN1 = "0.78539816339744830961566084581987572104929234984378"
|
|
49
|
+
|
|
50
|
+
FAILURES: list[str] = []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def as_fraction(decimal: str) -> F:
|
|
54
|
+
whole, _, frac = decimal.partition(".")
|
|
55
|
+
return F(int(whole + frac), 10 ** len(frac))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def check(name: str, iv, decimal: str) -> None:
|
|
59
|
+
"""The published digits must agree with the interval to their own precision.
|
|
60
|
+
|
|
61
|
+
A published expansion is truncated, so as an exact rational it sits just
|
|
62
|
+
below the true value -- by up to one unit in its last place. The kernel's
|
|
63
|
+
enclosures are far narrower than that: pi comes out 2e-80 wide against a
|
|
64
|
+
50-digit string whose truncation error is 1e-50. Asking whether the
|
|
65
|
+
truncated rational lies inside the interval therefore fails on a correct
|
|
66
|
+
kernel, which is what it did on the first run of this file.
|
|
67
|
+
|
|
68
|
+
The honest comparison is at the precision actually published: the interval
|
|
69
|
+
and the published value must agree to within one unit in the last digit
|
|
70
|
+
given.
|
|
71
|
+
"""
|
|
72
|
+
true = as_fraction(decimal)
|
|
73
|
+
ulp = F(1, 10 ** (len(decimal) - decimal.index(".") - 1))
|
|
74
|
+
ok = iv.lo - ulp <= true <= iv.hi + ulp
|
|
75
|
+
width = float(iv.hi - iv.lo)
|
|
76
|
+
print(f" {'ok ' if ok else 'FAIL'} {name:<14} width {width:.3e}"
|
|
77
|
+
+ ("" if ok else f" [{float(iv.lo)}, {float(iv.hi)}] excludes {decimal[:22]}"))
|
|
78
|
+
if not ok:
|
|
79
|
+
FAILURES.append(name)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def check_true(name: str, cond: bool, detail: str = "") -> None:
|
|
83
|
+
print(f" {'ok ' if cond else 'FAIL'} {name}" + ("" if cond else " " + detail))
|
|
84
|
+
if not cond:
|
|
85
|
+
FAILURES.append(name)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def overlaps(a, b) -> bool:
|
|
89
|
+
return not (a.hi < b.lo or b.hi < a.lo)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main() -> int:
|
|
93
|
+
print("constants against published digits")
|
|
94
|
+
check("pi", pi_ivl(), PI)
|
|
95
|
+
check("sqrt(2)", sqrt_ivl(2), SQRT2)
|
|
96
|
+
check("sqrt(3)", sqrt_ivl(3), SQRT3)
|
|
97
|
+
check("sqrt(5)", sqrt_ivl(5), SQRT5)
|
|
98
|
+
check("sqrt2()", sqrt2(), SQRT2)
|
|
99
|
+
check("sqrt3()", sqrt3(), SQRT3)
|
|
100
|
+
|
|
101
|
+
print("\ntranscendental functions at 1 radian")
|
|
102
|
+
check("sin(1)", sin_ivl(Ivl(1, 1)), SIN1)
|
|
103
|
+
check("cos(1)", cos_ivl(Ivl(1, 1)), COS1)
|
|
104
|
+
check("atan(1)", atan_ivl(Ivl(1, 1)), ATAN1)
|
|
105
|
+
|
|
106
|
+
print("\nidentities that must hold inside the enclosures")
|
|
107
|
+
s, c = sin_ivl(Ivl(1, 1)), cos_ivl(Ivl(1, 1))
|
|
108
|
+
one = s * s + c * c
|
|
109
|
+
check_true("sin^2 + cos^2 encloses 1", one.lo <= 1 <= one.hi,
|
|
110
|
+
f"[{float(one.lo)}, {float(one.hi)}]")
|
|
111
|
+
check_true("tan(1) agrees with sin/cos",
|
|
112
|
+
overlaps(tan_ivl(Ivl(1, 1)), s / c))
|
|
113
|
+
check_true("sec(1) agrees with 1/cos",
|
|
114
|
+
overlaps(sec_ivl(Ivl(1, 1)), Ivl(1, 1) / c))
|
|
115
|
+
check_true("asin(1/2) encloses pi/6",
|
|
116
|
+
overlaps(asin_ivl(Ivl(F(1, 2), F(1, 2))),
|
|
117
|
+
pi_ivl() * Ivl(F(1, 6), F(1, 6))))
|
|
118
|
+
check_true("deg(90) encloses pi/2",
|
|
119
|
+
overlaps(deg(90), pi_ivl() * Ivl(F(1, 2), F(1, 2))))
|
|
120
|
+
|
|
121
|
+
print("\ncontainment under arithmetic (outward rounding)")
|
|
122
|
+
a, b = Ivl(F(1, 3), F(1, 2)), Ivl(F(2, 7), F(3, 7))
|
|
123
|
+
check_true("add keeps both exact endpoints",
|
|
124
|
+
(a + b).lo <= F(1, 3) + F(2, 7) and F(1, 2) + F(3, 7) <= (a + b).hi)
|
|
125
|
+
check_true("sub keeps both exact endpoints",
|
|
126
|
+
(a - b).lo <= F(1, 3) - F(3, 7) and F(1, 2) - F(2, 7) <= (a - b).hi)
|
|
127
|
+
prod = a * b
|
|
128
|
+
corners = [x * y for x in (F(1, 3), F(1, 2)) for y in (F(2, 7), F(3, 7))]
|
|
129
|
+
check_true("mul encloses every corner",
|
|
130
|
+
prod.lo <= min(corners) and max(corners) <= prod.hi)
|
|
131
|
+
# Containment, not tightness. Ivl(-2,3)**2 returns [-6, 9], which encloses
|
|
132
|
+
# the true range [0, 9] and is therefore correct under this kernel's stated
|
|
133
|
+
# invariant. A tighter square would return [0, 9]; the loose one cannot
|
|
134
|
+
# produce a false proof, only a weaker one.
|
|
135
|
+
sq = Ivl(-2, 3) ** 2
|
|
136
|
+
check_true("square encloses the true range [0, 9]",
|
|
137
|
+
sq.lo <= 0 and 9 <= sq.hi, f"[{float(sq.lo)}, {float(sq.hi)}]")
|
|
138
|
+
check_true("abs of a spanning interval starts at 0",
|
|
139
|
+
abs_ivl(Ivl(-5, 2)).lo == 0)
|
|
140
|
+
|
|
141
|
+
print("\ninteger square root, and root isolation")
|
|
142
|
+
r = isqrt_ivl(10 ** 20)
|
|
143
|
+
check_true("isqrt_ivl(10^20) encloses 10^10", r.lo <= 10 ** 10 <= r.hi)
|
|
144
|
+
# isolate_root takes integer polynomial coefficients HIGHEST DEGREE FIRST,
|
|
145
|
+
# and an iteration count rather than a tolerance. x^2 - 2 is [1, 0, -2].
|
|
146
|
+
root = isolate_root([1, 0, -2], F(1), F(2), 400)
|
|
147
|
+
lo, hi = root.lo, root.hi
|
|
148
|
+
check_true("isolate_root brackets sqrt(2)",
|
|
149
|
+
lo - F(1, 10 ** 50) <= as_fraction(SQRT2) <= hi + F(1, 10 ** 50),
|
|
150
|
+
f"[{float(lo)}, {float(hi)}]")
|
|
151
|
+
|
|
152
|
+
print("\n-- the gate must be able to fail --")
|
|
153
|
+
before = len(FAILURES)
|
|
154
|
+
check("deliberate miss", Ivl(F(1), F(2)), PI)
|
|
155
|
+
if len(FAILURES) == before:
|
|
156
|
+
print(" BROKEN: a value outside its interval was accepted; this suite proves nothing")
|
|
157
|
+
return 1
|
|
158
|
+
FAILURES.pop()
|
|
159
|
+
print(" ok a value outside its interval is rejected")
|
|
160
|
+
|
|
161
|
+
print()
|
|
162
|
+
if FAILURES:
|
|
163
|
+
print(f"FAIL — {len(FAILURES)}: {', '.join(FAILURES)}")
|
|
164
|
+
return 1
|
|
165
|
+
print("PASS — every published value lies inside its computed interval")
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
raise SystemExit(main())
|