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.
Files changed (56) hide show
  1. funcprops/__init__.py +234 -0
  2. funcprops/_semialg.py +246 -0
  3. funcprops/api.py +70 -0
  4. funcprops/branchcuts/__init__.py +141 -0
  5. funcprops/branchcuts/computation.py +664 -0
  6. funcprops/branchcuts/core.py +1204 -0
  7. funcprops/branchcuts/post_simplify.py +461 -0
  8. funcprops/conditions.py +124 -0
  9. funcprops/data/catalogue.json +127449 -0
  10. funcprops/data/dlmf_audit.json +3382 -0
  11. funcprops/dlmf.py +29 -0
  12. funcprops/engine.py +324 -0
  13. funcprops/exceptions.py +29 -0
  14. funcprops/proof_cache.py +373 -0
  15. funcprops/real_analysis/__init__.py +52 -0
  16. funcprops/real_analysis/advanced_convexity.py +217 -0
  17. funcprops/real_analysis/boundedness.py +168 -0
  18. funcprops/real_analysis/common.py +244 -0
  19. funcprops/real_analysis/convexity.py +698 -0
  20. funcprops/real_analysis/differentiability.py +112 -0
  21. funcprops/real_analysis/domain.py +188 -0
  22. funcprops/real_analysis/mapping.py +374 -0
  23. funcprops/real_analysis/monotonic_certificates.py +867 -0
  24. funcprops/real_analysis/monotonicity.py +673 -0
  25. funcprops/real_analysis/oscillation.py +151 -0
  26. funcprops/real_analysis/piecewise.py +174 -0
  27. funcprops/real_analysis/range.py +552 -0
  28. funcprops/real_analysis/sign.py +561 -0
  29. funcprops/real_analysis/smoothness.py +268 -0
  30. funcprops/real_analysis/special_functions.py +515 -0
  31. funcprops/real_analysis/structural_properties.py +558 -0
  32. funcprops/real_analysis/zeros.py +327 -0
  33. funcprops/registry.py +274 -0
  34. funcprops/results.py +239 -0
  35. funcprops/singularities.py +1767 -0
  36. funcprops/stratification.py +494 -0
  37. funcprops/sympy_compare.py +63 -0
  38. funcprops/transcendental/__init__.py +140 -0
  39. funcprops/transcendental/algebraize.py +201 -0
  40. funcprops/transcendental/analytic.py +261 -0
  41. funcprops/transcendental/complex_roots.py +542 -0
  42. funcprops/transcendental/exp_fourier.py +221 -0
  43. funcprops/transcendental/extrema.py +821 -0
  44. funcprops/transcendental/periodic.py +222 -0
  45. funcprops/transcendental/proof.py +104 -0
  46. funcprops/transcendental/real_interval.py +128 -0
  47. funcprops/transcendental/real_sign.py +117 -0
  48. funcprops/transcendental/reduce.py +288 -0
  49. funcprops/transcendental/roots.py +1380 -0
  50. funcprops/transcendental/zero_geometry.py +1197 -0
  51. funcprops/verification.py +410 -0
  52. funcprops-0.1.0.dist-info/METADATA +208 -0
  53. funcprops-0.1.0.dist-info/RECORD +56 -0
  54. funcprops-0.1.0.dist-info/WHEEL +5 -0
  55. funcprops-0.1.0.dist-info/licenses/LICENSE +674 -0
  56. 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