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