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,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)
|