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,542 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import itertools
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import mpmath as mp
|
|
8
|
+
import sympy as sp
|
|
9
|
+
from sympy import S
|
|
10
|
+
|
|
11
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
12
|
+
from .analytic import AnalyticNeighborhoodCertificate, holomorphic_on_rectangle
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ComplexRectangle:
|
|
17
|
+
"""Closed axis-aligned complex rectangle with exact real bounds."""
|
|
18
|
+
|
|
19
|
+
re_min: sp.Expr
|
|
20
|
+
re_max: sp.Expr
|
|
21
|
+
im_min: sp.Expr
|
|
22
|
+
im_max: sp.Expr
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def lower_left(self):
|
|
26
|
+
return self.re_min + sp.I * self.im_min
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def upper_right(self):
|
|
30
|
+
return self.re_max + sp.I * self.im_max
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def center(self):
|
|
34
|
+
return (self.re_min + self.re_max) / 2 + sp.I * (self.im_min + self.im_max) / 2
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def width(self):
|
|
38
|
+
return sp.simplify(self.re_max - self.re_min)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def height(self):
|
|
42
|
+
return sp.simplify(self.im_max - self.im_min)
|
|
43
|
+
|
|
44
|
+
def as_pair(self):
|
|
45
|
+
return self.lower_left, self.upper_right
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ArgumentPrincipleCertificate:
|
|
50
|
+
rectangle: ComplexRectangle
|
|
51
|
+
winding_number: int | None
|
|
52
|
+
zero_count: int | None
|
|
53
|
+
pole_count: int | None = 0
|
|
54
|
+
certified: bool = False
|
|
55
|
+
method: str = "unknown"
|
|
56
|
+
boundary_segments: int = 0
|
|
57
|
+
precision: int | None = None
|
|
58
|
+
analytic_certificate: AnalyticNeighborhoodCertificate | None = None
|
|
59
|
+
evidence: tuple[object, ...] = ()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class CertifiedComplexRootRectangle:
|
|
64
|
+
rectangle: ComplexRectangle
|
|
65
|
+
root_count: int
|
|
66
|
+
certificate: ArgumentPrincipleCertificate
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class ComplexRootIsolationResult:
|
|
71
|
+
roots: tuple[sp.Expr, ...] = ()
|
|
72
|
+
rectangles: tuple[CertifiedComplexRootRectangle, ...] = ()
|
|
73
|
+
complete: bool = False
|
|
74
|
+
total_count: int | None = None
|
|
75
|
+
method: str = "unknown"
|
|
76
|
+
evidence: tuple[object, ...] = ()
|
|
77
|
+
certificates: tuple[ArgumentPrincipleCertificate, ...] = ()
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def certified(self):
|
|
81
|
+
return self.complete
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _truth(expr):
|
|
85
|
+
if expr in (True, S.true):
|
|
86
|
+
return True
|
|
87
|
+
if expr in (False, S.false):
|
|
88
|
+
return False
|
|
89
|
+
with contextlib.suppress(*SYMBOLIC_QUERY_ERRORS):
|
|
90
|
+
value = sp.simplify(expr)
|
|
91
|
+
if value is S.true:
|
|
92
|
+
return True
|
|
93
|
+
if value is S.false:
|
|
94
|
+
return False
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _normalize_rectangle(rectangle):
|
|
99
|
+
if isinstance(rectangle, ComplexRectangle):
|
|
100
|
+
rect = rectangle
|
|
101
|
+
else:
|
|
102
|
+
try:
|
|
103
|
+
a, b = map(sp.sympify, rectangle)
|
|
104
|
+
except (TypeError, ValueError):
|
|
105
|
+
return None
|
|
106
|
+
bounds = tuple(map(sp.simplify, (sp.re(a), sp.re(b), sp.im(a), sp.im(b))))
|
|
107
|
+
if not all(q.is_real is True for q in bounds):
|
|
108
|
+
return None
|
|
109
|
+
rect = ComplexRectangle(
|
|
110
|
+
sp.Min(bounds[0], bounds[1]),
|
|
111
|
+
sp.Max(bounds[0], bounds[1]),
|
|
112
|
+
sp.Min(bounds[2], bounds[3]),
|
|
113
|
+
sp.Max(bounds[2], bounds[3]),
|
|
114
|
+
)
|
|
115
|
+
if _truth(rect.re_min < rect.re_max) is not True:
|
|
116
|
+
return None
|
|
117
|
+
if _truth(rect.im_min < rect.im_max) is not True:
|
|
118
|
+
return None
|
|
119
|
+
return rect
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _contains(rect: ComplexRectangle, root):
|
|
123
|
+
root = sp.sympify(root)
|
|
124
|
+
checks = (
|
|
125
|
+
rect.re_min <= sp.re(root),
|
|
126
|
+
sp.re(root) <= rect.re_max,
|
|
127
|
+
rect.im_min <= sp.im(root),
|
|
128
|
+
sp.im(root) <= rect.im_max,
|
|
129
|
+
)
|
|
130
|
+
vals = tuple(_truth(q) for q in checks)
|
|
131
|
+
if all(v is True for v in vals):
|
|
132
|
+
return True
|
|
133
|
+
if any(v is False for v in vals):
|
|
134
|
+
return False
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _exact_roots(expr, var, rect):
|
|
139
|
+
with contextlib.suppress(*SYMBOLIC_QUERY_ERRORS):
|
|
140
|
+
solved = sp.solveset(sp.Eq(expr, 0), var, domain=S.Complexes)
|
|
141
|
+
if isinstance(solved, sp.FiniteSet):
|
|
142
|
+
roots = []
|
|
143
|
+
for root in solved:
|
|
144
|
+
inside = _contains(rect, root)
|
|
145
|
+
if inside is None:
|
|
146
|
+
return None
|
|
147
|
+
if inside:
|
|
148
|
+
roots.append(root)
|
|
149
|
+
return tuple(sorted(set(roots), key=sp.default_sort_key))
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _iv_modules():
|
|
154
|
+
names = (
|
|
155
|
+
"sin",
|
|
156
|
+
"cos",
|
|
157
|
+
"tan",
|
|
158
|
+
"asin",
|
|
159
|
+
"acos",
|
|
160
|
+
"atan",
|
|
161
|
+
"sinh",
|
|
162
|
+
"cosh",
|
|
163
|
+
"tanh",
|
|
164
|
+
"asinh",
|
|
165
|
+
"acosh",
|
|
166
|
+
"atanh",
|
|
167
|
+
"exp",
|
|
168
|
+
"log",
|
|
169
|
+
"sqrt",
|
|
170
|
+
"erf",
|
|
171
|
+
"erfc",
|
|
172
|
+
)
|
|
173
|
+
return {name: getattr(mp.iv, name) for name in names if hasattr(mp.iv, name)}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _interval_function(expr, var):
|
|
177
|
+
try:
|
|
178
|
+
return sp.lambdify(var, expr, modules=[_iv_modules(), "mpmath"])
|
|
179
|
+
except (TypeError, ValueError, NameError):
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _mp_number(value, dps):
|
|
184
|
+
return mp.mpf(str(sp.N(value, dps + 10)))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _segment_box(z0, z1, dps):
|
|
188
|
+
r0, r1 = sorted((_mp_number(sp.re(z0), dps), _mp_number(sp.re(z1), dps)))
|
|
189
|
+
i0, i1 = sorted((_mp_number(sp.im(z0), dps), _mp_number(sp.im(z1), dps)))
|
|
190
|
+
return mp.iv.mpc([r0, r1], [i0, i1])
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _image_excludes_zero(value):
|
|
194
|
+
try:
|
|
195
|
+
return not (0 in value.real and 0 in value.imag)
|
|
196
|
+
except (TypeError, ValueError, AttributeError):
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _point_value(function, z, dps):
|
|
201
|
+
box = _segment_box(z, z, dps)
|
|
202
|
+
value = function(box)
|
|
203
|
+
if not hasattr(value, "real"):
|
|
204
|
+
value = mp.iv.mpc(value)
|
|
205
|
+
# The midpoint of an interval point evaluation is a valid representative of
|
|
206
|
+
# its convex enclosure. It is used only after the containing boundary image
|
|
207
|
+
# has been certified zero-free.
|
|
208
|
+
re = (mp.mpf(value.real.a) + mp.mpf(value.real.b)) / 2
|
|
209
|
+
im = (mp.mpf(value.imag.a) + mp.mpf(value.imag.b)) / 2
|
|
210
|
+
return mp.mpc(re, im)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _certify_segment(function, z0, z1, *, dps, depth, max_depth, vertices):
|
|
214
|
+
try:
|
|
215
|
+
image = function(_segment_box(z0, z1, dps))
|
|
216
|
+
if not hasattr(image, "real"):
|
|
217
|
+
image = mp.iv.mpc(image)
|
|
218
|
+
except (ArithmeticError, TypeError, ValueError, ZeroDivisionError):
|
|
219
|
+
return False
|
|
220
|
+
if _image_excludes_zero(image):
|
|
221
|
+
vertices.append(z1)
|
|
222
|
+
return True
|
|
223
|
+
if depth >= max_depth:
|
|
224
|
+
return False
|
|
225
|
+
mid = sp.simplify((z0 + z1) / 2)
|
|
226
|
+
return _certify_segment(
|
|
227
|
+
function,
|
|
228
|
+
z0,
|
|
229
|
+
mid,
|
|
230
|
+
dps=dps,
|
|
231
|
+
depth=depth + 1,
|
|
232
|
+
max_depth=max_depth,
|
|
233
|
+
vertices=vertices,
|
|
234
|
+
) and _certify_segment(
|
|
235
|
+
function,
|
|
236
|
+
mid,
|
|
237
|
+
z1,
|
|
238
|
+
dps=dps,
|
|
239
|
+
depth=depth + 1,
|
|
240
|
+
max_depth=max_depth,
|
|
241
|
+
vertices=vertices,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _boundary_vertices(rect, function, *, dps, max_depth):
|
|
246
|
+
a = rect.re_min
|
|
247
|
+
b = rect.re_max
|
|
248
|
+
c = rect.im_min
|
|
249
|
+
d = rect.im_max
|
|
250
|
+
corners = (
|
|
251
|
+
a + sp.I * c,
|
|
252
|
+
b + sp.I * c,
|
|
253
|
+
b + sp.I * d,
|
|
254
|
+
a + sp.I * d,
|
|
255
|
+
a + sp.I * c,
|
|
256
|
+
)
|
|
257
|
+
vertices = [corners[0]]
|
|
258
|
+
for z0, z1 in itertools.pairwise(corners):
|
|
259
|
+
if not _certify_segment(
|
|
260
|
+
function,
|
|
261
|
+
z0,
|
|
262
|
+
z1,
|
|
263
|
+
dps=dps,
|
|
264
|
+
depth=0,
|
|
265
|
+
max_depth=max_depth,
|
|
266
|
+
vertices=vertices,
|
|
267
|
+
):
|
|
268
|
+
return None
|
|
269
|
+
return tuple(vertices)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _polygon_winding(values):
|
|
273
|
+
total = mp.mpf("0")
|
|
274
|
+
for a, b in itertools.pairwise(values):
|
|
275
|
+
if a == 0 or b == 0:
|
|
276
|
+
return None
|
|
277
|
+
cross = a.real * b.imag - a.imag * b.real
|
|
278
|
+
dot = a.real * b.real + a.imag * b.imag
|
|
279
|
+
total += mp.atan2(cross, dot)
|
|
280
|
+
turns = total / (2 * mp.pi)
|
|
281
|
+
winding = int(mp.nint(turns))
|
|
282
|
+
if abs(turns - winding) > mp.mpf("1e-20"):
|
|
283
|
+
return None
|
|
284
|
+
return winding
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _numerical_winding(expr, var, rect, *, dps=60, samples=256):
|
|
288
|
+
try:
|
|
289
|
+
f = sp.lambdify(var, expr, "mpmath")
|
|
290
|
+
a = complex(float(sp.N(rect.re_min)), float(sp.N(rect.im_min)))
|
|
291
|
+
b = complex(float(sp.N(rect.re_max)), float(sp.N(rect.im_min)))
|
|
292
|
+
c = complex(float(sp.N(rect.re_max)), float(sp.N(rect.im_max)))
|
|
293
|
+
d = complex(float(sp.N(rect.re_min)), float(sp.N(rect.im_max)))
|
|
294
|
+
except (TypeError, ValueError, NameError):
|
|
295
|
+
return None
|
|
296
|
+
old = mp.mp.dps
|
|
297
|
+
mp.mp.dps = dps
|
|
298
|
+
try:
|
|
299
|
+
points = []
|
|
300
|
+
per_edge = max(8, samples // 4)
|
|
301
|
+
for start, stop in ((a, b), (b, c), (c, d), (d, a)):
|
|
302
|
+
for k in range(per_edge):
|
|
303
|
+
t = k / per_edge
|
|
304
|
+
points.append(start + t * (stop - start))
|
|
305
|
+
points.append(a)
|
|
306
|
+
values = []
|
|
307
|
+
for z in points:
|
|
308
|
+
try:
|
|
309
|
+
values.append(complex(f(z)))
|
|
310
|
+
except (ArithmeticError, TypeError, ValueError, ZeroDivisionError):
|
|
311
|
+
return None
|
|
312
|
+
return _polygon_winding(values)
|
|
313
|
+
finally:
|
|
314
|
+
mp.mp.dps = old
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def argument_principle_count(
|
|
318
|
+
expr,
|
|
319
|
+
var,
|
|
320
|
+
rectangle,
|
|
321
|
+
*,
|
|
322
|
+
precision=60,
|
|
323
|
+
max_boundary_depth=14,
|
|
324
|
+
):
|
|
325
|
+
"""Count zeros in a rectangle by a validated argument-principle contour.
|
|
326
|
+
|
|
327
|
+
A positive certificate requires a symbolic proof of holomorphy on the closed
|
|
328
|
+
rectangle and interval-arithmetic enclosures of every boundary segment that
|
|
329
|
+
exclude zero. If validation fails, a numerical winding estimate may be
|
|
330
|
+
returned as evidence but ``certified`` remains false.
|
|
331
|
+
"""
|
|
332
|
+
expr = sp.sympify(expr)
|
|
333
|
+
var = sp.sympify(var)
|
|
334
|
+
rect = _normalize_rectangle(rectangle)
|
|
335
|
+
if rect is None:
|
|
336
|
+
return ArgumentPrincipleCertificate(
|
|
337
|
+
ComplexRectangle(S.Zero, S.One, S.Zero, S.One),
|
|
338
|
+
None,
|
|
339
|
+
None,
|
|
340
|
+
certified=False,
|
|
341
|
+
method="invalid_rectangle",
|
|
342
|
+
)
|
|
343
|
+
analytic = holomorphic_on_rectangle(expr, var, rect.as_pair())
|
|
344
|
+
function = _interval_function(expr, var)
|
|
345
|
+
if analytic.holomorphic is True and function is not None:
|
|
346
|
+
old_iv_dps = mp.iv.dps
|
|
347
|
+
old_mp_dps = mp.mp.dps
|
|
348
|
+
mp.iv.dps = int(precision)
|
|
349
|
+
mp.mp.dps = int(precision)
|
|
350
|
+
try:
|
|
351
|
+
vertices = _boundary_vertices(
|
|
352
|
+
rect,
|
|
353
|
+
function,
|
|
354
|
+
dps=int(precision),
|
|
355
|
+
max_depth=max_boundary_depth,
|
|
356
|
+
)
|
|
357
|
+
if vertices is not None:
|
|
358
|
+
try:
|
|
359
|
+
values = tuple(
|
|
360
|
+
_point_value(function, z, int(precision)) for z in vertices
|
|
361
|
+
)
|
|
362
|
+
winding = _polygon_winding(values)
|
|
363
|
+
except (ArithmeticError, TypeError, ValueError, ZeroDivisionError):
|
|
364
|
+
winding = None
|
|
365
|
+
if winding is not None and winding >= 0:
|
|
366
|
+
return ArgumentPrincipleCertificate(
|
|
367
|
+
rect,
|
|
368
|
+
winding,
|
|
369
|
+
winding,
|
|
370
|
+
0,
|
|
371
|
+
True,
|
|
372
|
+
"validated_interval_argument_principle",
|
|
373
|
+
len(vertices) - 1,
|
|
374
|
+
int(precision),
|
|
375
|
+
analytic,
|
|
376
|
+
(("boundary_zero_free", True),),
|
|
377
|
+
)
|
|
378
|
+
finally:
|
|
379
|
+
mp.iv.dps = old_iv_dps
|
|
380
|
+
mp.mp.dps = old_mp_dps
|
|
381
|
+
estimate = _numerical_winding(expr, var, rect, dps=int(precision))
|
|
382
|
+
return ArgumentPrincipleCertificate(
|
|
383
|
+
rect,
|
|
384
|
+
estimate,
|
|
385
|
+
None,
|
|
386
|
+
0 if analytic.holomorphic is True else None,
|
|
387
|
+
False,
|
|
388
|
+
"numerical_argument_principle_estimate",
|
|
389
|
+
0,
|
|
390
|
+
int(precision),
|
|
391
|
+
analytic,
|
|
392
|
+
(("winding_estimate", estimate),),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _children(rect, rx, ry):
|
|
397
|
+
x = sp.simplify(rect.re_min + rx * (rect.re_max - rect.re_min))
|
|
398
|
+
y = sp.simplify(rect.im_min + ry * (rect.im_max - rect.im_min))
|
|
399
|
+
return (
|
|
400
|
+
ComplexRectangle(rect.re_min, x, rect.im_min, y),
|
|
401
|
+
ComplexRectangle(x, rect.re_max, rect.im_min, y),
|
|
402
|
+
ComplexRectangle(x, rect.re_max, y, rect.im_max),
|
|
403
|
+
ComplexRectangle(rect.re_min, x, y, rect.im_max),
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def isolate_complex_roots(
|
|
408
|
+
expr,
|
|
409
|
+
var,
|
|
410
|
+
rectangle,
|
|
411
|
+
*,
|
|
412
|
+
precision=60,
|
|
413
|
+
max_boundary_depth=14,
|
|
414
|
+
max_isolation_depth=10,
|
|
415
|
+
):
|
|
416
|
+
"""Return certified complex root rectangles inside ``rectangle``.
|
|
417
|
+
|
|
418
|
+
Rectangles with count one isolate a unique simple zero. A terminal rectangle
|
|
419
|
+
with count greater than one is a certified cluster counted with multiplicity.
|
|
420
|
+
Completeness means that the certified rectangle counts add to the certified
|
|
421
|
+
argument-principle count of the original rectangle.
|
|
422
|
+
"""
|
|
423
|
+
expr = sp.sympify(expr)
|
|
424
|
+
var = sp.sympify(var)
|
|
425
|
+
rect = _normalize_rectangle(rectangle)
|
|
426
|
+
if rect is None:
|
|
427
|
+
return ComplexRootIsolationResult(method="invalid_rectangle")
|
|
428
|
+
|
|
429
|
+
exact = _exact_roots(expr, var, rect)
|
|
430
|
+
if exact is not None:
|
|
431
|
+
return ComplexRootIsolationResult(
|
|
432
|
+
exact,
|
|
433
|
+
complete=True,
|
|
434
|
+
total_count=len(exact),
|
|
435
|
+
method="exact_complex_roots",
|
|
436
|
+
evidence=(("rectangle", rect),),
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
cache = {}
|
|
440
|
+
|
|
441
|
+
def count(r):
|
|
442
|
+
key = (r.re_min, r.re_max, r.im_min, r.im_max)
|
|
443
|
+
if key not in cache:
|
|
444
|
+
cache[key] = argument_principle_count(
|
|
445
|
+
expr,
|
|
446
|
+
var,
|
|
447
|
+
r,
|
|
448
|
+
precision=precision,
|
|
449
|
+
max_boundary_depth=max_boundary_depth,
|
|
450
|
+
)
|
|
451
|
+
return cache[key]
|
|
452
|
+
|
|
453
|
+
parent = count(rect)
|
|
454
|
+
if not parent.certified or parent.zero_count is None:
|
|
455
|
+
return ComplexRootIsolationResult(
|
|
456
|
+
complete=False,
|
|
457
|
+
total_count=None,
|
|
458
|
+
method="uncertified_complex_isolation",
|
|
459
|
+
evidence=(parent,),
|
|
460
|
+
certificates=(parent,),
|
|
461
|
+
)
|
|
462
|
+
if parent.zero_count == 0:
|
|
463
|
+
return ComplexRootIsolationResult(
|
|
464
|
+
complete=True,
|
|
465
|
+
total_count=0,
|
|
466
|
+
method="validated_argument_principle",
|
|
467
|
+
evidence=(parent,),
|
|
468
|
+
certificates=(parent,),
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
leaves = []
|
|
472
|
+
certs = [parent]
|
|
473
|
+
split_ratios = (
|
|
474
|
+
(sp.Rational(1, 2), sp.Rational(1, 2)),
|
|
475
|
+
(sp.Rational(2, 5), sp.Rational(2, 5)),
|
|
476
|
+
(sp.Rational(3, 5), sp.Rational(3, 5)),
|
|
477
|
+
(sp.Rational(2, 5), sp.Rational(3, 5)),
|
|
478
|
+
(sp.Rational(3, 5), sp.Rational(2, 5)),
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
def rec(r, cert, depth):
|
|
482
|
+
n = cert.zero_count
|
|
483
|
+
if n is None or n == 0:
|
|
484
|
+
return True
|
|
485
|
+
if n == 1:
|
|
486
|
+
leaves.append(CertifiedComplexRootRectangle(r, 1, cert))
|
|
487
|
+
return True
|
|
488
|
+
if depth >= max_isolation_depth:
|
|
489
|
+
leaves.append(CertifiedComplexRootRectangle(r, n, cert))
|
|
490
|
+
return True
|
|
491
|
+
for rx, ry in split_ratios:
|
|
492
|
+
child_rects = _children(r, rx, ry)
|
|
493
|
+
child_certs = tuple(count(q) for q in child_rects)
|
|
494
|
+
if not all(c.certified and c.zero_count is not None for c in child_certs):
|
|
495
|
+
continue
|
|
496
|
+
if sum(c.zero_count for c in child_certs) != n:
|
|
497
|
+
continue
|
|
498
|
+
certs.extend(child_certs)
|
|
499
|
+
return all(
|
|
500
|
+
rec(q, c, depth + 1)
|
|
501
|
+
for q, c in zip(child_rects, child_certs, strict=True)
|
|
502
|
+
)
|
|
503
|
+
leaves.append(CertifiedComplexRootRectangle(r, n, cert))
|
|
504
|
+
return True
|
|
505
|
+
|
|
506
|
+
complete = rec(rect, parent, 0)
|
|
507
|
+
return ComplexRootIsolationResult(
|
|
508
|
+
rectangles=tuple(leaves),
|
|
509
|
+
complete=complete,
|
|
510
|
+
total_count=parent.zero_count,
|
|
511
|
+
method="validated_complex_rectangle_isolation",
|
|
512
|
+
evidence=(parent,),
|
|
513
|
+
certificates=tuple(certs),
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def function_complex_zeros(
|
|
518
|
+
expr,
|
|
519
|
+
var,
|
|
520
|
+
*,
|
|
521
|
+
rectangle,
|
|
522
|
+
return_result=False,
|
|
523
|
+
precision=60,
|
|
524
|
+
max_boundary_depth=14,
|
|
525
|
+
max_isolation_depth=10,
|
|
526
|
+
):
|
|
527
|
+
"""Return zeros in a closed complex rectangle, with certification when needed."""
|
|
528
|
+
result = isolate_complex_roots(
|
|
529
|
+
expr,
|
|
530
|
+
var,
|
|
531
|
+
rectangle,
|
|
532
|
+
precision=precision,
|
|
533
|
+
max_boundary_depth=max_boundary_depth,
|
|
534
|
+
max_isolation_depth=max_isolation_depth,
|
|
535
|
+
)
|
|
536
|
+
if return_result:
|
|
537
|
+
return result
|
|
538
|
+
if result.complete and result.roots:
|
|
539
|
+
return sp.FiniteSet(*result.roots)
|
|
540
|
+
if result.complete and result.total_count == 0:
|
|
541
|
+
return S.EmptySet
|
|
542
|
+
return result
|