funcprops 0.1.0__py3-none-any.whl
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.
- funcprops/__init__.py +234 -0
- funcprops/_semialg.py +246 -0
- funcprops/api.py +70 -0
- funcprops/branchcuts/__init__.py +141 -0
- funcprops/branchcuts/computation.py +664 -0
- funcprops/branchcuts/core.py +1204 -0
- funcprops/branchcuts/post_simplify.py +461 -0
- funcprops/conditions.py +124 -0
- funcprops/data/catalogue.json +127449 -0
- funcprops/data/dlmf_audit.json +3382 -0
- funcprops/dlmf.py +29 -0
- funcprops/engine.py +324 -0
- funcprops/exceptions.py +29 -0
- funcprops/proof_cache.py +373 -0
- funcprops/real_analysis/__init__.py +52 -0
- funcprops/real_analysis/advanced_convexity.py +217 -0
- funcprops/real_analysis/boundedness.py +168 -0
- funcprops/real_analysis/common.py +244 -0
- funcprops/real_analysis/convexity.py +698 -0
- funcprops/real_analysis/differentiability.py +112 -0
- funcprops/real_analysis/domain.py +188 -0
- funcprops/real_analysis/mapping.py +374 -0
- funcprops/real_analysis/monotonic_certificates.py +867 -0
- funcprops/real_analysis/monotonicity.py +673 -0
- funcprops/real_analysis/oscillation.py +151 -0
- funcprops/real_analysis/piecewise.py +174 -0
- funcprops/real_analysis/range.py +552 -0
- funcprops/real_analysis/sign.py +561 -0
- funcprops/real_analysis/smoothness.py +268 -0
- funcprops/real_analysis/special_functions.py +515 -0
- funcprops/real_analysis/structural_properties.py +558 -0
- funcprops/real_analysis/zeros.py +327 -0
- funcprops/registry.py +274 -0
- funcprops/results.py +239 -0
- funcprops/singularities.py +1767 -0
- funcprops/stratification.py +494 -0
- funcprops/sympy_compare.py +63 -0
- funcprops/transcendental/__init__.py +140 -0
- funcprops/transcendental/algebraize.py +201 -0
- funcprops/transcendental/analytic.py +261 -0
- funcprops/transcendental/complex_roots.py +542 -0
- funcprops/transcendental/exp_fourier.py +221 -0
- funcprops/transcendental/extrema.py +821 -0
- funcprops/transcendental/periodic.py +222 -0
- funcprops/transcendental/proof.py +104 -0
- funcprops/transcendental/real_interval.py +128 -0
- funcprops/transcendental/real_sign.py +117 -0
- funcprops/transcendental/reduce.py +288 -0
- funcprops/transcendental/roots.py +1380 -0
- funcprops/transcendental/zero_geometry.py +1197 -0
- funcprops/verification.py +410 -0
- funcprops-0.1.0.dist-info/METADATA +208 -0
- funcprops-0.1.0.dist-info/RECORD +56 -0
- funcprops-0.1.0.dist-info/WHEEL +5 -0
- funcprops-0.1.0.dist-info/licenses/LICENSE +674 -0
- funcprops-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1380 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import math
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import sympy as sp
|
|
8
|
+
from sympy import S
|
|
9
|
+
|
|
10
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
11
|
+
from .analytic import analytic_neighborhood
|
|
12
|
+
from .periodic import PeriodicSet
|
|
13
|
+
from .real_sign import robust_real_sign
|
|
14
|
+
from .zero_geometry import ZeroGeometryCertificate, zero_geometry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class RootCertificate:
|
|
19
|
+
domain: object
|
|
20
|
+
root_count: int | None
|
|
21
|
+
method: str
|
|
22
|
+
proof: tuple[object, ...] = ()
|
|
23
|
+
complete: bool = True
|
|
24
|
+
multiplicity: int | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class CertifiedRootInterval:
|
|
29
|
+
interval: sp.Interval
|
|
30
|
+
certificate: RootCertificate
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ParametricZeroFamily:
|
|
35
|
+
count: sp.Expr
|
|
36
|
+
support: sp.Set
|
|
37
|
+
guard: sp.Expr = S.true
|
|
38
|
+
simple: bool | None = None
|
|
39
|
+
interlaces_prev_degree: bool | None = None
|
|
40
|
+
interlacing: tuple[str, ...] = ()
|
|
41
|
+
source: str = ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class RootIsolationResult:
|
|
46
|
+
roots: tuple[sp.Expr, ...] = ()
|
|
47
|
+
zero_intervals: tuple[sp.Interval, ...] = ()
|
|
48
|
+
isolated_intervals: tuple[CertifiedRootInterval, ...] = ()
|
|
49
|
+
periodic: PeriodicSet | None = None
|
|
50
|
+
complete: bool = False
|
|
51
|
+
method: str = "unknown"
|
|
52
|
+
evidence: tuple[object, ...] = ()
|
|
53
|
+
certificates: tuple[RootCertificate, ...] = ()
|
|
54
|
+
parametric_families: tuple[ParametricZeroFamily, ...] = ()
|
|
55
|
+
geometry: tuple[ZeroGeometryCertificate, ...] = ()
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def certified(self):
|
|
59
|
+
return self.complete
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _sort_unique(vals):
|
|
63
|
+
out = []
|
|
64
|
+
for v in sorted((sp.simplify(q) for q in vals), key=sp.default_sort_key):
|
|
65
|
+
if not any(sp.simplify(v - u) == 0 for u in out):
|
|
66
|
+
out.append(v)
|
|
67
|
+
return tuple(out)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _affine_data(arg, var):
|
|
71
|
+
try:
|
|
72
|
+
p = sp.Poly(sp.expand(arg), var)
|
|
73
|
+
if p.degree() != 1:
|
|
74
|
+
return None
|
|
75
|
+
return p.nth(1), p.nth(0)
|
|
76
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _periodic(expr, var, domain):
|
|
81
|
+
if expr.func not in (sp.sin, sp.cos, sp.tan):
|
|
82
|
+
return None
|
|
83
|
+
ab = _affine_data(expr.args[0], var)
|
|
84
|
+
if ab is None:
|
|
85
|
+
return None
|
|
86
|
+
a, b = ab
|
|
87
|
+
if a == 0 or a.is_real is False:
|
|
88
|
+
return None
|
|
89
|
+
target = S.Zero if expr.func in (sp.sin, sp.tan) else sp.pi / 2
|
|
90
|
+
p = sp.simplify(sp.pi / sp.Abs(a))
|
|
91
|
+
if p.is_positive is not True:
|
|
92
|
+
return None
|
|
93
|
+
x0 = sp.simplify((target - b) / a)
|
|
94
|
+
base = sp.FiniteSet(sp.simplify(sp.Mod(x0, p)))
|
|
95
|
+
ps = PeriodicSet(base, p, var)
|
|
96
|
+
if domain is S.Reals:
|
|
97
|
+
c = RootCertificate(
|
|
98
|
+
S.Reals, None, "periodic_formula", (sp.Eq(expr, 0), ps), True
|
|
99
|
+
)
|
|
100
|
+
return RootIsolationResult(
|
|
101
|
+
periodic=ps, complete=True, method="structural_periodic", certificates=(c,)
|
|
102
|
+
)
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _special(expr, var, domain):
|
|
107
|
+
if expr.func in (sp.erf, sp.erfi) and len(expr.args) == 1:
|
|
108
|
+
ab = _affine_data(expr.args[0], var)
|
|
109
|
+
if ab:
|
|
110
|
+
a, b = ab
|
|
111
|
+
r = sp.simplify(-b / a)
|
|
112
|
+
rts = () if sp.Contains(r, domain) is S.false else (r,)
|
|
113
|
+
c = RootCertificate(
|
|
114
|
+
domain,
|
|
115
|
+
len(rts),
|
|
116
|
+
"special_function_zero",
|
|
117
|
+
("erf/erfi has unique real zero at 0",),
|
|
118
|
+
True,
|
|
119
|
+
)
|
|
120
|
+
return RootIsolationResult(
|
|
121
|
+
rts, complete=True, method="special", certificates=(c,)
|
|
122
|
+
)
|
|
123
|
+
if expr.func is sp.gamma:
|
|
124
|
+
c = RootCertificate(
|
|
125
|
+
domain, 0, "special_function_no_zeros", ("gamma has no zeros",), True
|
|
126
|
+
)
|
|
127
|
+
return RootIsolationResult(
|
|
128
|
+
(), complete=True, method="special_no_zeros", certificates=(c,)
|
|
129
|
+
)
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _guard_proved(g):
|
|
134
|
+
g = sp.sympify(g)
|
|
135
|
+
if g is S.true:
|
|
136
|
+
return True
|
|
137
|
+
try:
|
|
138
|
+
q = sp.refine(g)
|
|
139
|
+
if q is S.true:
|
|
140
|
+
return True
|
|
141
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
142
|
+
pass
|
|
143
|
+
try:
|
|
144
|
+
q = sp.simplify(g)
|
|
145
|
+
if q is S.true:
|
|
146
|
+
return True
|
|
147
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
148
|
+
pass
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _orthogonal_special(expr, var, domain):
|
|
153
|
+
"""Return exact zero-family metadata for audited orthogonal polynomials.
|
|
154
|
+
|
|
155
|
+
Parameter guards are retained symbolically; completeness is asserted only
|
|
156
|
+
when the support and degree hypotheses are proved on the requested domain.
|
|
157
|
+
"""
|
|
158
|
+
# Formula-level DLMF zero distributions. For unresolved parameter guards
|
|
159
|
+
# retain exact conditional metadata without claiming completeness.
|
|
160
|
+
if not expr.args or expr.args[-1] != var:
|
|
161
|
+
return None
|
|
162
|
+
f = expr.func
|
|
163
|
+
n = expr.args[0] if expr.args else None
|
|
164
|
+
n_guard = sp.And(sp.Contains(n, S.Integers), n >= 1) if n is not None else S.false
|
|
165
|
+
support = None
|
|
166
|
+
guard = n_guard
|
|
167
|
+
source = ""
|
|
168
|
+
interlace = True
|
|
169
|
+
if f is sp.jacobi and len(expr.args) == 4:
|
|
170
|
+
a, b = expr.args[1:3]
|
|
171
|
+
support = sp.Interval.open(-1, 1)
|
|
172
|
+
guard = sp.And(n_guard, a > -1, b > -1)
|
|
173
|
+
source = "DLMF 18.16.E1 + 18.2(vi)"
|
|
174
|
+
elif f is sp.gegenbauer and len(expr.args) == 3:
|
|
175
|
+
lam = expr.args[1]
|
|
176
|
+
support = sp.Interval.open(-1, 1)
|
|
177
|
+
guard = sp.And(n_guard, lam > -sp.Rational(1, 2), sp.Ne(lam, 0))
|
|
178
|
+
source = "DLMF 18.16(iii) + 18.2(vi)"
|
|
179
|
+
elif f is sp.assoc_laguerre and len(expr.args) == 3:
|
|
180
|
+
a = expr.args[1]
|
|
181
|
+
support = sp.Interval.open(0, sp.oo)
|
|
182
|
+
guard = sp.And(n_guard, a > -1)
|
|
183
|
+
source = "DLMF 18.16.E9 + 18.2(vi)"
|
|
184
|
+
elif f is sp.laguerre and len(expr.args) == 2:
|
|
185
|
+
support = sp.Interval.open(0, sp.oo)
|
|
186
|
+
source = "DLMF 18.16.E9 + 18.2(vi)"
|
|
187
|
+
elif f in tuple(
|
|
188
|
+
q
|
|
189
|
+
for q in (getattr(sp, "hermite", None), getattr(sp, "hermite_prob", None))
|
|
190
|
+
if q is not None
|
|
191
|
+
):
|
|
192
|
+
support = S.Reals
|
|
193
|
+
source = "DLMF 18.16(v) + 18.2(vi)"
|
|
194
|
+
elif f is sp.chebyshevt and len(expr.args) == 2:
|
|
195
|
+
support = sp.Interval.open(-1, 1)
|
|
196
|
+
source = "DLMF 18.5.E1 + 18.16(iii)"
|
|
197
|
+
if _guard_proved(n_guard):
|
|
198
|
+
k = sp.Dummy("k", integer=True)
|
|
199
|
+
roots = sp.ImageSet(
|
|
200
|
+
sp.Lambda(k, sp.cos((2 * k - 1) * sp.pi / (2 * n))), sp.Range(1, n + 1)
|
|
201
|
+
)
|
|
202
|
+
with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
|
|
203
|
+
roots = sp.Intersection(roots, domain)
|
|
204
|
+
c = RootCertificate(domain, None, "DLMF_chebyshev_formula", (source,), True)
|
|
205
|
+
fam = ParametricZeroFamily(n, support, n_guard, True, True, (), source)
|
|
206
|
+
return RootIsolationResult(
|
|
207
|
+
complete=True,
|
|
208
|
+
method="dlmf_indexed_zeros",
|
|
209
|
+
evidence=(roots,),
|
|
210
|
+
certificates=(c,),
|
|
211
|
+
parametric_families=(fam,),
|
|
212
|
+
)
|
|
213
|
+
elif f is sp.chebyshevu and len(expr.args) == 2:
|
|
214
|
+
support = sp.Interval.open(-1, 1)
|
|
215
|
+
source = "DLMF 18.5.E2 + 18.16(iii)"
|
|
216
|
+
if _guard_proved(n_guard):
|
|
217
|
+
k = sp.Dummy("k", integer=True)
|
|
218
|
+
roots = sp.ImageSet(
|
|
219
|
+
sp.Lambda(k, sp.cos(k * sp.pi / (n + 1))), sp.Range(1, n + 1)
|
|
220
|
+
)
|
|
221
|
+
with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
|
|
222
|
+
roots = sp.Intersection(roots, domain)
|
|
223
|
+
c = RootCertificate(domain, None, "DLMF_chebyshev_formula", (source,), True)
|
|
224
|
+
fam = ParametricZeroFamily(n, support, n_guard, True, True, (), source)
|
|
225
|
+
return RootIsolationResult(
|
|
226
|
+
complete=True,
|
|
227
|
+
method="dlmf_indexed_zeros",
|
|
228
|
+
evidence=(roots,),
|
|
229
|
+
certificates=(c,),
|
|
230
|
+
parametric_families=(fam,),
|
|
231
|
+
)
|
|
232
|
+
elif f is sp.assoc_legendre and len(expr.args) == 3:
|
|
233
|
+
mu = expr.args[1]
|
|
234
|
+
support = sp.Interval.open(-1, 1)
|
|
235
|
+
guard = sp.And(
|
|
236
|
+
sp.Contains(n, S.Naturals0), sp.Contains(mu, S.Integers), sp.Abs(mu) <= n
|
|
237
|
+
)
|
|
238
|
+
count = sp.simplify(n - sp.Abs(mu))
|
|
239
|
+
source = "DLMF 14.16(ii), integer-degree case"
|
|
240
|
+
fam = ParametricZeroFamily(count, support, guard, None, None, (), source)
|
|
241
|
+
contained = domain == support
|
|
242
|
+
if _guard_proved(guard) and contained:
|
|
243
|
+
c = RootCertificate(
|
|
244
|
+
domain,
|
|
245
|
+
None,
|
|
246
|
+
"DLMF_Ferrers_zero_count",
|
|
247
|
+
(source, ("count", count), ("support", support)),
|
|
248
|
+
True,
|
|
249
|
+
)
|
|
250
|
+
return RootIsolationResult(
|
|
251
|
+
complete=True,
|
|
252
|
+
method="dlmf_zero_distribution",
|
|
253
|
+
evidence=(sp.ConditionSet(var, sp.Eq(expr, 0), support),),
|
|
254
|
+
certificates=(c,),
|
|
255
|
+
parametric_families=(fam,),
|
|
256
|
+
)
|
|
257
|
+
return RootIsolationResult(
|
|
258
|
+
complete=False,
|
|
259
|
+
method="conditional_dlmf_zero_distribution",
|
|
260
|
+
evidence=(sp.ConditionSet(var, sp.Eq(expr, 0), support),),
|
|
261
|
+
parametric_families=(fam,),
|
|
262
|
+
)
|
|
263
|
+
elif f in (sp.jn, sp.yn) and len(expr.args) == 2:
|
|
264
|
+
support = sp.Interval.open(0, sp.oo)
|
|
265
|
+
guard = sp.And(sp.Contains(n, S.Integers), n >= 0)
|
|
266
|
+
source = "DLMF 10.58.E1 + 10.21.E2"
|
|
267
|
+
rel = ("positive zeros interlace with adjacent order",)
|
|
268
|
+
fam = ParametricZeroFamily(sp.oo, support, guard, True, True, rel, source)
|
|
269
|
+
contained = domain == support
|
|
270
|
+
if _guard_proved(guard) and contained:
|
|
271
|
+
c = RootCertificate(
|
|
272
|
+
domain,
|
|
273
|
+
None,
|
|
274
|
+
"DLMF_spherical_bessel_zeros",
|
|
275
|
+
(source, ("count", sp.oo), ("support", support)),
|
|
276
|
+
True,
|
|
277
|
+
)
|
|
278
|
+
return RootIsolationResult(
|
|
279
|
+
complete=True,
|
|
280
|
+
method="dlmf_zero_distribution",
|
|
281
|
+
evidence=(sp.ConditionSet(var, sp.Eq(expr, 0), support),),
|
|
282
|
+
certificates=(c,),
|
|
283
|
+
parametric_families=(fam,),
|
|
284
|
+
)
|
|
285
|
+
return RootIsolationResult(
|
|
286
|
+
complete=False,
|
|
287
|
+
method="conditional_dlmf_zero_distribution",
|
|
288
|
+
evidence=(sp.ConditionSet(var, sp.Eq(expr, 0), support),),
|
|
289
|
+
parametric_families=(fam,),
|
|
290
|
+
)
|
|
291
|
+
if support is None:
|
|
292
|
+
return None
|
|
293
|
+
fam = ParametricZeroFamily(n, support, guard, True, interlace, (), source)
|
|
294
|
+
contained = domain is S.Reals or domain == support
|
|
295
|
+
if _guard_proved(guard) and contained:
|
|
296
|
+
c = RootCertificate(
|
|
297
|
+
domain,
|
|
298
|
+
None,
|
|
299
|
+
"DLMF_zero_distribution",
|
|
300
|
+
(source, ("count", n), ("support", support)),
|
|
301
|
+
True,
|
|
302
|
+
)
|
|
303
|
+
return RootIsolationResult(
|
|
304
|
+
complete=True,
|
|
305
|
+
method="dlmf_zero_distribution",
|
|
306
|
+
evidence=(sp.ConditionSet(var, sp.Eq(expr, 0), support),),
|
|
307
|
+
certificates=(c,),
|
|
308
|
+
parametric_families=(fam,),
|
|
309
|
+
)
|
|
310
|
+
return RootIsolationResult(
|
|
311
|
+
complete=False,
|
|
312
|
+
method="conditional_dlmf_zero_distribution",
|
|
313
|
+
evidence=(sp.ConditionSet(var, sp.Eq(expr, 0), support),),
|
|
314
|
+
parametric_families=(fam,),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _concrete_zero_set_parts(solution):
|
|
319
|
+
"""Split a concrete real solution set into exact points and zero intervals.
|
|
320
|
+
|
|
321
|
+
``None`` means the set still contains symbolic/conditional structure and cannot
|
|
322
|
+
support a completeness claim.
|
|
323
|
+
"""
|
|
324
|
+
if solution is S.EmptySet:
|
|
325
|
+
return (), ()
|
|
326
|
+
if isinstance(solution, sp.FiniteSet):
|
|
327
|
+
return _sort_unique(solution), ()
|
|
328
|
+
if isinstance(solution, sp.Interval):
|
|
329
|
+
return (), (solution,)
|
|
330
|
+
if isinstance(solution, sp.Union):
|
|
331
|
+
points = []
|
|
332
|
+
intervals = []
|
|
333
|
+
for part in solution.args:
|
|
334
|
+
split = _concrete_zero_set_parts(part)
|
|
335
|
+
if split is None:
|
|
336
|
+
return None
|
|
337
|
+
p, i = split
|
|
338
|
+
points.extend(p)
|
|
339
|
+
intervals.extend(i)
|
|
340
|
+
return _sort_unique(points), tuple(
|
|
341
|
+
sorted(set(intervals), key=sp.default_sort_key)
|
|
342
|
+
)
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _identically_zero_interval(expr, var, domain):
|
|
347
|
+
"""Prove that a complete interval cell belongs to the zero set."""
|
|
348
|
+
if not isinstance(domain, sp.Interval):
|
|
349
|
+
return None
|
|
350
|
+
try:
|
|
351
|
+
reduced = sp.simplify(expr)
|
|
352
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
353
|
+
reduced = expr
|
|
354
|
+
if reduced == 0 or reduced.is_zero is True:
|
|
355
|
+
cert = RootCertificate(
|
|
356
|
+
domain, None, "identically_zero_interval", (("simplified", reduced),), True
|
|
357
|
+
)
|
|
358
|
+
return RootIsolationResult(
|
|
359
|
+
zero_intervals=(domain,),
|
|
360
|
+
complete=True,
|
|
361
|
+
method="identically_zero_interval",
|
|
362
|
+
certificates=(cert,),
|
|
363
|
+
)
|
|
364
|
+
if isinstance(expr, sp.Piecewise):
|
|
365
|
+
try:
|
|
366
|
+
from ..real_analysis.common import condition_to_set
|
|
367
|
+
|
|
368
|
+
exclusive = sp.piecewise_exclusive(expr, deep=True)
|
|
369
|
+
for branch, condition in exclusive.args:
|
|
370
|
+
branch_set = condition_to_set(condition, var)
|
|
371
|
+
if branch_set is None:
|
|
372
|
+
continue
|
|
373
|
+
if domain.is_subset(branch_set) is True:
|
|
374
|
+
value = sp.simplify(branch)
|
|
375
|
+
if value == 0 or value.is_zero is True:
|
|
376
|
+
cert = RootCertificate(
|
|
377
|
+
domain,
|
|
378
|
+
None,
|
|
379
|
+
"solveset_identically_zero_interval",
|
|
380
|
+
(("branch_condition", condition),),
|
|
381
|
+
True,
|
|
382
|
+
)
|
|
383
|
+
return RootIsolationResult(
|
|
384
|
+
zero_intervals=(domain,),
|
|
385
|
+
complete=True,
|
|
386
|
+
method="identically_zero_interval",
|
|
387
|
+
certificates=(cert,),
|
|
388
|
+
)
|
|
389
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
390
|
+
pass
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _solveset(expr, var, domain):
|
|
395
|
+
try:
|
|
396
|
+
sol = sp.solveset(sp.Eq(expr, 0), var, domain=domain)
|
|
397
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
398
|
+
return None
|
|
399
|
+
concrete = _concrete_zero_set_parts(sol)
|
|
400
|
+
if concrete is None:
|
|
401
|
+
return None
|
|
402
|
+
roots, intervals = concrete
|
|
403
|
+
certs = []
|
|
404
|
+
if intervals:
|
|
405
|
+
certs.extend(
|
|
406
|
+
RootCertificate(
|
|
407
|
+
interval,
|
|
408
|
+
None,
|
|
409
|
+
"solveset_identically_zero_interval",
|
|
410
|
+
(("solution_set", sol),),
|
|
411
|
+
True,
|
|
412
|
+
)
|
|
413
|
+
for interval in intervals
|
|
414
|
+
)
|
|
415
|
+
if roots:
|
|
416
|
+
certs.append(
|
|
417
|
+
RootCertificate(
|
|
418
|
+
sp.FiniteSet(*roots),
|
|
419
|
+
len(roots),
|
|
420
|
+
"solveset",
|
|
421
|
+
(sol,),
|
|
422
|
+
True,
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
if not roots and not intervals:
|
|
426
|
+
certs.append(RootCertificate(domain, 0, "solveset", (sol,), True))
|
|
427
|
+
method = "solveset_zero_geometry" if intervals else "solveset"
|
|
428
|
+
return RootIsolationResult(
|
|
429
|
+
roots,
|
|
430
|
+
intervals,
|
|
431
|
+
complete=True,
|
|
432
|
+
method=method,
|
|
433
|
+
certificates=tuple(certs),
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _endpoint_value(expr, var, x, side):
|
|
438
|
+
from ..proof_cache import cached_endpoint_limit
|
|
439
|
+
|
|
440
|
+
return cached_endpoint_limit(expr, var, x, direction=side, attained=False)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _strict_derivative_sign(d, var, interval):
|
|
444
|
+
# exact positivity/negativity via solveset of the negating condition
|
|
445
|
+
try:
|
|
446
|
+
bad = sp.solveset(d <= 0, var, domain=interval)
|
|
447
|
+
if bad is S.EmptySet:
|
|
448
|
+
return 1
|
|
449
|
+
bad = sp.solveset(d >= 0, var, domain=interval)
|
|
450
|
+
if bad is S.EmptySet:
|
|
451
|
+
return -1
|
|
452
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
453
|
+
pass
|
|
454
|
+
return 0
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _certified_monotone_interval(expr, var, domain):
|
|
458
|
+
"""Certify at most one root on an interval from a strict derivative sign."""
|
|
459
|
+
if not isinstance(domain, sp.Interval):
|
|
460
|
+
return None
|
|
461
|
+
try:
|
|
462
|
+
from ..proof_cache import cached_derivative
|
|
463
|
+
|
|
464
|
+
d = cached_derivative(expr, var, order=1)
|
|
465
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
466
|
+
return None
|
|
467
|
+
s = _strict_derivative_sign(d, var, domain)
|
|
468
|
+
if not s:
|
|
469
|
+
return None
|
|
470
|
+
ana = (
|
|
471
|
+
analytic_neighborhood(expr, var, domain)
|
|
472
|
+
if domain.start not in (-sp.oo, sp.oo) and domain.end not in (-sp.oo, sp.oo)
|
|
473
|
+
else None
|
|
474
|
+
)
|
|
475
|
+
# real differentiability + strict derivative sign suffices; complex cert is extra evidence
|
|
476
|
+
left = _endpoint_value(expr, var, domain.start, "+")
|
|
477
|
+
right = _endpoint_value(expr, var, domain.end, "-")
|
|
478
|
+
if left is None or right is None:
|
|
479
|
+
return None
|
|
480
|
+
sl = sp.sign(left)
|
|
481
|
+
sr = sp.sign(right)
|
|
482
|
+
cert = RootCertificate(
|
|
483
|
+
domain,
|
|
484
|
+
None,
|
|
485
|
+
"strict_monotonicity",
|
|
486
|
+
(("derivative_sign", s), ("endpoint_values", left, right), ana),
|
|
487
|
+
True,
|
|
488
|
+
)
|
|
489
|
+
if sl.is_zero is True and not domain.left_open:
|
|
490
|
+
cert = RootCertificate(domain, 1, "strict_monotonicity", cert.proof, True)
|
|
491
|
+
return RootIsolationResult(
|
|
492
|
+
(domain.start,),
|
|
493
|
+
complete=True,
|
|
494
|
+
method="certified_monotonic",
|
|
495
|
+
certificates=(cert,),
|
|
496
|
+
)
|
|
497
|
+
if sr.is_zero is True and not domain.right_open:
|
|
498
|
+
cert = RootCertificate(domain, 1, "strict_monotonicity", cert.proof, True)
|
|
499
|
+
return RootIsolationResult(
|
|
500
|
+
(domain.end,),
|
|
501
|
+
complete=True,
|
|
502
|
+
method="certified_monotonic",
|
|
503
|
+
certificates=(cert,),
|
|
504
|
+
)
|
|
505
|
+
if (sl.is_positive and sr.is_positive) or (sl.is_negative and sr.is_negative):
|
|
506
|
+
cert = RootCertificate(domain, 0, "strict_monotonicity", cert.proof, True)
|
|
507
|
+
return RootIsolationResult(
|
|
508
|
+
(), complete=True, method="certified_monotonic", certificates=(cert,)
|
|
509
|
+
)
|
|
510
|
+
if (sl.is_negative and sr.is_positive) or (sl.is_positive and sr.is_negative):
|
|
511
|
+
# Unique root is certified even if there is no closed form. Produce a nested rational isolating interval.
|
|
512
|
+
if domain.start in (-sp.oo, sp.oo) or domain.end in (-sp.oo, sp.oo):
|
|
513
|
+
return None
|
|
514
|
+
a = sp.Rational(domain.start) if domain.start.is_Rational else domain.start
|
|
515
|
+
b = sp.Rational(domain.end) if domain.end.is_Rational else domain.end
|
|
516
|
+
# exact bisection using symbolic signs; stop at 24 certified subdivisions or earlier unknown
|
|
517
|
+
aa, bb = a, b
|
|
518
|
+
sa = sl
|
|
519
|
+
for _ in range(24):
|
|
520
|
+
m = sp.simplify((aa + bb) / 2)
|
|
521
|
+
sm = sp.sign(sp.simplify(expr.subs(var, m)))
|
|
522
|
+
if sm.is_zero is True:
|
|
523
|
+
cert = RootCertificate(
|
|
524
|
+
domain,
|
|
525
|
+
1,
|
|
526
|
+
"strict_monotonicity+exact_bisection",
|
|
527
|
+
(*cert.proof, ("exact_root", m)),
|
|
528
|
+
True,
|
|
529
|
+
)
|
|
530
|
+
return RootIsolationResult(
|
|
531
|
+
(m,),
|
|
532
|
+
complete=True,
|
|
533
|
+
method="certified_monotonic",
|
|
534
|
+
certificates=(cert,),
|
|
535
|
+
)
|
|
536
|
+
if sm.is_positive is not True and sm.is_negative is not True:
|
|
537
|
+
break
|
|
538
|
+
if (sa.is_negative and sm.is_positive) or (
|
|
539
|
+
sa.is_positive and sm.is_negative
|
|
540
|
+
):
|
|
541
|
+
bb = m
|
|
542
|
+
else:
|
|
543
|
+
aa = m
|
|
544
|
+
sa = sm
|
|
545
|
+
iv = sp.Interval(aa, bb)
|
|
546
|
+
cert = RootCertificate(
|
|
547
|
+
domain,
|
|
548
|
+
1,
|
|
549
|
+
"strict_monotonicity+isolation",
|
|
550
|
+
(*cert.proof, ("isolating_interval", iv)),
|
|
551
|
+
True,
|
|
552
|
+
)
|
|
553
|
+
return RootIsolationResult(
|
|
554
|
+
isolated_intervals=(CertifiedRootInterval(iv, cert),),
|
|
555
|
+
complete=True,
|
|
556
|
+
method="certified_monotonic_interval",
|
|
557
|
+
certificates=(cert,),
|
|
558
|
+
)
|
|
559
|
+
return None
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _exact_sign(value):
|
|
563
|
+
"""Return -1, 0, or 1 only when the real sign is certified."""
|
|
564
|
+
return robust_real_sign(value)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _certified_multiplicity(expr, var, root, *, max_order=8):
|
|
568
|
+
"""Return a proved finite multiplicity, or ``None`` if not established."""
|
|
569
|
+
current = expr
|
|
570
|
+
for order in range(max_order + 1):
|
|
571
|
+
try:
|
|
572
|
+
value = sp.simplify(current.subs(var, root))
|
|
573
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
574
|
+
return None
|
|
575
|
+
sign = _exact_sign(value)
|
|
576
|
+
if order == 0 and sign not in (0, None):
|
|
577
|
+
return None
|
|
578
|
+
if order > 0 and sign in (-1, 1):
|
|
579
|
+
return order
|
|
580
|
+
if sign is None:
|
|
581
|
+
return None
|
|
582
|
+
try:
|
|
583
|
+
current = sp.diff(current, var)
|
|
584
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
585
|
+
return None
|
|
586
|
+
return None
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _dyadic_interior_points(interval, *, max_level=24):
|
|
590
|
+
"""Yield exact dyadic points approaching both ends of a finite interval."""
|
|
591
|
+
if not isinstance(interval, sp.Interval):
|
|
592
|
+
return
|
|
593
|
+
try:
|
|
594
|
+
left = float(sp.N(interval.start, 40))
|
|
595
|
+
right = float(sp.N(interval.end, 40))
|
|
596
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
597
|
+
return
|
|
598
|
+
if not (math.isfinite(left) and math.isfinite(right) and left < right):
|
|
599
|
+
return
|
|
600
|
+
seen = set()
|
|
601
|
+
for level in range(4, max_level + 1):
|
|
602
|
+
den = 1 << level
|
|
603
|
+
lo_num = math.floor(left * den) + 1
|
|
604
|
+
hi_num = math.ceil(right * den) - 1
|
|
605
|
+
for num in (lo_num, hi_num):
|
|
606
|
+
q = sp.Rational(num, den)
|
|
607
|
+
if q in seen:
|
|
608
|
+
continue
|
|
609
|
+
seen.add(q)
|
|
610
|
+
try:
|
|
611
|
+
inside = interval.contains(q)
|
|
612
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
613
|
+
continue
|
|
614
|
+
if inside is S.true:
|
|
615
|
+
yield q
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _rational_sign_bracket(expr, var, interval, left_sign, right_sign):
|
|
619
|
+
"""Find rational points inside ``interval`` with the requested opposite signs."""
|
|
620
|
+
left = None
|
|
621
|
+
right = None
|
|
622
|
+
for q in _dyadic_interior_points(interval):
|
|
623
|
+
sign = _exact_sign(expr.subs(var, q))
|
|
624
|
+
if sign == left_sign and (left is None or q < left):
|
|
625
|
+
left = q
|
|
626
|
+
if sign == right_sign and (right is None or q > right):
|
|
627
|
+
right = q
|
|
628
|
+
if left is not None and right is not None and left < right:
|
|
629
|
+
return left, right
|
|
630
|
+
return None
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _bisect_certified_bracket(expr, var, a, b, sa, *, steps=32):
|
|
634
|
+
"""Tighten an exact opposite-sign rational bracket without losing certification."""
|
|
635
|
+
aa, bb = sp.Rational(a), sp.Rational(b)
|
|
636
|
+
left_sign = sa
|
|
637
|
+
for _ in range(steps):
|
|
638
|
+
m = (aa + bb) / 2
|
|
639
|
+
sm = _exact_sign(expr.subs(var, m))
|
|
640
|
+
if sm == 0:
|
|
641
|
+
return m, None
|
|
642
|
+
if sm not in (-1, 1):
|
|
643
|
+
break
|
|
644
|
+
if sm == left_sign:
|
|
645
|
+
aa = m
|
|
646
|
+
else:
|
|
647
|
+
bb = m
|
|
648
|
+
return None, sp.Interval(aa, bb)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _certified_range_on_interval(expr, var, interval):
|
|
652
|
+
"""Return an exact range set on ``interval`` when funcprops can certify it."""
|
|
653
|
+
try:
|
|
654
|
+
from ..real_analysis.common import condition_to_set, set_to_condition
|
|
655
|
+
from ..real_analysis.range import function_range
|
|
656
|
+
|
|
657
|
+
y = sp.Dummy("y", real=True)
|
|
658
|
+
cond = function_range(
|
|
659
|
+
expr,
|
|
660
|
+
var,
|
|
661
|
+
y,
|
|
662
|
+
constraints=set_to_condition(interval, var),
|
|
663
|
+
)
|
|
664
|
+
if hasattr(cond, "cases") and hasattr(cond, "primary"):
|
|
665
|
+
return None
|
|
666
|
+
result = condition_to_set(cond, y)
|
|
667
|
+
except (ImportError, *SYMBOLIC_QUERY_ERRORS):
|
|
668
|
+
return None
|
|
669
|
+
return result if isinstance(result, sp.Set) else None
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _range_sign(range_set):
|
|
673
|
+
"""Return a strict common sign for a certified range set, if available."""
|
|
674
|
+
try:
|
|
675
|
+
if sp.Contains(0, range_set) is not S.false:
|
|
676
|
+
return 0
|
|
677
|
+
positive = sp.Intersection(range_set, sp.Interval.open(0, sp.oo))
|
|
678
|
+
negative = sp.Intersection(range_set, sp.Interval.open(-sp.oo, 0))
|
|
679
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
680
|
+
return 0
|
|
681
|
+
if positive is not S.EmptySet and negative is S.EmptySet:
|
|
682
|
+
return 1
|
|
683
|
+
if negative is not S.EmptySet and positive is S.EmptySet:
|
|
684
|
+
return -1
|
|
685
|
+
return 0
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _certified_interval_resolution(expr, var, domain):
|
|
689
|
+
"""Resolve a finite real interval from exact range/derivative-range evidence.
|
|
690
|
+
|
|
691
|
+
This is the funcprops analogue of an interval-arithmetic undecided-cell pass:
|
|
692
|
+
an exact image excluding zero proves a cell root-free; an exact derivative image
|
|
693
|
+
of one strict sign proves monotonicity and reduces the zero question to endpoint
|
|
694
|
+
signs plus rational isolation.
|
|
695
|
+
"""
|
|
696
|
+
if (
|
|
697
|
+
not isinstance(domain, sp.Interval)
|
|
698
|
+
or domain.start in (-sp.oo, sp.oo)
|
|
699
|
+
or domain.end in (-sp.oo, sp.oo)
|
|
700
|
+
):
|
|
701
|
+
return None
|
|
702
|
+
|
|
703
|
+
try:
|
|
704
|
+
from .real_interval import certified_real_interval_evaluate
|
|
705
|
+
|
|
706
|
+
interval_value = certified_real_interval_evaluate(expr, var, domain)
|
|
707
|
+
except ImportError:
|
|
708
|
+
interval_value = None
|
|
709
|
+
if interval_value is not None and interval_value.excludes_zero:
|
|
710
|
+
cert = RootCertificate(
|
|
711
|
+
domain,
|
|
712
|
+
0,
|
|
713
|
+
"certified_interval_range_exclusion",
|
|
714
|
+
(("interval_evaluation", interval_value),),
|
|
715
|
+
True,
|
|
716
|
+
)
|
|
717
|
+
return RootIsolationResult(
|
|
718
|
+
complete=True,
|
|
719
|
+
method="certified_interval_resolution",
|
|
720
|
+
evidence=(interval_value,),
|
|
721
|
+
certificates=(cert,),
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
image = _certified_range_on_interval(expr, var, domain)
|
|
725
|
+
if image is not None:
|
|
726
|
+
try:
|
|
727
|
+
excludes_zero = sp.Contains(0, image) is S.false
|
|
728
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
729
|
+
excludes_zero = False
|
|
730
|
+
if excludes_zero:
|
|
731
|
+
cert = RootCertificate(
|
|
732
|
+
domain,
|
|
733
|
+
0,
|
|
734
|
+
"certified_interval_range_exclusion",
|
|
735
|
+
(("range", image),),
|
|
736
|
+
True,
|
|
737
|
+
)
|
|
738
|
+
return RootIsolationResult(
|
|
739
|
+
complete=True,
|
|
740
|
+
method="certified_interval_resolution",
|
|
741
|
+
evidence=(("range", image),),
|
|
742
|
+
certificates=(cert,),
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
try:
|
|
746
|
+
derivative = sp.diff(expr, var)
|
|
747
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
748
|
+
return None
|
|
749
|
+
try:
|
|
750
|
+
derivative_interval = certified_real_interval_evaluate(derivative, var, domain)
|
|
751
|
+
except (NameError, ValueError):
|
|
752
|
+
derivative_interval = None
|
|
753
|
+
derivative_image = _certified_range_on_interval(derivative, var, domain)
|
|
754
|
+
derivative_sign = (
|
|
755
|
+
derivative_interval.strict_sign
|
|
756
|
+
if derivative_interval is not None and derivative_interval.excludes_zero
|
|
757
|
+
else _range_sign(derivative_image)
|
|
758
|
+
if derivative_image is not None
|
|
759
|
+
else 0
|
|
760
|
+
)
|
|
761
|
+
if not derivative_sign:
|
|
762
|
+
return None
|
|
763
|
+
|
|
764
|
+
left = _endpoint_value(expr, var, domain.start, "+")
|
|
765
|
+
right = _endpoint_value(expr, var, domain.end, "-")
|
|
766
|
+
if left is None or right is None:
|
|
767
|
+
return None
|
|
768
|
+
sl = _exact_sign(left)
|
|
769
|
+
sr = _exact_sign(right)
|
|
770
|
+
if sl is None or sr is None:
|
|
771
|
+
return None
|
|
772
|
+
proof = (
|
|
773
|
+
("derivative_interval", derivative_interval),
|
|
774
|
+
("derivative_range", derivative_image),
|
|
775
|
+
("derivative_sign", derivative_sign),
|
|
776
|
+
("endpoint_values", left, right),
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
if sl == 0 and not domain.left_open:
|
|
780
|
+
mult = _certified_multiplicity(expr, var, domain.start)
|
|
781
|
+
cert = RootCertificate(
|
|
782
|
+
domain, 1, "interval_resolution_endpoint_root", proof, True, mult
|
|
783
|
+
)
|
|
784
|
+
return RootIsolationResult(
|
|
785
|
+
(domain.start,),
|
|
786
|
+
complete=True,
|
|
787
|
+
method="certified_interval_resolution",
|
|
788
|
+
certificates=(cert,),
|
|
789
|
+
)
|
|
790
|
+
if sr == 0 and not domain.right_open:
|
|
791
|
+
mult = _certified_multiplicity(expr, var, domain.end)
|
|
792
|
+
cert = RootCertificate(
|
|
793
|
+
domain, 1, "interval_resolution_endpoint_root", proof, True, mult
|
|
794
|
+
)
|
|
795
|
+
return RootIsolationResult(
|
|
796
|
+
(domain.end,),
|
|
797
|
+
complete=True,
|
|
798
|
+
method="certified_interval_resolution",
|
|
799
|
+
certificates=(cert,),
|
|
800
|
+
)
|
|
801
|
+
if sl == sr and sl in (-1, 1):
|
|
802
|
+
cert = RootCertificate(
|
|
803
|
+
domain, 0, "interval_resolution_monotone_same_sign", proof, True
|
|
804
|
+
)
|
|
805
|
+
return RootIsolationResult(
|
|
806
|
+
complete=True,
|
|
807
|
+
method="certified_interval_resolution",
|
|
808
|
+
certificates=(cert,),
|
|
809
|
+
)
|
|
810
|
+
if sl * sr != -1:
|
|
811
|
+
return None
|
|
812
|
+
|
|
813
|
+
bracket = _rational_sign_bracket(expr, var, domain, sl, sr)
|
|
814
|
+
if bracket is None:
|
|
815
|
+
return None
|
|
816
|
+
a, b = bracket
|
|
817
|
+
exact_root, isolating = _bisect_certified_bracket(expr, var, a, b, sl)
|
|
818
|
+
if exact_root is not None:
|
|
819
|
+
mult = _certified_multiplicity(expr, var, exact_root)
|
|
820
|
+
cert = RootCertificate(
|
|
821
|
+
domain,
|
|
822
|
+
1,
|
|
823
|
+
"interval_resolution_exact_bisection",
|
|
824
|
+
(*proof, ("rational_bracket", a, b), ("exact_root", exact_root)),
|
|
825
|
+
True,
|
|
826
|
+
mult,
|
|
827
|
+
)
|
|
828
|
+
return RootIsolationResult(
|
|
829
|
+
(exact_root,),
|
|
830
|
+
complete=True,
|
|
831
|
+
method="certified_interval_resolution",
|
|
832
|
+
certificates=(cert,),
|
|
833
|
+
)
|
|
834
|
+
cert = RootCertificate(
|
|
835
|
+
domain,
|
|
836
|
+
1,
|
|
837
|
+
"interval_resolution_rational_isolation",
|
|
838
|
+
(*proof, ("rational_bracket", a, b), ("isolating_interval", isolating)),
|
|
839
|
+
True,
|
|
840
|
+
1,
|
|
841
|
+
)
|
|
842
|
+
return RootIsolationResult(
|
|
843
|
+
isolated_intervals=(CertifiedRootInterval(isolating, cert),),
|
|
844
|
+
complete=True,
|
|
845
|
+
method="certified_interval_resolution",
|
|
846
|
+
certificates=(cert,),
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def _certified_real_interval_isolation(expr, var, domain):
|
|
851
|
+
"""Certify all real roots on a finite interval from a complete monotone partition.
|
|
852
|
+
|
|
853
|
+
Candidate discovery is delegated to the existing exact derivative-zero/monotonicity
|
|
854
|
+
machinery. Certification itself uses exact endpoint signs and rational
|
|
855
|
+
opposite-sign brackets. Completeness is asserted only when every monotone cell
|
|
856
|
+
is classified and the partition is complete.
|
|
857
|
+
"""
|
|
858
|
+
if (
|
|
859
|
+
not isinstance(domain, sp.Interval)
|
|
860
|
+
or domain.start in (-sp.oo, sp.oo)
|
|
861
|
+
or domain.end in (-sp.oo, sp.oo)
|
|
862
|
+
):
|
|
863
|
+
return None
|
|
864
|
+
try:
|
|
865
|
+
from ..real_analysis.monotonic_certificates import monotonic_partition
|
|
866
|
+
|
|
867
|
+
partition = monotonic_partition(expr, var, domain=domain)
|
|
868
|
+
except (ImportError, *SYMBOLIC_QUERY_ERRORS):
|
|
869
|
+
return None
|
|
870
|
+
if not partition.finite or not partition.cells:
|
|
871
|
+
return None
|
|
872
|
+
|
|
873
|
+
roots = []
|
|
874
|
+
isolated = []
|
|
875
|
+
certs = []
|
|
876
|
+
complete = bool(partition.complete)
|
|
877
|
+
|
|
878
|
+
# Critical points and attained outer endpoints can themselves be roots,
|
|
879
|
+
# including even-multiplicity roots that do not produce a sign change.
|
|
880
|
+
boundary_points = list(partition.critical_points)
|
|
881
|
+
if not domain.left_open:
|
|
882
|
+
boundary_points.append(domain.start)
|
|
883
|
+
if not domain.right_open:
|
|
884
|
+
boundary_points.append(domain.end)
|
|
885
|
+
for point in _sort_unique(boundary_points):
|
|
886
|
+
try:
|
|
887
|
+
if sp.Contains(point, domain) is S.false:
|
|
888
|
+
continue
|
|
889
|
+
value = expr.subs(var, point)
|
|
890
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
891
|
+
complete = False
|
|
892
|
+
continue
|
|
893
|
+
if _exact_sign(value) == 0:
|
|
894
|
+
roots.append(point)
|
|
895
|
+
mult = _certified_multiplicity(expr, var, point)
|
|
896
|
+
certs.append(
|
|
897
|
+
RootCertificate(
|
|
898
|
+
sp.FiniteSet(point),
|
|
899
|
+
1,
|
|
900
|
+
"exact_partition_boundary_root",
|
|
901
|
+
(("value", value), ("partition", partition.source)),
|
|
902
|
+
True,
|
|
903
|
+
mult,
|
|
904
|
+
)
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
for cell in partition.cells:
|
|
908
|
+
interval = cell.interval
|
|
909
|
+
if (
|
|
910
|
+
not isinstance(interval, sp.Interval)
|
|
911
|
+
or not cell.strict
|
|
912
|
+
or not cell.continuous
|
|
913
|
+
):
|
|
914
|
+
complete = False
|
|
915
|
+
continue
|
|
916
|
+
sl = _exact_sign(cell.left_limit) if cell.left_limit is not None else None
|
|
917
|
+
sr = _exact_sign(cell.right_limit) if cell.right_limit is not None else None
|
|
918
|
+
if sl is None or sr is None:
|
|
919
|
+
complete = False
|
|
920
|
+
continue
|
|
921
|
+
# A zero limit at an excluded critical boundary is handled above if the
|
|
922
|
+
# boundary belongs to the requested domain; strict monotonicity excludes
|
|
923
|
+
# another zero in the open cell on the same side.
|
|
924
|
+
if sl == 0 or sr == 0:
|
|
925
|
+
certs.append(
|
|
926
|
+
RootCertificate(
|
|
927
|
+
interval,
|
|
928
|
+
0,
|
|
929
|
+
"strict_monotone_boundary_limit",
|
|
930
|
+
(("endpoint_signs", sl, sr), ("direction", cell.direction)),
|
|
931
|
+
True,
|
|
932
|
+
)
|
|
933
|
+
)
|
|
934
|
+
continue
|
|
935
|
+
if sl == sr:
|
|
936
|
+
certs.append(
|
|
937
|
+
RootCertificate(
|
|
938
|
+
interval,
|
|
939
|
+
0,
|
|
940
|
+
"strict_monotone_same_sign",
|
|
941
|
+
(("endpoint_signs", sl, sr), ("direction", cell.direction)),
|
|
942
|
+
True,
|
|
943
|
+
)
|
|
944
|
+
)
|
|
945
|
+
continue
|
|
946
|
+
bracket = _rational_sign_bracket(expr, var, interval, sl, sr)
|
|
947
|
+
if bracket is None:
|
|
948
|
+
complete = False
|
|
949
|
+
continue
|
|
950
|
+
a, b = bracket
|
|
951
|
+
exact_root, isolating = _bisect_certified_bracket(expr, var, a, b, sl)
|
|
952
|
+
proof = (
|
|
953
|
+
("endpoint_signs", sl, sr),
|
|
954
|
+
("rational_bracket", a, b),
|
|
955
|
+
("direction", cell.direction),
|
|
956
|
+
("partition", partition.source),
|
|
957
|
+
)
|
|
958
|
+
if exact_root is not None:
|
|
959
|
+
roots.append(exact_root)
|
|
960
|
+
mult = _certified_multiplicity(expr, var, exact_root)
|
|
961
|
+
certs.append(
|
|
962
|
+
RootCertificate(
|
|
963
|
+
interval,
|
|
964
|
+
1,
|
|
965
|
+
"strict_monotonicity+exact_bisection",
|
|
966
|
+
(*proof, ("exact_root", exact_root)),
|
|
967
|
+
True,
|
|
968
|
+
mult,
|
|
969
|
+
)
|
|
970
|
+
)
|
|
971
|
+
else:
|
|
972
|
+
cert = RootCertificate(
|
|
973
|
+
interval,
|
|
974
|
+
1,
|
|
975
|
+
"strict_monotonicity+rational_isolation",
|
|
976
|
+
(*proof, ("isolating_interval", isolating)),
|
|
977
|
+
True,
|
|
978
|
+
1,
|
|
979
|
+
)
|
|
980
|
+
isolated.append(CertifiedRootInterval(isolating, cert))
|
|
981
|
+
certs.append(cert)
|
|
982
|
+
|
|
983
|
+
roots = _sort_unique(roots)
|
|
984
|
+
method = (
|
|
985
|
+
"certified_real_interval_isolation"
|
|
986
|
+
if complete
|
|
987
|
+
else "partial_certified_real_interval_isolation"
|
|
988
|
+
)
|
|
989
|
+
return RootIsolationResult(
|
|
990
|
+
roots,
|
|
991
|
+
isolated_intervals=tuple(isolated),
|
|
992
|
+
complete=complete,
|
|
993
|
+
method=method,
|
|
994
|
+
evidence=(partition,),
|
|
995
|
+
certificates=tuple(certs),
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def _scale_root_certificate(cert, factor, *, method_prefix="repeated_factor"):
|
|
1000
|
+
"""Lift a certificate through ``g(x)**factor`` without losing provenance."""
|
|
1001
|
+
base_multiplicity = cert.multiplicity
|
|
1002
|
+
if (
|
|
1003
|
+
base_multiplicity is None
|
|
1004
|
+
and cert.root_count == 1
|
|
1005
|
+
and cert.method
|
|
1006
|
+
in {
|
|
1007
|
+
"strict_monotonicity+isolation",
|
|
1008
|
+
"strict_monotonicity+rational_isolation",
|
|
1009
|
+
"strict_monotonicity+exact_bisection",
|
|
1010
|
+
}
|
|
1011
|
+
):
|
|
1012
|
+
base_multiplicity = 1
|
|
1013
|
+
multiplicity = base_multiplicity * factor if base_multiplicity is not None else None
|
|
1014
|
+
return RootCertificate(
|
|
1015
|
+
cert.domain,
|
|
1016
|
+
cert.root_count,
|
|
1017
|
+
f"{method_prefix}:{cert.method}",
|
|
1018
|
+
(*cert.proof, ("multiplicity_scale", factor)),
|
|
1019
|
+
cert.complete,
|
|
1020
|
+
multiplicity,
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def _repeated_power_roots(expr, var, domain):
|
|
1025
|
+
"""Lift certified roots of a repeated factor ``base**m``.
|
|
1026
|
+
|
|
1027
|
+
This is especially important for even powers: the original expression has no
|
|
1028
|
+
sign change at a simple zero of ``base``, while ``base`` itself does. The
|
|
1029
|
+
recursive query disables heuristic numerical roots; every lifted root therefore
|
|
1030
|
+
retains a proof from the underlying factor.
|
|
1031
|
+
"""
|
|
1032
|
+
if not isinstance(expr, sp.Pow):
|
|
1033
|
+
return None
|
|
1034
|
+
base, exponent = expr.as_base_exp()
|
|
1035
|
+
if exponent.is_Integer is not True or exponent < 2 or base == expr:
|
|
1036
|
+
return None
|
|
1037
|
+
underlying = transcendental_roots(base, var, domain, allow_numerical=False)
|
|
1038
|
+
if not (
|
|
1039
|
+
underlying.roots
|
|
1040
|
+
or underlying.isolated_intervals
|
|
1041
|
+
or underlying.zero_intervals
|
|
1042
|
+
or underlying.periodic is not None
|
|
1043
|
+
or underlying.complete
|
|
1044
|
+
):
|
|
1045
|
+
return None
|
|
1046
|
+
factor = int(exponent)
|
|
1047
|
+
lifted_isolated = tuple(
|
|
1048
|
+
CertifiedRootInterval(
|
|
1049
|
+
item.interval, _scale_root_certificate(item.certificate, factor)
|
|
1050
|
+
)
|
|
1051
|
+
for item in underlying.isolated_intervals
|
|
1052
|
+
)
|
|
1053
|
+
lifted_certs = tuple(
|
|
1054
|
+
_scale_root_certificate(cert, factor) for cert in underlying.certificates
|
|
1055
|
+
)
|
|
1056
|
+
# Exact roots can often prove a sharper multiplicity directly (for example
|
|
1057
|
+
# when the base root itself is multiple), so prefer that over mechanical scaling.
|
|
1058
|
+
exact_certs = []
|
|
1059
|
+
for root in underlying.roots:
|
|
1060
|
+
multiplicity = _certified_multiplicity(expr, var, root)
|
|
1061
|
+
exact_certs.append(
|
|
1062
|
+
RootCertificate(
|
|
1063
|
+
sp.FiniteSet(root),
|
|
1064
|
+
1,
|
|
1065
|
+
"repeated_factor_exact_root",
|
|
1066
|
+
(("base", base), ("power", factor)),
|
|
1067
|
+
True,
|
|
1068
|
+
multiplicity,
|
|
1069
|
+
)
|
|
1070
|
+
)
|
|
1071
|
+
return RootIsolationResult(
|
|
1072
|
+
underlying.roots,
|
|
1073
|
+
underlying.zero_intervals,
|
|
1074
|
+
lifted_isolated,
|
|
1075
|
+
underlying.periodic,
|
|
1076
|
+
underlying.complete,
|
|
1077
|
+
"repeated_factor_root_isolation",
|
|
1078
|
+
(*underlying.evidence, ("repeated_factor", base, factor)),
|
|
1079
|
+
tuple(exact_certs) + lifted_certs,
|
|
1080
|
+
underlying.parametric_families,
|
|
1081
|
+
underlying.geometry,
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def _validated_even_multiplicity_numeric(expr, var, domain, *, max_samples=96):
|
|
1086
|
+
"""Discover touching roots numerically and promote only exact validations.
|
|
1087
|
+
|
|
1088
|
+
Numerical data is used solely to propose critical-point candidates. A candidate
|
|
1089
|
+
becomes a certified root only when exact substitution proves ``f(r) == 0`` and
|
|
1090
|
+
repeated differentiation proves an even multiplicity. Consequently this helper
|
|
1091
|
+
can improve coverage without treating a small residual as mathematical equality.
|
|
1092
|
+
"""
|
|
1093
|
+
if (
|
|
1094
|
+
not isinstance(domain, sp.Interval)
|
|
1095
|
+
or domain.start in (-sp.oo, sp.oo)
|
|
1096
|
+
or domain.end in (-sp.oo, sp.oo)
|
|
1097
|
+
):
|
|
1098
|
+
return None
|
|
1099
|
+
try:
|
|
1100
|
+
import mpmath as mp
|
|
1101
|
+
|
|
1102
|
+
a = mp.mpf(str(sp.N(domain.start, 50)))
|
|
1103
|
+
b = mp.mpf(str(sp.N(domain.end, 50)))
|
|
1104
|
+
if not (mp.isfinite(a) and mp.isfinite(b) and a < b):
|
|
1105
|
+
return None
|
|
1106
|
+
derivative = sp.diff(expr, var)
|
|
1107
|
+
f = sp.lambdify(var, expr, "mpmath")
|
|
1108
|
+
df = sp.lambdify(var, derivative, "mpmath")
|
|
1109
|
+
except (ImportError, *SYMBOLIC_QUERY_ERRORS):
|
|
1110
|
+
return None
|
|
1111
|
+
|
|
1112
|
+
old_dps = mp.mp.dps
|
|
1113
|
+
mp.mp.dps = max(old_dps, 80)
|
|
1114
|
+
try:
|
|
1115
|
+
xs = [a + (b - a) * i / max_samples for i in range(max_samples + 1)]
|
|
1116
|
+
candidates = []
|
|
1117
|
+
dvals = []
|
|
1118
|
+
for q in xs:
|
|
1119
|
+
try:
|
|
1120
|
+
value = df(q)
|
|
1121
|
+
dvals.append(mp.mpf(value) if mp.im(value) == 0 else mp.nan)
|
|
1122
|
+
except (ArithmeticError, ValueError, TypeError):
|
|
1123
|
+
dvals.append(mp.nan)
|
|
1124
|
+
for x1, x2, y1, y2 in zip(xs, xs[1:], dvals, dvals[1:], strict=False):
|
|
1125
|
+
if not (mp.isfinite(y1) and mp.isfinite(y2)):
|
|
1126
|
+
continue
|
|
1127
|
+
if y1 == 0:
|
|
1128
|
+
candidates.append(x1)
|
|
1129
|
+
if y1 * y2 < 0:
|
|
1130
|
+
try:
|
|
1131
|
+
candidates.append(mp.findroot(df, (x1, x2), solver="anderson"))
|
|
1132
|
+
except (ArithmeticError, ValueError, ZeroDivisionError):
|
|
1133
|
+
with contextlib.suppress(
|
|
1134
|
+
ArithmeticError, ValueError, ZeroDivisionError
|
|
1135
|
+
):
|
|
1136
|
+
candidates.append(mp.findroot(df, (x1, x2)))
|
|
1137
|
+
# Sample-local minima of |f| catch stationary roots whose derivative root is
|
|
1138
|
+
# numerically flat enough that a sign-change scan of f' misses it.
|
|
1139
|
+
fvals = []
|
|
1140
|
+
for q in xs:
|
|
1141
|
+
try:
|
|
1142
|
+
value = f(q)
|
|
1143
|
+
fvals.append(abs(value) if mp.im(value) == 0 else mp.inf)
|
|
1144
|
+
except (ArithmeticError, ValueError, TypeError):
|
|
1145
|
+
fvals.append(mp.inf)
|
|
1146
|
+
scale = max([v for v in fvals if mp.isfinite(v)] or [mp.mpf(1)])
|
|
1147
|
+
threshold = max(mp.mpf("1e-30"), scale * mp.mpf("1e-18"))
|
|
1148
|
+
for i in range(1, len(xs) - 1):
|
|
1149
|
+
if (
|
|
1150
|
+
fvals[i] <= fvals[i - 1]
|
|
1151
|
+
and fvals[i] <= fvals[i + 1]
|
|
1152
|
+
and fvals[i] <= threshold
|
|
1153
|
+
):
|
|
1154
|
+
with contextlib.suppress(
|
|
1155
|
+
ArithmeticError, ValueError, ZeroDivisionError
|
|
1156
|
+
):
|
|
1157
|
+
candidates.append(mp.findroot(df, (xs[i - 1], xs[i + 1])))
|
|
1158
|
+
|
|
1159
|
+
roots = []
|
|
1160
|
+
certs = []
|
|
1161
|
+
for candidate in candidates:
|
|
1162
|
+
if not mp.isfinite(candidate) or not (a <= candidate <= b):
|
|
1163
|
+
continue
|
|
1164
|
+
approx = sp.Float(str(candidate), 70)
|
|
1165
|
+
guesses = []
|
|
1166
|
+
for constants in ((), (sp.pi, sp.E, sp.sqrt(2), sp.sqrt(3), sp.log(2))):
|
|
1167
|
+
with contextlib.suppress(*SYMBOLIC_QUERY_ERRORS):
|
|
1168
|
+
guesses.append(
|
|
1169
|
+
sp.nsimplify(
|
|
1170
|
+
approx,
|
|
1171
|
+
constants=list(constants),
|
|
1172
|
+
tolerance=1e-28,
|
|
1173
|
+
full=bool(constants),
|
|
1174
|
+
)
|
|
1175
|
+
)
|
|
1176
|
+
# Keep the exact decimal rational as a final candidate. It almost never
|
|
1177
|
+
# validates, but makes the reconstruction policy explicit and deterministic.
|
|
1178
|
+
guesses.append(sp.Rational(str(candidate)))
|
|
1179
|
+
for root in guesses:
|
|
1180
|
+
try:
|
|
1181
|
+
if sp.Contains(root, domain) is S.false:
|
|
1182
|
+
continue
|
|
1183
|
+
if _exact_sign(expr.subs(var, root)) != 0:
|
|
1184
|
+
continue
|
|
1185
|
+
multiplicity = _certified_multiplicity(
|
|
1186
|
+
expr, var, root, max_order=16
|
|
1187
|
+
)
|
|
1188
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
1189
|
+
continue
|
|
1190
|
+
if multiplicity is None or multiplicity < 2 or multiplicity % 2:
|
|
1191
|
+
continue
|
|
1192
|
+
if root not in roots:
|
|
1193
|
+
roots.append(root)
|
|
1194
|
+
certs.append(
|
|
1195
|
+
RootCertificate(
|
|
1196
|
+
sp.FiniteSet(root),
|
|
1197
|
+
1,
|
|
1198
|
+
"numerical_candidate+exact_even_multiplicity_validation",
|
|
1199
|
+
(
|
|
1200
|
+
("approximation", approx),
|
|
1201
|
+
("derivative_candidate", derivative),
|
|
1202
|
+
),
|
|
1203
|
+
True,
|
|
1204
|
+
multiplicity,
|
|
1205
|
+
)
|
|
1206
|
+
)
|
|
1207
|
+
break
|
|
1208
|
+
if not roots:
|
|
1209
|
+
return None
|
|
1210
|
+
return RootIsolationResult(
|
|
1211
|
+
_sort_unique(roots),
|
|
1212
|
+
complete=False,
|
|
1213
|
+
method="validated_even_multiplicity_numeric_fallback",
|
|
1214
|
+
evidence=(("numeric_candidates", len(candidates)),),
|
|
1215
|
+
certificates=tuple(certs),
|
|
1216
|
+
)
|
|
1217
|
+
finally:
|
|
1218
|
+
mp.mp.dps = old_dps
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def _numeric(expr, var, domain, max_samples=80):
|
|
1222
|
+
if (
|
|
1223
|
+
not isinstance(domain, sp.Interval)
|
|
1224
|
+
or domain.start in (-sp.oo, sp.oo)
|
|
1225
|
+
or domain.end in (-sp.oo, sp.oo)
|
|
1226
|
+
):
|
|
1227
|
+
return None
|
|
1228
|
+
try:
|
|
1229
|
+
a = float(sp.N(domain.start))
|
|
1230
|
+
b = float(sp.N(domain.end))
|
|
1231
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
1232
|
+
return None
|
|
1233
|
+
if not math.isfinite(a) or not math.isfinite(b) or not a < b:
|
|
1234
|
+
return None
|
|
1235
|
+
import mpmath as mp
|
|
1236
|
+
|
|
1237
|
+
f = sp.lambdify(var, expr, "mpmath")
|
|
1238
|
+
roots = []
|
|
1239
|
+
xs = [a + (b - a) * i / max_samples for i in range(max_samples + 1)]
|
|
1240
|
+
vals = []
|
|
1241
|
+
for q in xs:
|
|
1242
|
+
try:
|
|
1243
|
+
vals.append(float(f(q)))
|
|
1244
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
1245
|
+
vals.append(float("nan"))
|
|
1246
|
+
for x1, x2, y1, y2 in zip(xs, xs[1:], vals, vals[1:], strict=False):
|
|
1247
|
+
if not (math.isfinite(y1) and math.isfinite(y2)):
|
|
1248
|
+
continue
|
|
1249
|
+
if y1 * y2 < 0:
|
|
1250
|
+
with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
|
|
1251
|
+
roots.append(sp.Float(str(mp.findroot(lambda t: f(t), (x1, x2))), 30))
|
|
1252
|
+
return RootIsolationResult(
|
|
1253
|
+
_sort_unique(roots),
|
|
1254
|
+
complete=False,
|
|
1255
|
+
method="numerical_scan",
|
|
1256
|
+
evidence=("candidate roots only",),
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def _exponential_fourier_count(expr, var, domain):
|
|
1261
|
+
"""Attach a certified derivative-sequence count for supported exp polynomials."""
|
|
1262
|
+
try:
|
|
1263
|
+
from .exp_fourier import exponential_fourier_count
|
|
1264
|
+
|
|
1265
|
+
count = exponential_fourier_count(expr, var, domain)
|
|
1266
|
+
except (ImportError, *SYMBOLIC_QUERY_ERRORS):
|
|
1267
|
+
return None
|
|
1268
|
+
if count is None or not count.complete or count.root_count is None:
|
|
1269
|
+
return None
|
|
1270
|
+
cert = RootCertificate(
|
|
1271
|
+
domain,
|
|
1272
|
+
count.root_count,
|
|
1273
|
+
"exponential_fourier_derivative_sequence",
|
|
1274
|
+
(count,),
|
|
1275
|
+
True,
|
|
1276
|
+
)
|
|
1277
|
+
if count.root_count == 0:
|
|
1278
|
+
return RootIsolationResult(
|
|
1279
|
+
complete=True,
|
|
1280
|
+
method="exponential_fourier_count",
|
|
1281
|
+
evidence=(count,),
|
|
1282
|
+
certificates=(cert,),
|
|
1283
|
+
)
|
|
1284
|
+
return RootIsolationResult(
|
|
1285
|
+
complete=False,
|
|
1286
|
+
method="exponential_fourier_count",
|
|
1287
|
+
evidence=(count,),
|
|
1288
|
+
certificates=(cert,),
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
def transcendental_roots(expr, var, domain=S.Reals, *, allow_numerical=True):
|
|
1293
|
+
expr = sp.sympify(expr)
|
|
1294
|
+
var = sp.sympify(var)
|
|
1295
|
+
if expr == 0:
|
|
1296
|
+
zi = (domain,) if isinstance(domain, sp.Interval) else ()
|
|
1297
|
+
c = RootCertificate(domain, None, "identically_zero", (expr,), True)
|
|
1298
|
+
return RootIsolationResult(
|
|
1299
|
+
(), zi, complete=True, method="identically_zero", certificates=(c,)
|
|
1300
|
+
)
|
|
1301
|
+
if not expr.has(var):
|
|
1302
|
+
c = RootCertificate(domain, 0, "constant", (expr,), True)
|
|
1303
|
+
return RootIsolationResult(
|
|
1304
|
+
(), complete=True, method="constant", certificates=(c,)
|
|
1305
|
+
)
|
|
1306
|
+
# General zero-geometry theorems precede generic solveset. They preserve
|
|
1307
|
+
# indexed families/interlacing even when individual zeros have no closed form.
|
|
1308
|
+
geom = zero_geometry(expr, var, domain=domain)
|
|
1309
|
+
if geom:
|
|
1310
|
+
exact_guard = all(_guard_proved(g.guard) for g in geom)
|
|
1311
|
+
support_ok = all(
|
|
1312
|
+
domain == g.support
|
|
1313
|
+
or (domain is S.Reals and g.family in ("AiryAi", "AiryBi"))
|
|
1314
|
+
for g in geom
|
|
1315
|
+
)
|
|
1316
|
+
priority = all(
|
|
1317
|
+
g.family in ("AiryAi", "AiryBi", "BesselJ", "BesselY", "FerrersQ")
|
|
1318
|
+
for g in geom
|
|
1319
|
+
)
|
|
1320
|
+
if priority and exact_guard and support_ok:
|
|
1321
|
+
certs = tuple(
|
|
1322
|
+
RootCertificate(
|
|
1323
|
+
domain,
|
|
1324
|
+
None,
|
|
1325
|
+
"zero_geometry",
|
|
1326
|
+
((g.source, g.count, g.support),),
|
|
1327
|
+
g.complete_on_support,
|
|
1328
|
+
)
|
|
1329
|
+
for g in geom
|
|
1330
|
+
)
|
|
1331
|
+
return RootIsolationResult(
|
|
1332
|
+
complete=all(g.complete_on_support for g in geom),
|
|
1333
|
+
method="zero_geometry",
|
|
1334
|
+
evidence=geom,
|
|
1335
|
+
certificates=certs,
|
|
1336
|
+
geometry=geom,
|
|
1337
|
+
)
|
|
1338
|
+
for fn in (
|
|
1339
|
+
_periodic,
|
|
1340
|
+
_special,
|
|
1341
|
+
_orthogonal_special,
|
|
1342
|
+
_repeated_power_roots,
|
|
1343
|
+
_identically_zero_interval,
|
|
1344
|
+
_solveset,
|
|
1345
|
+
_exponential_fourier_count,
|
|
1346
|
+
_certified_interval_resolution,
|
|
1347
|
+
_certified_monotone_interval,
|
|
1348
|
+
_certified_real_interval_isolation,
|
|
1349
|
+
):
|
|
1350
|
+
r = fn(expr, var, domain)
|
|
1351
|
+
if r is not None:
|
|
1352
|
+
if geom and not r.geometry:
|
|
1353
|
+
r = RootIsolationResult(
|
|
1354
|
+
r.roots,
|
|
1355
|
+
r.zero_intervals,
|
|
1356
|
+
r.isolated_intervals,
|
|
1357
|
+
r.periodic,
|
|
1358
|
+
r.complete,
|
|
1359
|
+
r.method,
|
|
1360
|
+
r.evidence,
|
|
1361
|
+
r.certificates,
|
|
1362
|
+
r.parametric_families,
|
|
1363
|
+
geom,
|
|
1364
|
+
)
|
|
1365
|
+
return r
|
|
1366
|
+
if geom:
|
|
1367
|
+
return RootIsolationResult(
|
|
1368
|
+
complete=False,
|
|
1369
|
+
method="conditional_zero_geometry",
|
|
1370
|
+
evidence=geom,
|
|
1371
|
+
geometry=geom,
|
|
1372
|
+
)
|
|
1373
|
+
if allow_numerical:
|
|
1374
|
+
r = _validated_even_multiplicity_numeric(expr, var, domain)
|
|
1375
|
+
if r is not None:
|
|
1376
|
+
return r
|
|
1377
|
+
r = _numeric(expr, var, domain)
|
|
1378
|
+
if r is not None:
|
|
1379
|
+
return r
|
|
1380
|
+
return RootIsolationResult(complete=False, method="unknown")
|