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,201 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import sympy as sp
6
+ from sympy import S
7
+
8
+ from ..exceptions import SYMBOLIC_QUERY_ERRORS
9
+ from ..real_analysis.common import is_semialgebraic_expression
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class AlgebraizationResult:
14
+ expression: sp.Expr
15
+ substitutions: tuple[tuple[sp.Symbol, sp.Expr], ...]
16
+ constraints: sp.Basic = S.true
17
+ exact: bool = True
18
+ method: str = "none"
19
+ source_domain: object = S.Reals
20
+ relations: tuple[sp.Basic, ...] = ()
21
+ inverse_substitutions: tuple[tuple[sp.Symbol, sp.Expr], ...] = ()
22
+
23
+ @property
24
+ def changed(self):
25
+ return bool(self.substitutions)
26
+
27
+
28
+ def _linear_coeff(expr, var):
29
+ try:
30
+ p = sp.Poly(sp.expand(expr), var)
31
+ if p.degree() <= 1:
32
+ return p.nth(1), p.nth(0)
33
+ except SYMBOLIC_QUERY_ERRORS:
34
+ pass
35
+ return None
36
+
37
+
38
+ def _exp_generator(expr, var):
39
+ exps = list(expr.atoms(sp.exp))
40
+ if not exps:
41
+ return None
42
+ coeffs = []
43
+ for e in exps:
44
+ lc = _linear_coeff(e.args[0], var)
45
+ if lc is None:
46
+ return None
47
+ a, b = lc
48
+ if a == 0 or a.is_rational is not True or b.is_real is not True:
49
+ return None
50
+ coeffs.append((e, a, b))
51
+ slopes = [sp.Rational(a) for _, a, _ in coeffs]
52
+ from math import gcd, lcm
53
+
54
+ L = 1
55
+ for a in slopes:
56
+ L = lcm(L, int(a.q))
57
+ ints = [int(a * L) for a in slopes]
58
+ g = abs(ints[0])
59
+ for n in ints[1:]:
60
+ g = gcd(g, abs(n))
61
+ slope = sp.Rational(g, L)
62
+ return coeffs, slope if slope else None
63
+
64
+
65
+ def algebraize_exponentials(expr, var):
66
+ expr = sp.sympify(expr)
67
+ data = _exp_generator(expr, var)
68
+ if data is None:
69
+ return AlgebraizationResult(expr, ())
70
+ coeffs, slope = data
71
+ u = sp.Dummy("u", real=True)
72
+ repl = {}
73
+ for e, a, b in coeffs:
74
+ power = sp.simplify(a / slope)
75
+ if power.is_Integer is not True:
76
+ return AlgebraizationResult(expr, ())
77
+ repl[e] = sp.exp(b) * u**power
78
+ new = sp.cancel(expr.xreplace(repl))
79
+ if not is_semialgebraic_expression(new, u):
80
+ return AlgebraizationResult(expr, ())
81
+ return AlgebraizationResult(
82
+ new,
83
+ ((u, sp.exp(slope * var)),),
84
+ u > 0,
85
+ True,
86
+ "exponential_substitution",
87
+ S.Reals,
88
+ (),
89
+ ((var, sp.log(u) / slope),),
90
+ )
91
+
92
+
93
+ def rewrite_hyperbolic_to_exp(expr):
94
+ expr = sp.sympify(expr)
95
+ if expr.has(sp.sinh, sp.cosh, sp.tanh, sp.coth, sp.sech, sp.csch):
96
+ try:
97
+ return sp.cancel(expr.rewrite(sp.exp))
98
+ except SYMBOLIC_QUERY_ERRORS:
99
+ return expr
100
+ return expr
101
+
102
+
103
+ def algebraize_trigonometric(expr, var, *, source_domain=None):
104
+ """Tangent-half-angle algebraization on a single branch.
105
+
106
+ Exact on ``(-pi, pi)`` (or a supplied subset thereof):
107
+ ``u=tan(x/2)``, ``sin(x)=2u/(1+u^2)``, ``cos(x)=(1-u^2)/(1+u^2)``.
108
+ """
109
+ expr = sp.sympify(expr)
110
+ var = sp.sympify(var)
111
+ if not expr.has(sp.sin(var), sp.cos(var)):
112
+ return AlgebraizationResult(expr, ())
113
+ # Only direct sin(x), cos(x); affine phases are handled by periodic reduction first.
114
+ bad = [a for a in expr.atoms(sp.sin, sp.cos) if a.args[0] != var]
115
+ if bad:
116
+ return AlgebraizationResult(expr, ())
117
+ u = sp.Dummy("t", real=True)
118
+ new = expr.xreplace(
119
+ {sp.sin(var): 2 * u / (1 + u**2), sp.cos(var): (1 - u**2) / (1 + u**2)}
120
+ )
121
+ new = sp.cancel(new)
122
+ # rational in u is semialgebraic away from its denominator; 1+u^2 is positive.
123
+ dom = (
124
+ sp.Interval.open(-sp.pi, sp.pi)
125
+ if source_domain is None
126
+ else sp.Intersection(sp.sympify(source_domain), sp.Interval.open(-sp.pi, sp.pi))
127
+ )
128
+ return AlgebraizationResult(
129
+ new,
130
+ ((u, sp.tan(var / 2)),),
131
+ S.true,
132
+ True,
133
+ "tangent_half_angle",
134
+ dom,
135
+ (),
136
+ ((var, 2 * sp.atan(u)),),
137
+ )
138
+
139
+
140
+ def algebraize_trig_circle(expr, var):
141
+ """Algebraize direct sin/cos dependence over a full phase using s^2+c^2=1.
142
+
143
+ This transform is exact for existential range/sign questions over a full
144
+ period, but callers must respect the returned relation.
145
+ """
146
+ expr = sp.sympify(expr)
147
+ var = sp.sympify(var)
148
+ if not (expr.has(sp.sin(var)) or expr.has(sp.cos(var))):
149
+ return AlgebraizationResult(expr, ())
150
+ if any(a.args[0] != var for a in expr.atoms(sp.sin, sp.cos)):
151
+ return AlgebraizationResult(expr, ())
152
+ s = sp.Dummy("s", real=True)
153
+ c = sp.Dummy("c", real=True)
154
+ new = expr.xreplace({sp.sin(var): s, sp.cos(var): c})
155
+ return AlgebraizationResult(
156
+ new,
157
+ ((s, sp.sin(var)), (c, sp.cos(var))),
158
+ sp.Eq(s**2 + c**2, 1),
159
+ True,
160
+ "trig_circle",
161
+ sp.Interval.Ropen(0, 2 * sp.pi),
162
+ (sp.Eq(s**2 + c**2, 1),),
163
+ )
164
+
165
+
166
+ def algebraize_common_subexpressions(expr, var):
167
+ expr = sp.sympify(expr)
168
+ counts = {}
169
+ for node in sp.preorder_traversal(expr):
170
+ if isinstance(node, sp.Function) and node.has(var):
171
+ counts[node] = counts.get(node, 0) + 1
172
+ repeated = [e for e, n in counts.items() if n > 1]
173
+ if not repeated:
174
+ return AlgebraizationResult(expr, ())
175
+ repl = {
176
+ e: sp.Dummy(f"t{i}", real=True)
177
+ for i, e in enumerate(sorted(repeated, key=sp.default_sort_key), 1)
178
+ }
179
+ return AlgebraizationResult(
180
+ expr.xreplace(repl),
181
+ tuple((v, k) for k, v in repl.items()),
182
+ S.true,
183
+ False,
184
+ "common_subexpression",
185
+ )
186
+
187
+
188
+ def algebraize(expr, var, *, domain=None, allow_trig=False):
189
+ expr = sp.sympify(expr)
190
+ var = sp.sympify(var)
191
+ if is_semialgebraic_expression(expr, var):
192
+ return AlgebraizationResult(expr, ())
193
+ rewritten = rewrite_hyperbolic_to_exp(expr)
194
+ r = algebraize_exponentials(rewritten, var)
195
+ if r.changed:
196
+ return r
197
+ if allow_trig:
198
+ r = algebraize_trigonometric(expr, var, source_domain=domain)
199
+ if r.changed:
200
+ return r
201
+ return AlgebraizationResult(expr, ())
@@ -0,0 +1,261 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import sympy as sp
6
+ from sympy import S
7
+
8
+ from ..exceptions import SYMBOLIC_QUERY_ERRORS
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class AnalyticNeighborhoodCertificate:
13
+ holomorphic: bool | None
14
+ region: object
15
+ method: str = "unknown"
16
+ obstructions: tuple[object, ...] = ()
17
+ evidence: tuple[object, ...] = ()
18
+
19
+ @property
20
+ def certified(self):
21
+ return self.holomorphic is not None
22
+
23
+
24
+ _ENTIRE = (sp.exp, sp.sin, sp.cos, sp.sinh, sp.cosh, sp.erf, sp.erfc, sp.erfi)
25
+
26
+
27
+ def _truth3(expr):
28
+ if expr in (True, S.true):
29
+ return True
30
+ if expr in (False, S.false):
31
+ return False
32
+ try:
33
+ q = sp.simplify(expr)
34
+ if q is S.true:
35
+ return True
36
+ if q is S.false:
37
+ return False
38
+ except SYMBOLIC_QUERY_ERRORS:
39
+ pass
40
+ return None
41
+
42
+
43
+ def _all3(values):
44
+ unknown = False
45
+ for v in values:
46
+ if v is False:
47
+ return False
48
+ if v is None:
49
+ unknown = True
50
+ return None if unknown else True
51
+
52
+
53
+ def _rect_contains(rect, z):
54
+ a, b = rect
55
+ z = sp.sympify(z)
56
+ tests = (
57
+ sp.Le(sp.re(a), sp.re(z)),
58
+ sp.Le(sp.re(z), sp.re(b)),
59
+ sp.Le(sp.im(a), sp.im(z)),
60
+ sp.Le(sp.im(z), sp.im(b)),
61
+ )
62
+ vals = [_truth3(t) for t in tests]
63
+ if any(v is False for v in vals):
64
+ return False
65
+ if all(v is True for v in vals):
66
+ return True
67
+ return None
68
+
69
+
70
+ def _affine_data(g, var):
71
+ try:
72
+ p = sp.Poly(sp.expand(g), var)
73
+ if p.degree() > 1:
74
+ return None
75
+ c = p.nth(1)
76
+ d = p.nth(0)
77
+ if c.has(var) or d.has(var):
78
+ return None
79
+ return c, d
80
+ except SYMBOLIC_QUERY_ERRORS:
81
+ return None
82
+
83
+
84
+ def _affine_image_bounds(g, var, rect):
85
+ # exact rectangular enclosure for affine g(z)=c*z+d
86
+ cd = _affine_data(g, var)
87
+ if cd is None:
88
+ return None
89
+ c, d = cd
90
+ try:
91
+ a, b = rect
92
+ corners = [
93
+ sp.re(a) + sp.I * sp.im(a),
94
+ sp.re(a) + sp.I * sp.im(b),
95
+ sp.re(b) + sp.I * sp.im(a),
96
+ sp.re(b) + sp.I * sp.im(b),
97
+ ]
98
+ vals = [sp.expand_complex(c * q + d) for q in corners]
99
+ rs = [sp.re(v) for v in vals]
100
+ ims = [sp.im(v) for v in vals]
101
+ if not all(v.is_real is True for v in rs + ims):
102
+ return None
103
+ # default_sort_key is syntactic, not numerical ordering. Min/Max keep
104
+ # the bound exact and simplify when comparisons are decidable.
105
+ return sp.Min(*rs), sp.Max(*rs), sp.Min(*ims), sp.Max(*ims)
106
+ except SYMBOLIC_QUERY_ERRORS:
107
+ return None
108
+
109
+
110
+ def _avoids_negative_real_axis(g, var, rect):
111
+ b = _affine_image_bounds(g, var, rect)
112
+ if b is None:
113
+ return None
114
+ r0, _r1, i0, i1 = b
115
+ # definitely above/below real axis, or entirely right of 0
116
+ if i0.is_positive is True or i1.is_negative is True or r0.is_positive is True:
117
+ return True
118
+ intersects_box = (
119
+ r0.is_nonpositive is True
120
+ and i0.is_nonpositive is True
121
+ and i1.is_nonnegative is True
122
+ )
123
+ if not intersects_box:
124
+ return None
125
+ # Bounding-box intersection does not prove intersection of a rotated
126
+ # parallelogram with the cut. It *is* exact for real/pure-imaginary affine
127
+ # maps, whose image of an axis-aligned rectangle is axis-aligned.
128
+ cd = _affine_data(g, var)
129
+ if cd is not None:
130
+ c, _ = cd
131
+ axis_aligned = c.is_real is True or sp.simplify(sp.re(c)) == 0
132
+ if axis_aligned:
133
+ return False
134
+ return None
135
+
136
+
137
+ def _nonzero_on_rectangle(g, var, rect):
138
+ if not g.has(var):
139
+ z = sp.simplify(g)
140
+ if z.is_zero is True:
141
+ return False
142
+ if z.is_zero is False:
143
+ return True
144
+ return None
145
+ try:
146
+ sol = sp.solveset(sp.Eq(g, 0), var, domain=S.Complexes)
147
+ if isinstance(sol, sp.FiniteSet):
148
+ unknown = False
149
+ for r in sol:
150
+ q = _rect_contains(rect, r)
151
+ if q is True:
152
+ return False
153
+ if q is None:
154
+ unknown = True
155
+ if not unknown:
156
+ return True
157
+ except SYMBOLIC_QUERY_ERRORS:
158
+ pass
159
+ b = _affine_image_bounds(g, var, rect)
160
+ if b is not None:
161
+ r0, r1, i0, i1 = b
162
+ if (
163
+ r0.is_positive is True
164
+ or r1.is_negative is True
165
+ or i0.is_positive is True
166
+ or i1.is_negative is True
167
+ ):
168
+ return True
169
+ return None
170
+
171
+
172
+ def holomorphic_on_rectangle(expr, var, rectangle):
173
+ """Conservatively certify principal-branch holomorphy on a closed rectangle.
174
+
175
+ Returns ``holomorphic=None`` when the current symbolic tests cannot certify
176
+ either outcome. No floating sampling is used for a positive certificate.
177
+ """
178
+ expr = sp.sympify(expr)
179
+ var = sp.sympify(var)
180
+ rect = tuple(map(sp.sympify, rectangle))
181
+ obs = []
182
+ ev = []
183
+
184
+ def rec(e):
185
+ if not e.has(var) or e == var:
186
+ return True
187
+ if e.func in _ENTIRE:
188
+ return _all3(rec(a) for a in e.args)
189
+ if e.func in (sp.Add, sp.Mul):
190
+ return _all3(rec(a) for a in e.args)
191
+ if e.func is sp.Pow:
192
+ b, p = e.args
193
+ sub = _all3((rec(b), rec(p)))
194
+ if sub is False:
195
+ return False
196
+ if p.is_Integer:
197
+ if p.is_nonnegative:
198
+ return sub
199
+ nz = _nonzero_on_rectangle(b, var, rect)
200
+ ev.append(("nonzero", b, nz))
201
+ if nz is False:
202
+ obs.append(sp.Eq(b, 0))
203
+ return False
204
+ return _all3((sub, nz))
205
+ av = _avoids_negative_real_axis(b, var, rect)
206
+ ev.append(("principal_power_cut", b, av))
207
+ if av is False:
208
+ obs.append(("negative_real_axis", b))
209
+ return False
210
+ return _all3((sub, av))
211
+ if e.func is sp.log:
212
+ sub = rec(e.args[0])
213
+ if sub is False:
214
+ return False
215
+ av = _avoids_negative_real_axis(e.args[0], var, rect)
216
+ ev.append(("log_cut", e.args[0], av))
217
+ if av is False:
218
+ obs.append(("negative_real_axis", e.args[0]))
219
+ return False
220
+ return _all3((sub, av))
221
+ if e.func in (sp.tan,):
222
+ sub = rec(e.args[0])
223
+ if sub is False:
224
+ return False
225
+ nz = _nonzero_on_rectangle(sp.cos(e.args[0]), var, rect)
226
+ ev.append(("tan_poles", nz))
227
+ if nz is False:
228
+ obs.append(sp.Eq(sp.cos(e.args[0]), 0))
229
+ return False
230
+ return _all3((sub, nz))
231
+ if e.func in (sp.gamma,):
232
+ sub = rec(e.args[0])
233
+ if sub is False:
234
+ return False
235
+ # finite exact preimages of nonpositive integers need a dedicated
236
+ # pole-set proof; do not turn uncertainty into non-holomorphy.
237
+ return None
238
+ return None
239
+
240
+ ans = rec(expr)
241
+ return AnalyticNeighborhoodCertificate(
242
+ ans, rect, "structural_rectangle", tuple(obs), tuple(ev)
243
+ )
244
+
245
+
246
+ def analytic_neighborhood(expr, var, interval, radius=None):
247
+ """Certify a complex rectangular neighborhood of a finite real interval."""
248
+ interval = sp.sympify(interval)
249
+ if (
250
+ not isinstance(interval, sp.Interval)
251
+ or interval.start in (-sp.oo, sp.oo)
252
+ or interval.end in (-sp.oo, sp.oo)
253
+ ):
254
+ return AnalyticNeighborhoodCertificate(None, interval, "unsupported_interval")
255
+ if radius is None:
256
+ radius = sp.Rational(1, 100)
257
+ r = sp.sympify(radius)
258
+ if r.is_positive is not True:
259
+ return AnalyticNeighborhoodCertificate(None, interval, "invalid_radius")
260
+ rect = (interval.start - sp.I * r, interval.end + sp.I * r)
261
+ return holomorphic_on_rectangle(expr, var, rect)