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,1767 @@
|
|
|
1
|
+
"""Structural singularity, branch, analyticity and continuity analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
import sympy as sp
|
|
9
|
+
from sympy import S
|
|
10
|
+
from sympy.calculus.util import continuous_domain
|
|
11
|
+
from sympy.core.relational import Relational
|
|
12
|
+
from sympy.logic.boolalg import And, Boolean, Or
|
|
13
|
+
|
|
14
|
+
from .engine import evaluate_property, simplify_condition
|
|
15
|
+
from .exceptions import SYMBOLIC_QUERY_ERRORS
|
|
16
|
+
from .results import (
|
|
17
|
+
Analyticity,
|
|
18
|
+
ConditionalValue,
|
|
19
|
+
Continuity,
|
|
20
|
+
Meromorphicity,
|
|
21
|
+
PropertyResult,
|
|
22
|
+
Singularity,
|
|
23
|
+
SingularityKind,
|
|
24
|
+
)
|
|
25
|
+
from .stratification import evaluate_parameter_strata
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _is_empty(value) -> bool:
|
|
29
|
+
return value in ({}, (), [], S.EmptySet, sp.Tuple())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _replace_helper_heads(expr):
|
|
33
|
+
"""Normalize a few helper heads used only by the catalogue property dump."""
|
|
34
|
+
if not isinstance(expr, sp.Basic):
|
|
35
|
+
return expr
|
|
36
|
+
name = getattr(expr.func, "__name__", "")
|
|
37
|
+
if name == "RemoveQuadratics" and len(expr.args) == 1:
|
|
38
|
+
a = expr.args[0]
|
|
39
|
+
if getattr(a, "func", None) is sp.Abs:
|
|
40
|
+
return _replace_helper_heads(a.args[0])
|
|
41
|
+
if isinstance(a, sp.Pow):
|
|
42
|
+
return _replace_helper_heads(a.args[0])
|
|
43
|
+
return _replace_helper_heads(a)
|
|
44
|
+
if name == "If" and len(expr.args) == 3:
|
|
45
|
+
cond = _replace_helper_heads(expr.args[0])
|
|
46
|
+
if cond in (S.true, True):
|
|
47
|
+
return _replace_helper_heads(expr.args[1])
|
|
48
|
+
if cond in (S.false, False):
|
|
49
|
+
return _replace_helper_heads(expr.args[2])
|
|
50
|
+
args = tuple(_replace_helper_heads(a) for a in expr.args)
|
|
51
|
+
try:
|
|
52
|
+
return expr.func(*args)
|
|
53
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
54
|
+
return expr
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _combine_guard(a, b, assumptions=S.true):
|
|
58
|
+
return simplify_condition(And(a, b), assumptions=assumptions)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _split_locus_guard(condition, var, *, assumptions=S.true):
|
|
62
|
+
"""Split a condition into variable-locus and parameter-only parts."""
|
|
63
|
+
c = sp.sympify(condition)
|
|
64
|
+
if c in (S.true, S.false):
|
|
65
|
+
return c, S.true
|
|
66
|
+
if c.func is And:
|
|
67
|
+
locus = []
|
|
68
|
+
guard = []
|
|
69
|
+
for atom in c.args:
|
|
70
|
+
(locus if atom.has(var) else guard).append(atom)
|
|
71
|
+
lc = And(*locus) if locus else S.true
|
|
72
|
+
gd = And(*guard) if guard else S.true
|
|
73
|
+
return simplify_condition(lc, assumptions=assumptions), simplify_condition(
|
|
74
|
+
gd, assumptions=assumptions
|
|
75
|
+
)
|
|
76
|
+
if c.has(var):
|
|
77
|
+
return c, S.true
|
|
78
|
+
return S.true, simplify_condition(c, assumptions=assumptions)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _registry_loci(
|
|
82
|
+
expr,
|
|
83
|
+
prop: str,
|
|
84
|
+
kind: SingularityKind,
|
|
85
|
+
*,
|
|
86
|
+
mode="complex",
|
|
87
|
+
var=None,
|
|
88
|
+
assumptions=S.true,
|
|
89
|
+
):
|
|
90
|
+
result = evaluate_property(expr, prop, var=var, mode=mode, assumptions=assumptions)
|
|
91
|
+
out = []
|
|
92
|
+
for case in result.cases:
|
|
93
|
+
value = _replace_helper_heads(case.value)
|
|
94
|
+
if _is_empty(value):
|
|
95
|
+
continue
|
|
96
|
+
entries = (
|
|
97
|
+
tuple(value) if isinstance(value, (sp.Tuple, tuple, list)) else (value,)
|
|
98
|
+
)
|
|
99
|
+
for entry in entries:
|
|
100
|
+
entry = _replace_helper_heads(entry)
|
|
101
|
+
if isinstance(entry, (sp.Tuple, tuple, list)) and len(entry) >= 2:
|
|
102
|
+
locus, local_condition = entry[0], entry[1]
|
|
103
|
+
local_locus, local_guard = _split_locus_guard(
|
|
104
|
+
local_condition, var, assumptions=assumptions
|
|
105
|
+
)
|
|
106
|
+
locus = _combine_guard(locus, local_locus, assumptions=assumptions)
|
|
107
|
+
guard = _combine_guard(
|
|
108
|
+
case.condition, local_guard, assumptions=assumptions
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
locus, guard = entry, case.condition
|
|
112
|
+
if guard is S.false or locus is S.false:
|
|
113
|
+
continue
|
|
114
|
+
out.append(
|
|
115
|
+
Singularity(
|
|
116
|
+
kind, sp.sympify(locus), sp.sympify(guard), source="registry"
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
return tuple(out)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _unique(items: Iterable[Singularity]):
|
|
123
|
+
out = []
|
|
124
|
+
seen = set()
|
|
125
|
+
for item in items:
|
|
126
|
+
key = (
|
|
127
|
+
item.kind,
|
|
128
|
+
sp.srepr(item.condition),
|
|
129
|
+
sp.srepr(item.guard),
|
|
130
|
+
str(item.multiplicity),
|
|
131
|
+
)
|
|
132
|
+
if key in seen:
|
|
133
|
+
continue
|
|
134
|
+
seen.add(key)
|
|
135
|
+
out.append(item)
|
|
136
|
+
return tuple(out)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def function_order_at(expr, var, point):
|
|
140
|
+
"""Return the local integer/Puiseux order when SymPy can certify it.
|
|
141
|
+
|
|
142
|
+
Positive means a zero, negative a pole, and zero a finite nonzero value.
|
|
143
|
+
``None`` means the order could not be established symbolically.
|
|
144
|
+
"""
|
|
145
|
+
expr = sp.sympify(expr)
|
|
146
|
+
var = sp.sympify(var)
|
|
147
|
+
point = sp.sympify(point)
|
|
148
|
+
t = sp.Dummy("funcprops_local")
|
|
149
|
+
try:
|
|
150
|
+
shifted = expr.subs(var, point + t)
|
|
151
|
+
lead = shifted.as_leading_term(t)
|
|
152
|
+
pd = lead.as_powers_dict()
|
|
153
|
+
order = sp.simplify(pd.get(t, S.Zero))
|
|
154
|
+
if order.is_number:
|
|
155
|
+
# ``as_leading_term`` can return an unresolved special function
|
|
156
|
+
# such as zeta(1+t); treating the absence of an explicit t power
|
|
157
|
+
# as order zero is not a certificate.
|
|
158
|
+
if order == 0 and lead.has(t):
|
|
159
|
+
pass
|
|
160
|
+
else:
|
|
161
|
+
return order
|
|
162
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
163
|
+
pass
|
|
164
|
+
# A short series fallback catches cancellation such as sin(x)/x**3.
|
|
165
|
+
try:
|
|
166
|
+
ser = sp.series(expr, var, point, 8).removeO().expand()
|
|
167
|
+
shifted = ser.subs(var, point + t).expand()
|
|
168
|
+
terms = sp.Add.make_args(shifted)
|
|
169
|
+
orders = []
|
|
170
|
+
for term in terms:
|
|
171
|
+
power = term.as_powers_dict().get(t, S.Zero)
|
|
172
|
+
if power.is_number:
|
|
173
|
+
orders.append(power)
|
|
174
|
+
if orders:
|
|
175
|
+
return min(orders, key=lambda q: float(q))
|
|
176
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
177
|
+
pass
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _explicit_denominator_poles(expr, var):
|
|
182
|
+
"""Extract exact or conditional poles from a denominator without solving generally."""
|
|
183
|
+
expr = sp.sympify(expr)
|
|
184
|
+
try:
|
|
185
|
+
_num, den = sp.fraction(sp.together(expr))
|
|
186
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
187
|
+
return ()
|
|
188
|
+
if den == 1 or not den.has(var):
|
|
189
|
+
return ()
|
|
190
|
+
out = []
|
|
191
|
+
# Polynomial denominators get exact factor multiplicities and local
|
|
192
|
+
# cancellation checks at explicitly solvable roots.
|
|
193
|
+
try:
|
|
194
|
+
poly = sp.Poly(den, var)
|
|
195
|
+
_, factors = sp.factor_list(poly.as_expr(), var)
|
|
196
|
+
for factor, mult in factors:
|
|
197
|
+
roots = []
|
|
198
|
+
try:
|
|
199
|
+
roots = sp.solve(sp.Eq(factor, 0), var)
|
|
200
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
201
|
+
roots = []
|
|
202
|
+
if roots and all(not r.has(var) for r in roots):
|
|
203
|
+
for r in roots:
|
|
204
|
+
order = function_order_at(expr, var, r)
|
|
205
|
+
if order is not None and order.is_negative:
|
|
206
|
+
out.append(
|
|
207
|
+
Singularity(
|
|
208
|
+
SingularityKind.POLE,
|
|
209
|
+
sp.Eq(var, r),
|
|
210
|
+
S.true,
|
|
211
|
+
int(-order) if (-order).is_Integer else -order,
|
|
212
|
+
source="local-order",
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
elif order is not None and order.is_nonnegative:
|
|
216
|
+
# Removable or regular: cancellation is certified, so do
|
|
217
|
+
# not retain the denominator candidate as a pole.
|
|
218
|
+
continue
|
|
219
|
+
else:
|
|
220
|
+
out.append(
|
|
221
|
+
Singularity(
|
|
222
|
+
SingularityKind.POLE,
|
|
223
|
+
sp.Eq(factor, 0),
|
|
224
|
+
S.true,
|
|
225
|
+
mult,
|
|
226
|
+
source="denominator",
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
out.append(
|
|
231
|
+
Singularity(
|
|
232
|
+
SingularityKind.POLE,
|
|
233
|
+
sp.Eq(factor, 0),
|
|
234
|
+
S.true,
|
|
235
|
+
mult,
|
|
236
|
+
source="denominator",
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
return _unique(out)
|
|
240
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
241
|
+
pass
|
|
242
|
+
# Generic denominator: deliberately leave the condition unsolved.
|
|
243
|
+
return (
|
|
244
|
+
Singularity(
|
|
245
|
+
SingularityKind.POLE, sp.Eq(den, 0), S.true, None, source="denominator"
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _recursive_argument_singularities(expr, var, kind, assumptions=S.true):
|
|
251
|
+
out = []
|
|
252
|
+
for arg in getattr(expr, "args", ()):
|
|
253
|
+
if not getattr(arg, "has", lambda *_: False)(var):
|
|
254
|
+
continue
|
|
255
|
+
if kind is SingularityKind.POLE:
|
|
256
|
+
out.extend(function_poles(arg, var, assumptions=assumptions))
|
|
257
|
+
elif kind is SingularityKind.BRANCH_POINT:
|
|
258
|
+
out.extend(function_branchpoints(arg, var, assumptions=assumptions))
|
|
259
|
+
elif kind is SingularityKind.ESSENTIAL:
|
|
260
|
+
out.extend(
|
|
261
|
+
function_essential_singularities(arg, var, assumptions=assumptions)
|
|
262
|
+
)
|
|
263
|
+
return out
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _validate_pole_candidates(expr, var, candidates):
|
|
267
|
+
out = []
|
|
268
|
+
for p in candidates:
|
|
269
|
+
# Primitive registry/DLMF poles are already exact facts. Local-order
|
|
270
|
+
# probing is for algebraic denominator candidates and must not erase a
|
|
271
|
+
# special-function pole merely because SymPy cannot expand locally.
|
|
272
|
+
if p.source == "registry":
|
|
273
|
+
out.append(p)
|
|
274
|
+
continue
|
|
275
|
+
cond = p.condition
|
|
276
|
+
point = None
|
|
277
|
+
if isinstance(cond, sp.Equality):
|
|
278
|
+
if cond.lhs == var and not cond.rhs.has(var):
|
|
279
|
+
point = cond.rhs
|
|
280
|
+
elif cond.rhs == var and not cond.lhs.has(var):
|
|
281
|
+
point = cond.lhs
|
|
282
|
+
if point is not None:
|
|
283
|
+
order = function_order_at(expr, var, point)
|
|
284
|
+
if order is not None:
|
|
285
|
+
if order.is_negative:
|
|
286
|
+
mult = -order
|
|
287
|
+
out.append(
|
|
288
|
+
Singularity(
|
|
289
|
+
SingularityKind.POLE,
|
|
290
|
+
cond,
|
|
291
|
+
p.guard,
|
|
292
|
+
int(mult) if mult.is_Integer else mult,
|
|
293
|
+
source="local-order",
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
# nonnegative means cancellation/regularity, drop candidate
|
|
297
|
+
continue
|
|
298
|
+
out.append(p)
|
|
299
|
+
return _unique(out)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def function_removable_singularities(expr, var):
|
|
303
|
+
"""Return denominator singularities whose local order is nonnegative.
|
|
304
|
+
|
|
305
|
+
This is intentionally internal for now; ``function_singularities`` exposes
|
|
306
|
+
the records under its ``removable`` key without adding another top-level
|
|
307
|
+
public function.
|
|
308
|
+
"""
|
|
309
|
+
expr = sp.sympify(expr)
|
|
310
|
+
var = sp.sympify(var)
|
|
311
|
+
try:
|
|
312
|
+
_, den = sp.fraction(sp.together(expr))
|
|
313
|
+
if den == 1 or not den.has(var):
|
|
314
|
+
return ()
|
|
315
|
+
poly = sp.Poly(den, var)
|
|
316
|
+
roots = sp.solve(sp.Eq(poly.as_expr(), 0), var)
|
|
317
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
318
|
+
return ()
|
|
319
|
+
out = []
|
|
320
|
+
for point in roots:
|
|
321
|
+
if point.has(var):
|
|
322
|
+
continue
|
|
323
|
+
order = function_order_at(expr, var, point)
|
|
324
|
+
if order is not None and order.is_nonnegative:
|
|
325
|
+
out.append(
|
|
326
|
+
Singularity(
|
|
327
|
+
SingularityKind.REMOVABLE,
|
|
328
|
+
sp.Eq(var, point),
|
|
329
|
+
S.true,
|
|
330
|
+
source="local-order",
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
return _unique(out)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def function_poles(expr, var, *, assumptions=S.true):
|
|
337
|
+
expr = sp.sympify(expr)
|
|
338
|
+
var = sp.sympify(var)
|
|
339
|
+
out = []
|
|
340
|
+
# Exact algebraic/rational structure is preferable to table data because it
|
|
341
|
+
# sees cancellation in the concrete expression.
|
|
342
|
+
out.extend(_explicit_denominator_poles(expr, var))
|
|
343
|
+
if expr.is_Function:
|
|
344
|
+
out.extend(
|
|
345
|
+
_registry_loci(
|
|
346
|
+
expr,
|
|
347
|
+
"Poles",
|
|
348
|
+
SingularityKind.POLE,
|
|
349
|
+
mode="complex",
|
|
350
|
+
var=var,
|
|
351
|
+
assumptions=assumptions,
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
# A pole of an argument is not normally a pole of a transcendental outer
|
|
355
|
+
# function, so recurse only through algebraic Add/Mul/Pow containers.
|
|
356
|
+
if expr.is_Add or expr.is_Mul or isinstance(expr, sp.Pow):
|
|
357
|
+
out.extend(
|
|
358
|
+
_recursive_argument_singularities(
|
|
359
|
+
expr, var, SingularityKind.POLE, assumptions
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
return _validate_pole_candidates(expr, var, _unique(out))
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def function_branchpoints(expr, var, *, assumptions=S.true):
|
|
366
|
+
expr = sp.sympify(expr)
|
|
367
|
+
var = sp.sympify(var)
|
|
368
|
+
out = []
|
|
369
|
+
if expr.is_Function:
|
|
370
|
+
out.extend(
|
|
371
|
+
_registry_loci(
|
|
372
|
+
expr,
|
|
373
|
+
"BranchPoints",
|
|
374
|
+
SingularityKind.BRANCH_POINT,
|
|
375
|
+
mode="complex",
|
|
376
|
+
var=var,
|
|
377
|
+
assumptions=assumptions,
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
if isinstance(expr, sp.Pow):
|
|
381
|
+
base, exponent = expr.args
|
|
382
|
+
if exponent.is_integer is not True and base.has(var):
|
|
383
|
+
out.append(
|
|
384
|
+
Singularity(
|
|
385
|
+
SingularityKind.BRANCH_POINT, sp.Eq(base, 0), S.true, source="power"
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
out.extend(
|
|
389
|
+
_recursive_argument_singularities(
|
|
390
|
+
expr, var, SingularityKind.BRANCH_POINT, assumptions
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
return _unique(out)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def function_definition_cuts(expr, var, *, mode="real", assumptions=S.true):
|
|
397
|
+
expr = sp.sympify(expr)
|
|
398
|
+
var = sp.sympify(var)
|
|
399
|
+
out = []
|
|
400
|
+
if expr.is_Function:
|
|
401
|
+
out.extend(
|
|
402
|
+
_registry_loci(
|
|
403
|
+
expr,
|
|
404
|
+
"DefCuts",
|
|
405
|
+
SingularityKind.DEFINITION_CUT,
|
|
406
|
+
mode=mode,
|
|
407
|
+
var=var,
|
|
408
|
+
assumptions=assumptions,
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
# Elementary nonsmooth heads have incomplete DefCuts in parts of the source
|
|
412
|
+
# database, so make their real definition boundaries explicit.
|
|
413
|
+
if mode == "real":
|
|
414
|
+
if expr.func in (sp.Abs, sp.sign) and expr.args and expr.args[0].has(var):
|
|
415
|
+
out.append(
|
|
416
|
+
Singularity(
|
|
417
|
+
SingularityKind.DEFINITION_CUT,
|
|
418
|
+
sp.Eq(expr.args[0], 0),
|
|
419
|
+
S.true,
|
|
420
|
+
source="structural",
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
if expr.func in (sp.floor, sp.ceiling) and expr.args and expr.args[0].has(var):
|
|
424
|
+
a = expr.args[0]
|
|
425
|
+
out.append(
|
|
426
|
+
Singularity(
|
|
427
|
+
SingularityKind.DEFINITION_CUT,
|
|
428
|
+
sp.Contains(a, S.Integers),
|
|
429
|
+
S.true,
|
|
430
|
+
source="structural",
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
if isinstance(expr, sp.Pow):
|
|
434
|
+
base, exponent = expr.args
|
|
435
|
+
if exponent.is_integer is not True and base.has(var):
|
|
436
|
+
out.append(
|
|
437
|
+
Singularity(
|
|
438
|
+
SingularityKind.DEFINITION_CUT,
|
|
439
|
+
sp.Eq(base, 0),
|
|
440
|
+
S.true,
|
|
441
|
+
source="power",
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
for arg in getattr(expr, "args", ()):
|
|
445
|
+
if getattr(arg, "has", lambda *_: False)(var):
|
|
446
|
+
out.extend(
|
|
447
|
+
function_definition_cuts(arg, var, mode=mode, assumptions=assumptions)
|
|
448
|
+
)
|
|
449
|
+
return _unique(out)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def function_essential_singularities(expr, var, *, assumptions=S.true):
|
|
453
|
+
expr = sp.sympify(expr)
|
|
454
|
+
var = sp.sympify(var)
|
|
455
|
+
out = []
|
|
456
|
+
if expr.is_Function:
|
|
457
|
+
out.extend(
|
|
458
|
+
_registry_loci(
|
|
459
|
+
expr,
|
|
460
|
+
"Essential",
|
|
461
|
+
SingularityKind.ESSENTIAL,
|
|
462
|
+
mode="complex",
|
|
463
|
+
var=var,
|
|
464
|
+
assumptions=assumptions,
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
# A non-polynomial entire transcendental function composed with a pole
|
|
468
|
+
# has an essential singularity at that pole (or, for periodic
|
|
469
|
+
# meromorphic heads, a non-meromorphic accumulation). These common
|
|
470
|
+
# heads cover the elementary cases that matter for composition.
|
|
471
|
+
transcendental_entire = {
|
|
472
|
+
sp.exp,
|
|
473
|
+
sp.sin,
|
|
474
|
+
sp.cos,
|
|
475
|
+
sp.sinh,
|
|
476
|
+
sp.cosh,
|
|
477
|
+
sp.erf,
|
|
478
|
+
sp.erfc,
|
|
479
|
+
}
|
|
480
|
+
if expr.func in transcendental_entire:
|
|
481
|
+
for arg in expr.args:
|
|
482
|
+
if arg.has(var):
|
|
483
|
+
for pole in function_poles(arg, var, assumptions=assumptions):
|
|
484
|
+
out.append(
|
|
485
|
+
Singularity(
|
|
486
|
+
SingularityKind.ESSENTIAL,
|
|
487
|
+
pole.condition,
|
|
488
|
+
pole.guard,
|
|
489
|
+
source="composition",
|
|
490
|
+
)
|
|
491
|
+
)
|
|
492
|
+
if expr.is_Add or expr.is_Mul or isinstance(expr, sp.Pow):
|
|
493
|
+
out.extend(
|
|
494
|
+
_recursive_argument_singularities(
|
|
495
|
+
expr, var, SingularityKind.ESSENTIAL, assumptions
|
|
496
|
+
)
|
|
497
|
+
)
|
|
498
|
+
return _unique(out)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def function_branchcuts(expr, var, *, assumptions=S.true, mode="conditions"):
|
|
502
|
+
"""Return principal branch-cut loci, unsolved by default."""
|
|
503
|
+
from .branchcuts import (
|
|
504
|
+
IMPLEMENTED_RULE_NAMES,
|
|
505
|
+
branch_cut_conditions,
|
|
506
|
+
branch_cut_jumps,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
expr = sp.sympify(expr)
|
|
510
|
+
var = sp.sympify(var)
|
|
511
|
+
if mode == "jumps":
|
|
512
|
+
return branch_cut_jumps(expr, var, assumptions=assumptions)
|
|
513
|
+
out = []
|
|
514
|
+
for cond in branch_cut_conditions(expr, var, assumptions=assumptions):
|
|
515
|
+
locus, guard = _split_locus_guard(cond, var, assumptions=assumptions)
|
|
516
|
+
if locus is not S.false and guard is not S.false:
|
|
517
|
+
out.append(
|
|
518
|
+
Singularity(
|
|
519
|
+
SingularityKind.BRANCH_CUT, locus, guard, source="branch-rules"
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
# Structural jump rules cover the primitive heads for which funcprops can
|
|
523
|
+
# compute boundary values. Catalogue loci supplement other function heads
|
|
524
|
+
# without duplicating equivalent structural cuts.
|
|
525
|
+
head = getattr(expr.func, "__name__", "").lower()
|
|
526
|
+
if isinstance(expr, sp.Pow):
|
|
527
|
+
head = "power"
|
|
528
|
+
if expr.is_Function and head not in IMPLEMENTED_RULE_NAMES:
|
|
529
|
+
out.extend(
|
|
530
|
+
_registry_loci(
|
|
531
|
+
expr,
|
|
532
|
+
"BranchCuts",
|
|
533
|
+
SingularityKind.BRANCH_CUT,
|
|
534
|
+
mode="complex",
|
|
535
|
+
var=var,
|
|
536
|
+
assumptions=assumptions,
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# Formula-level DLMF structural cuts broader than the available exact jump
|
|
541
|
+
# formulas. Record the locus without inventing a jump formula at resonant
|
|
542
|
+
# parameter values.
|
|
543
|
+
if expr.func is sp.hyper and len(expr.args) == 3 and expr.args[2] == var:
|
|
544
|
+
aa = tuple(expr.args[0])
|
|
545
|
+
bb = tuple(expr.args[1])
|
|
546
|
+
if len(aa) == len(bb) + 1:
|
|
547
|
+
terminating = (
|
|
548
|
+
Or(*[And(sp.Contains(a, S.Integers), a <= 0) for a in aa])
|
|
549
|
+
if aa
|
|
550
|
+
else S.false
|
|
551
|
+
)
|
|
552
|
+
bottom_bad = (
|
|
553
|
+
Or(*[And(sp.Contains(b, S.Integers), b <= 0) for b in bb])
|
|
554
|
+
if bb
|
|
555
|
+
else S.false
|
|
556
|
+
)
|
|
557
|
+
guard = simplify_condition(
|
|
558
|
+
And(sp.Not(terminating), sp.Not(bottom_bad)), assumptions=assumptions
|
|
559
|
+
)
|
|
560
|
+
if guard is not S.false:
|
|
561
|
+
out.append(
|
|
562
|
+
Singularity(
|
|
563
|
+
SingularityKind.BRANCH_CUT,
|
|
564
|
+
And(sp.Contains(var, S.Reals), var >= 1),
|
|
565
|
+
guard,
|
|
566
|
+
source="DLMF 16.2(iii)",
|
|
567
|
+
)
|
|
568
|
+
)
|
|
569
|
+
if expr.func is sp.assoc_legendre and len(expr.args) == 3 and expr.args[2] == var:
|
|
570
|
+
nu, mu = expr.args[:2]
|
|
571
|
+
polynomial_exception = And(
|
|
572
|
+
sp.Contains(nu, S.Naturals0),
|
|
573
|
+
sp.Contains(mu, S.Integers),
|
|
574
|
+
sp.Abs(mu) <= nu,
|
|
575
|
+
sp.Eq(sp.Mod(mu, 2), 0),
|
|
576
|
+
)
|
|
577
|
+
zero_exception = And(
|
|
578
|
+
sp.Contains(nu, S.Naturals0), sp.Contains(mu, S.Naturals), mu > nu
|
|
579
|
+
)
|
|
580
|
+
guard = simplify_condition(
|
|
581
|
+
And(sp.Not(polynomial_exception), sp.Not(zero_exception)),
|
|
582
|
+
assumptions=assumptions,
|
|
583
|
+
)
|
|
584
|
+
if guard is not S.false:
|
|
585
|
+
out.append(
|
|
586
|
+
Singularity(
|
|
587
|
+
SingularityKind.BRANCH_CUT,
|
|
588
|
+
And(sp.Contains(var, S.Reals), var <= 1),
|
|
589
|
+
guard,
|
|
590
|
+
source="DLMF 14.21",
|
|
591
|
+
)
|
|
592
|
+
)
|
|
593
|
+
if (
|
|
594
|
+
expr.func in (sp.elliptic_f, sp.elliptic_e)
|
|
595
|
+
and len(expr.args) == 2
|
|
596
|
+
and expr.args[1] == var
|
|
597
|
+
):
|
|
598
|
+
phi = expr.args[0]
|
|
599
|
+
q = 1 - var * sp.sin(phi) ** 2
|
|
600
|
+
out.append(
|
|
601
|
+
Singularity(
|
|
602
|
+
SingularityKind.BRANCH_CUT,
|
|
603
|
+
And(sp.Eq(sp.im(q), 0), sp.re(q) <= 0),
|
|
604
|
+
S.true,
|
|
605
|
+
source="DLMF 19.2.4-19.2.5",
|
|
606
|
+
)
|
|
607
|
+
)
|
|
608
|
+
if expr.func is sp.elliptic_pi and len(expr.args) == 3:
|
|
609
|
+
_n, phi, m = expr.args
|
|
610
|
+
if m == var:
|
|
611
|
+
q = 1 - var * sp.sin(phi) ** 2
|
|
612
|
+
out.append(
|
|
613
|
+
Singularity(
|
|
614
|
+
SingularityKind.BRANCH_CUT,
|
|
615
|
+
And(sp.Eq(sp.im(q), 0), sp.re(q) <= 0),
|
|
616
|
+
S.true,
|
|
617
|
+
source="DLMF 19.2.7",
|
|
618
|
+
)
|
|
619
|
+
)
|
|
620
|
+
out = _unique(out)
|
|
621
|
+
conds = tuple(
|
|
622
|
+
s.condition if s.guard is S.true else And(s.condition, s.guard) for s in out
|
|
623
|
+
)
|
|
624
|
+
if mode == "conditions":
|
|
625
|
+
return conds
|
|
626
|
+
if mode == "records":
|
|
627
|
+
return out
|
|
628
|
+
if mode == "union":
|
|
629
|
+
return Or(*conds) if conds else S.false
|
|
630
|
+
raise ValueError("mode must be 'conditions', 'records', 'union', or 'jumps'")
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _piecewise_boundaries(expr):
|
|
634
|
+
out = []
|
|
635
|
+
if not isinstance(expr, sp.Piecewise):
|
|
636
|
+
return out
|
|
637
|
+
for _, cond in expr.args[:-1]:
|
|
638
|
+
for rel in cond.atoms(Relational) if hasattr(cond, "atoms") else ():
|
|
639
|
+
out.append(sp.Eq(rel.lhs, rel.rhs))
|
|
640
|
+
return out
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _piecewise_branch_for_side(expr, var, point, side):
|
|
644
|
+
t = sp.Dummy("funcprops_side", positive=True)
|
|
645
|
+
probe = point + t if side == "+" else point - t
|
|
646
|
+
for value, cond in expr.args:
|
|
647
|
+
if cond in (S.true, True):
|
|
648
|
+
return value
|
|
649
|
+
try:
|
|
650
|
+
test = sp.simplify(sp.refine(cond.subs(var, probe), sp.Q.positive(t)))
|
|
651
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
652
|
+
test = cond.subs(var, probe)
|
|
653
|
+
if test is S.true or test is True:
|
|
654
|
+
return value
|
|
655
|
+
if test is S.false or test is False:
|
|
656
|
+
continue
|
|
657
|
+
# reduce simple one-variable inequalities in the positive probe.
|
|
658
|
+
try:
|
|
659
|
+
red = sp.reduce_inequalities(test, t)
|
|
660
|
+
if red is S.true:
|
|
661
|
+
return value
|
|
662
|
+
if red is S.false:
|
|
663
|
+
continue
|
|
664
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
665
|
+
pass
|
|
666
|
+
return None
|
|
667
|
+
return None
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def _piecewise_discontinuous_at(expr, var, condition):
|
|
671
|
+
"""Return True/False/None for a simple explicitly solvable boundary."""
|
|
672
|
+
try:
|
|
673
|
+
sols = sp.solve(condition, var)
|
|
674
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
675
|
+
return None
|
|
676
|
+
if not sols or any(s.has(var) for s in sols):
|
|
677
|
+
return None
|
|
678
|
+
any_unknown = False
|
|
679
|
+
for p in sols:
|
|
680
|
+
left_branch = _piecewise_branch_for_side(expr, var, p, "-")
|
|
681
|
+
right_branch = _piecewise_branch_for_side(expr, var, p, "+")
|
|
682
|
+
if left_branch is None or right_branch is None:
|
|
683
|
+
any_unknown = True
|
|
684
|
+
continue
|
|
685
|
+
try:
|
|
686
|
+
left = (
|
|
687
|
+
sp.limit(left_branch, var, p, dir="-")
|
|
688
|
+
if left_branch.has(var)
|
|
689
|
+
else left_branch
|
|
690
|
+
)
|
|
691
|
+
right = (
|
|
692
|
+
sp.limit(right_branch, var, p, dir="+")
|
|
693
|
+
if right_branch.has(var)
|
|
694
|
+
else right_branch
|
|
695
|
+
)
|
|
696
|
+
value = expr.subs(var, p)
|
|
697
|
+
if sp.simplify(left - right) != 0 or sp.simplify(value - left) != 0:
|
|
698
|
+
return True
|
|
699
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
700
|
+
any_unknown = True
|
|
701
|
+
return None if any_unknown else False
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def function_discontinuities(expr, var, *, assumptions=S.true):
|
|
705
|
+
"""Return unsolved real discontinuity conditions."""
|
|
706
|
+
expr = sp.sympify(expr)
|
|
707
|
+
var = sp.sympify(var)
|
|
708
|
+
out = []
|
|
709
|
+
if expr.is_Function:
|
|
710
|
+
r = evaluate_property(expr, "Discontinuities", var=var, assumptions=assumptions)
|
|
711
|
+
for case in r.cases:
|
|
712
|
+
value = case.value
|
|
713
|
+
if _is_empty(value):
|
|
714
|
+
continue
|
|
715
|
+
entries = (
|
|
716
|
+
tuple(value) if isinstance(value, (sp.Tuple, tuple, list)) else (value,)
|
|
717
|
+
)
|
|
718
|
+
for entry in entries:
|
|
719
|
+
cond = (
|
|
720
|
+
entry[0]
|
|
721
|
+
if isinstance(entry, (sp.Tuple, tuple, list)) and entry
|
|
722
|
+
else entry
|
|
723
|
+
)
|
|
724
|
+
out.append(
|
|
725
|
+
Singularity(
|
|
726
|
+
SingularityKind.DISCONTINUITY,
|
|
727
|
+
sp.sympify(cond),
|
|
728
|
+
case.condition,
|
|
729
|
+
source="registry",
|
|
730
|
+
)
|
|
731
|
+
)
|
|
732
|
+
if isinstance(expr, sp.Piecewise):
|
|
733
|
+
for boundary in _piecewise_boundaries(expr):
|
|
734
|
+
verdict = _piecewise_discontinuous_at(expr, var, boundary)
|
|
735
|
+
if verdict is not False:
|
|
736
|
+
out.append(
|
|
737
|
+
Singularity(
|
|
738
|
+
SingularityKind.DISCONTINUITY,
|
|
739
|
+
boundary,
|
|
740
|
+
S.true,
|
|
741
|
+
source="piecewise",
|
|
742
|
+
)
|
|
743
|
+
)
|
|
744
|
+
# Discontinuities of inner arguments propagate through ordinary composition
|
|
745
|
+
# unless an outer rule proves a cancellation; conservatively retain them.
|
|
746
|
+
recursive_args = (
|
|
747
|
+
[v for v, _ in expr.args]
|
|
748
|
+
if isinstance(expr, sp.Piecewise)
|
|
749
|
+
else getattr(expr, "args", ())
|
|
750
|
+
)
|
|
751
|
+
for arg in recursive_args:
|
|
752
|
+
if getattr(arg, "has", lambda *_: False)(var):
|
|
753
|
+
out.extend(function_discontinuities(arg, var, assumptions=assumptions))
|
|
754
|
+
return _unique(out)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def function_singularities(expr, var, *, domain=S.Complexes, assumptions=S.true):
|
|
758
|
+
"""Return all known singularity loci without solving their equations."""
|
|
759
|
+
complex_mode = domain in (S.Complexes, "complex", "complexes", "C", "CC")
|
|
760
|
+
result = {
|
|
761
|
+
"poles": function_poles(expr, var, assumptions=assumptions),
|
|
762
|
+
"removable": function_removable_singularities(expr, var),
|
|
763
|
+
"essential": function_essential_singularities(
|
|
764
|
+
expr, var, assumptions=assumptions
|
|
765
|
+
),
|
|
766
|
+
"branch_points": function_branchpoints(expr, var, assumptions=assumptions),
|
|
767
|
+
"branch_cuts": function_branchcuts(
|
|
768
|
+
expr, var, assumptions=assumptions, mode="records"
|
|
769
|
+
)
|
|
770
|
+
if complex_mode
|
|
771
|
+
else (),
|
|
772
|
+
"definition_cuts": function_definition_cuts(
|
|
773
|
+
expr,
|
|
774
|
+
var,
|
|
775
|
+
mode="complex" if complex_mode else "real",
|
|
776
|
+
assumptions=assumptions,
|
|
777
|
+
),
|
|
778
|
+
}
|
|
779
|
+
if not complex_mode:
|
|
780
|
+
result["discontinuities"] = function_discontinuities(
|
|
781
|
+
expr, var, assumptions=assumptions
|
|
782
|
+
)
|
|
783
|
+
return result
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _real_domain_condition(expr, var):
|
|
787
|
+
from .api import function_domain
|
|
788
|
+
|
|
789
|
+
try:
|
|
790
|
+
d = function_domain(expr, var, domain=S.Reals)
|
|
791
|
+
if isinstance(d, PropertyResult):
|
|
792
|
+
return None
|
|
793
|
+
return sp.sympify(d)
|
|
794
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
795
|
+
return None
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _condition_intersects_domain(cond, domain_cond, assumptions=S.true):
|
|
799
|
+
if domain_cond is None:
|
|
800
|
+
return None
|
|
801
|
+
c = simplify_condition(And(cond, domain_cond), assumptions=assumptions)
|
|
802
|
+
if c is S.false:
|
|
803
|
+
return False
|
|
804
|
+
if c is S.true:
|
|
805
|
+
return True
|
|
806
|
+
# For one-dimensional explicit relational conditions SymPy often resolves
|
|
807
|
+
# conjunction consistency via reduce_inequalities.
|
|
808
|
+
syms = tuple(c.free_symbols)
|
|
809
|
+
if len(syms) == 1:
|
|
810
|
+
try:
|
|
811
|
+
red = sp.reduce_inequalities(c, syms[0])
|
|
812
|
+
return red is not S.false
|
|
813
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
814
|
+
pass
|
|
815
|
+
return None
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _conditional_boolean_status(result):
|
|
819
|
+
"""Return True/False when conditional Boolean cases cover all possibilities."""
|
|
820
|
+
if result.primary is S.true:
|
|
821
|
+
return True
|
|
822
|
+
if result.primary is S.false:
|
|
823
|
+
return False
|
|
824
|
+
if not result.cases:
|
|
825
|
+
return None
|
|
826
|
+
if all(c.value is S.true for c in result.cases):
|
|
827
|
+
try:
|
|
828
|
+
if sp.simplify_logic(Or(*[c.condition for c in result.cases])) is S.true:
|
|
829
|
+
return True
|
|
830
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
831
|
+
pass
|
|
832
|
+
if all(c.value is S.false for c in result.cases):
|
|
833
|
+
try:
|
|
834
|
+
if sp.simplify_logic(Or(*[c.condition for c in result.cases])) is S.true:
|
|
835
|
+
return False
|
|
836
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
837
|
+
pass
|
|
838
|
+
return None
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _variable_tuple(var):
|
|
842
|
+
if isinstance(var, (tuple, list, sp.Tuple)):
|
|
843
|
+
vv = tuple(sp.sympify(v) for v in var)
|
|
844
|
+
else:
|
|
845
|
+
vv = (sp.sympify(var),)
|
|
846
|
+
if not vv:
|
|
847
|
+
raise ValueError("at least one variable is required")
|
|
848
|
+
return vv
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _multivariate_structural_analytic(expr, variables, *, complex_mode=False):
|
|
852
|
+
"""Conservative joint analyticity classifier for several variables."""
|
|
853
|
+
vv = tuple(variables)
|
|
854
|
+
expr = sp.sympify(expr)
|
|
855
|
+
if not any(expr.has(v) for v in vv):
|
|
856
|
+
return Analyticity.ANALYTIC
|
|
857
|
+
if expr.func in (sp.Abs, sp.sign, sp.floor, sp.ceiling) or isinstance(
|
|
858
|
+
expr, sp.Piecewise
|
|
859
|
+
):
|
|
860
|
+
return Analyticity.NOT_ANALYTIC
|
|
861
|
+
try:
|
|
862
|
+
if expr.is_polynomial(*vv) or expr.is_rational_function(*vv):
|
|
863
|
+
return Analyticity.ANALYTIC
|
|
864
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
865
|
+
pass
|
|
866
|
+
if expr.is_Add or expr.is_Mul or (isinstance(expr, sp.Pow) and expr.exp.is_integer):
|
|
867
|
+
vals = [
|
|
868
|
+
_multivariate_structural_analytic(a, vv, complex_mode=complex_mode)
|
|
869
|
+
for a in expr.args
|
|
870
|
+
]
|
|
871
|
+
if any(v is Analyticity.NOT_ANALYTIC for v in vals):
|
|
872
|
+
return Analyticity.NOT_ANALYTIC
|
|
873
|
+
if all(v is Analyticity.ANALYTIC for v in vals):
|
|
874
|
+
return Analyticity.ANALYTIC
|
|
875
|
+
return Analyticity.UNKNOWN
|
|
876
|
+
analytic_heads = (
|
|
877
|
+
sp.exp,
|
|
878
|
+
sp.sin,
|
|
879
|
+
sp.cos,
|
|
880
|
+
sp.sinh,
|
|
881
|
+
sp.cosh,
|
|
882
|
+
sp.tanh,
|
|
883
|
+
sp.erf,
|
|
884
|
+
sp.erfc,
|
|
885
|
+
)
|
|
886
|
+
if expr.func in analytic_heads:
|
|
887
|
+
vals = [
|
|
888
|
+
_multivariate_structural_analytic(a, vv, complex_mode=complex_mode)
|
|
889
|
+
for a in expr.args
|
|
890
|
+
if any(a.has(v) for v in vv)
|
|
891
|
+
]
|
|
892
|
+
if all(v is Analyticity.ANALYTIC for v in vals):
|
|
893
|
+
return Analyticity.ANALYTIC
|
|
894
|
+
if any(v is Analyticity.NOT_ANALYTIC for v in vals):
|
|
895
|
+
return Analyticity.NOT_ANALYTIC
|
|
896
|
+
return Analyticity.UNKNOWN
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _multivariate_structural_continuity(expr, variables):
|
|
900
|
+
"""Conservative joint continuity classifier on the natural real domain."""
|
|
901
|
+
vv = tuple(variables)
|
|
902
|
+
expr = sp.sympify(expr)
|
|
903
|
+
if not any(expr.has(v) for v in vv):
|
|
904
|
+
return Continuity.CONTINUOUS
|
|
905
|
+
if expr.func in (sp.floor, sp.ceiling, sp.sign):
|
|
906
|
+
return Continuity.DISCONTINUOUS
|
|
907
|
+
try:
|
|
908
|
+
if expr.is_polynomial(*vv) or expr.is_rational_function(*vv):
|
|
909
|
+
return Continuity.CONTINUOUS
|
|
910
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
911
|
+
pass
|
|
912
|
+
if expr.func is sp.Abs:
|
|
913
|
+
vals = [_multivariate_structural_continuity(a, vv) for a in expr.args]
|
|
914
|
+
return (
|
|
915
|
+
Continuity.CONTINUOUS
|
|
916
|
+
if all(v is Continuity.CONTINUOUS for v in vals)
|
|
917
|
+
else Continuity.UNKNOWN
|
|
918
|
+
)
|
|
919
|
+
if expr.is_Add or expr.is_Mul or isinstance(expr, sp.Pow):
|
|
920
|
+
vals = [
|
|
921
|
+
_multivariate_structural_continuity(a, vv)
|
|
922
|
+
for a in expr.args
|
|
923
|
+
if any(a.has(v) for v in vv)
|
|
924
|
+
]
|
|
925
|
+
if any(v is Continuity.DISCONTINUOUS for v in vals):
|
|
926
|
+
return Continuity.DISCONTINUOUS
|
|
927
|
+
if all(v is Continuity.CONTINUOUS for v in vals):
|
|
928
|
+
return Continuity.CONTINUOUS
|
|
929
|
+
return Continuity.UNKNOWN
|
|
930
|
+
continuous_heads = (
|
|
931
|
+
sp.exp,
|
|
932
|
+
sp.log,
|
|
933
|
+
sp.sin,
|
|
934
|
+
sp.cos,
|
|
935
|
+
sp.tan,
|
|
936
|
+
sp.cot,
|
|
937
|
+
sp.sinh,
|
|
938
|
+
sp.cosh,
|
|
939
|
+
sp.tanh,
|
|
940
|
+
sp.asin,
|
|
941
|
+
sp.acos,
|
|
942
|
+
sp.atan,
|
|
943
|
+
sp.asinh,
|
|
944
|
+
sp.acosh,
|
|
945
|
+
sp.atanh,
|
|
946
|
+
sp.erf,
|
|
947
|
+
sp.erfc,
|
|
948
|
+
sp.gamma,
|
|
949
|
+
)
|
|
950
|
+
if expr.func in continuous_heads:
|
|
951
|
+
vals = [
|
|
952
|
+
_multivariate_structural_continuity(a, vv)
|
|
953
|
+
for a in expr.args
|
|
954
|
+
if any(a.has(v) for v in vv)
|
|
955
|
+
]
|
|
956
|
+
if all(v is Continuity.CONTINUOUS for v in vals):
|
|
957
|
+
return Continuity.CONTINUOUS
|
|
958
|
+
return Continuity.UNKNOWN
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def function_analytic(
|
|
962
|
+
expr,
|
|
963
|
+
var,
|
|
964
|
+
*,
|
|
965
|
+
domain=S.Reals,
|
|
966
|
+
assumptions=S.true,
|
|
967
|
+
details=False,
|
|
968
|
+
_domain_ready=False,
|
|
969
|
+
):
|
|
970
|
+
"""Classify real analyticity by default, holomorphy for Complexes."""
|
|
971
|
+
expr = sp.sympify(expr)
|
|
972
|
+
vv = _variable_tuple(var)
|
|
973
|
+
is_complex = domain in (S.Complexes, "complex", "complexes", "C", "CC")
|
|
974
|
+
if len(vv) > 1:
|
|
975
|
+
val = _multivariate_structural_analytic(expr, vv, complex_mode=is_complex)
|
|
976
|
+
result = PropertyResult(val, source="multivariate-structural-analyticity")
|
|
977
|
+
return result if details else val
|
|
978
|
+
var = vv[0]
|
|
979
|
+
if (
|
|
980
|
+
not is_complex
|
|
981
|
+
and domain not in (S.Reals, "real", "reals", "R", "RR")
|
|
982
|
+
and not isinstance(domain, (sp.Set, Boolean, Relational))
|
|
983
|
+
):
|
|
984
|
+
raise ValueError(
|
|
985
|
+
"domain must be Reals, Complexes, a set, or a Boolean domain condition"
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
# Algebraic expressions are analytic on their natural domain except at
|
|
989
|
+
# branch/definition boundaries, handled below.
|
|
990
|
+
if is_complex:
|
|
991
|
+
# "Analytic" means holomorphic on the function's natural complex
|
|
992
|
+
# domain, matching the source LocallyAnalytic property.
|
|
993
|
+
if expr.func in (sp.Abs, sp.sign, sp.floor, sp.ceiling) or isinstance(
|
|
994
|
+
expr, sp.Piecewise
|
|
995
|
+
):
|
|
996
|
+
result = PropertyResult(
|
|
997
|
+
Analyticity.NOT_ANALYTIC, source="nonholomorphic-head"
|
|
998
|
+
)
|
|
999
|
+
return result if details else result.primary
|
|
1000
|
+
# Generalized hypergeometric functions: DLMF 16.2 states that p<=q
|
|
1001
|
+
# is entire in z, while p=q+1 has a principal analytic branch off its
|
|
1002
|
+
# standard cut. A terminating top parameter makes the series a
|
|
1003
|
+
# polynomial. We require bottom parameters to be provably outside
|
|
1004
|
+
# the excluded nonpositive integers rather than assuming genericity.
|
|
1005
|
+
if expr.func is sp.hyper and len(expr.args) == 3 and expr.args[2] == var:
|
|
1006
|
+
aa = tuple(expr.args[0])
|
|
1007
|
+
bb = tuple(expr.args[1])
|
|
1008
|
+
|
|
1009
|
+
def _bottom_ok(b):
|
|
1010
|
+
return b.is_integer is False or b.is_positive is True
|
|
1011
|
+
|
|
1012
|
+
bottoms_ok = all(_bottom_ok(b) for b in bb)
|
|
1013
|
+
terminating = any(
|
|
1014
|
+
a.is_integer is True and a.is_nonpositive is True for a in aa
|
|
1015
|
+
)
|
|
1016
|
+
if bottoms_ok and (len(aa) <= len(bb) + 1 or terminating):
|
|
1017
|
+
src = (
|
|
1018
|
+
"DLMF 16.2(ii)"
|
|
1019
|
+
if len(aa) <= len(bb)
|
|
1020
|
+
else (
|
|
1021
|
+
"DLMF 16.2 terminating polynomial"
|
|
1022
|
+
if terminating
|
|
1023
|
+
else "DLMF 16.2(iii) principal branch"
|
|
1024
|
+
)
|
|
1025
|
+
)
|
|
1026
|
+
result = PropertyResult(Analyticity.ANALYTIC, source=src)
|
|
1027
|
+
return result if details else result.primary
|
|
1028
|
+
local = evaluate_property(
|
|
1029
|
+
expr, "LocallyAnalytic", mode="complex", var=var, assumptions=assumptions
|
|
1030
|
+
)
|
|
1031
|
+
local_status = _conditional_boolean_status(local)
|
|
1032
|
+
if local_status is False:
|
|
1033
|
+
return (
|
|
1034
|
+
PropertyResult(Analyticity.NOT_ANALYTIC, source="registry")
|
|
1035
|
+
if details
|
|
1036
|
+
else Analyticity.NOT_ANALYTIC
|
|
1037
|
+
)
|
|
1038
|
+
if local_status is True:
|
|
1039
|
+
return (
|
|
1040
|
+
PropertyResult(Analyticity.ANALYTIC, source="registry")
|
|
1041
|
+
if details
|
|
1042
|
+
else Analyticity.ANALYTIC
|
|
1043
|
+
)
|
|
1044
|
+
entire = evaluate_property(
|
|
1045
|
+
expr, "Entire", mode="complex", var=var, assumptions=assumptions
|
|
1046
|
+
)
|
|
1047
|
+
if _conditional_boolean_status(entire) is True or expr.is_polynomial(var):
|
|
1048
|
+
return (
|
|
1049
|
+
PropertyResult(Analyticity.ANALYTIC, source="analytic-primitive")
|
|
1050
|
+
if details
|
|
1051
|
+
else Analyticity.ANALYTIC
|
|
1052
|
+
)
|
|
1053
|
+
# Arithmetic closure is valid without needing a primitive function
|
|
1054
|
+
# theorem. Do not apply generic composition closure to an unknown head.
|
|
1055
|
+
if (
|
|
1056
|
+
expr.is_Add
|
|
1057
|
+
or expr.is_Mul
|
|
1058
|
+
or (isinstance(expr, sp.Pow) and expr.exp.is_integer)
|
|
1059
|
+
):
|
|
1060
|
+
vals = [
|
|
1061
|
+
function_analytic(a, var, domain=S.Complexes, assumptions=assumptions)
|
|
1062
|
+
for a in expr.args
|
|
1063
|
+
if a.has(var)
|
|
1064
|
+
]
|
|
1065
|
+
if vals and all(v == Analyticity.ANALYTIC for v in vals):
|
|
1066
|
+
return (
|
|
1067
|
+
PropertyResult(Analyticity.ANALYTIC, source="holomorphic-closure")
|
|
1068
|
+
if details
|
|
1069
|
+
else Analyticity.ANALYTIC
|
|
1070
|
+
)
|
|
1071
|
+
if any(v == Analyticity.NOT_ANALYTIC for v in vals):
|
|
1072
|
+
return (
|
|
1073
|
+
PropertyResult(
|
|
1074
|
+
Analyticity.NOT_ANALYTIC, source="holomorphic-closure"
|
|
1075
|
+
)
|
|
1076
|
+
if details
|
|
1077
|
+
else Analyticity.NOT_ANALYTIC
|
|
1078
|
+
)
|
|
1079
|
+
return (
|
|
1080
|
+
PropertyResult(Analyticity.UNKNOWN, source="insufficient-evidence")
|
|
1081
|
+
if details
|
|
1082
|
+
else Analyticity.UNKNOWN
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
# Real analyticity on the requested/natural real domain.
|
|
1086
|
+
domain_cond = (
|
|
1087
|
+
sp.sympify(domain)
|
|
1088
|
+
if _domain_ready
|
|
1089
|
+
else (
|
|
1090
|
+
_real_domain_condition(expr, var)
|
|
1091
|
+
if domain in (S.Reals, "real", "reals", "R", "RR")
|
|
1092
|
+
else (
|
|
1093
|
+
domain.as_relational(var)
|
|
1094
|
+
if isinstance(domain, sp.Set)
|
|
1095
|
+
else sp.sympify(domain)
|
|
1096
|
+
)
|
|
1097
|
+
)
|
|
1098
|
+
)
|
|
1099
|
+
if not _domain_ready and domain_cond is not None:
|
|
1100
|
+
split = evaluate_parameter_strata(
|
|
1101
|
+
domain_cond,
|
|
1102
|
+
var,
|
|
1103
|
+
lambda branch_domain: function_analytic(
|
|
1104
|
+
expr,
|
|
1105
|
+
var,
|
|
1106
|
+
domain=branch_domain,
|
|
1107
|
+
assumptions=assumptions,
|
|
1108
|
+
details=details,
|
|
1109
|
+
_domain_ready=True,
|
|
1110
|
+
),
|
|
1111
|
+
source="analyticity-parameter-stratification",
|
|
1112
|
+
)
|
|
1113
|
+
if split is not None:
|
|
1114
|
+
return split
|
|
1115
|
+
if expr.func in (sp.Abs, sp.sign, sp.floor, sp.ceiling):
|
|
1116
|
+
cuts = function_definition_cuts(expr, var, mode="real", assumptions=assumptions)
|
|
1117
|
+
bad = []
|
|
1118
|
+
unknown_cut = False
|
|
1119
|
+
for cut in cuts:
|
|
1120
|
+
hit = _condition_intersects_domain(
|
|
1121
|
+
And(cut.condition, cut.guard), domain_cond, assumptions
|
|
1122
|
+
)
|
|
1123
|
+
if hit is True:
|
|
1124
|
+
bad.append(cut)
|
|
1125
|
+
elif hit is None:
|
|
1126
|
+
unknown_cut = True
|
|
1127
|
+
if bad:
|
|
1128
|
+
result = PropertyResult(
|
|
1129
|
+
Analyticity.NOT_ANALYTIC, exceptions=tuple(bad), source="nonsmooth-head"
|
|
1130
|
+
)
|
|
1131
|
+
return result if details else result.primary
|
|
1132
|
+
if unknown_cut:
|
|
1133
|
+
result = PropertyResult(
|
|
1134
|
+
Analyticity.UNKNOWN,
|
|
1135
|
+
exceptions=cuts,
|
|
1136
|
+
source="conditional-nonsmooth-cuts",
|
|
1137
|
+
)
|
|
1138
|
+
return result if details else result.primary
|
|
1139
|
+
# Abs/sign/floor/ceiling are locally analytic on every real component
|
|
1140
|
+
# that excludes their kink/jump lattice. This matters for restricted
|
|
1141
|
+
# domains such as Abs(x) on x>0.
|
|
1142
|
+
result = PropertyResult(Analyticity.ANALYTIC, source="nonsmooth-head-off-cuts")
|
|
1143
|
+
return result if details else result.primary
|
|
1144
|
+
if isinstance(expr, sp.Piecewise):
|
|
1145
|
+
boundaries = _piecewise_boundaries(expr)
|
|
1146
|
+
bad = []
|
|
1147
|
+
unknown_boundary = False
|
|
1148
|
+
for b in boundaries:
|
|
1149
|
+
hit = _condition_intersects_domain(b, domain_cond, assumptions)
|
|
1150
|
+
if hit is True:
|
|
1151
|
+
bad.append(b)
|
|
1152
|
+
elif hit is None:
|
|
1153
|
+
unknown_boundary = True
|
|
1154
|
+
if bad:
|
|
1155
|
+
exc = tuple(
|
|
1156
|
+
Singularity(
|
|
1157
|
+
SingularityKind.DEFINITION_CUT, b, S.true, source="piecewise"
|
|
1158
|
+
)
|
|
1159
|
+
for b in bad
|
|
1160
|
+
)
|
|
1161
|
+
result = PropertyResult(
|
|
1162
|
+
Analyticity.NOT_ANALYTIC, exceptions=exc, source="piecewise"
|
|
1163
|
+
)
|
|
1164
|
+
return result if details else result.primary
|
|
1165
|
+
if unknown_boundary:
|
|
1166
|
+
return (
|
|
1167
|
+
PropertyResult(Analyticity.UNKNOWN, source="piecewise-boundary-unknown")
|
|
1168
|
+
if details
|
|
1169
|
+
else Analyticity.UNKNOWN
|
|
1170
|
+
)
|
|
1171
|
+
cuts = function_definition_cuts(expr, var, mode="real", assumptions=assumptions)
|
|
1172
|
+
bad = []
|
|
1173
|
+
unknown = False
|
|
1174
|
+
for cut in cuts:
|
|
1175
|
+
cond = And(cut.condition, cut.guard)
|
|
1176
|
+
intersects = _condition_intersects_domain(cond, domain_cond, assumptions)
|
|
1177
|
+
if intersects is True:
|
|
1178
|
+
bad.append(cut)
|
|
1179
|
+
elif intersects is None:
|
|
1180
|
+
unknown = True
|
|
1181
|
+
if bad:
|
|
1182
|
+
result = PropertyResult(
|
|
1183
|
+
Analyticity.NOT_ANALYTIC, exceptions=tuple(bad), source="definition-cuts"
|
|
1184
|
+
)
|
|
1185
|
+
return result if details else result.primary
|
|
1186
|
+
if unknown:
|
|
1187
|
+
return (
|
|
1188
|
+
PropertyResult(
|
|
1189
|
+
Analyticity.UNKNOWN,
|
|
1190
|
+
exceptions=cuts,
|
|
1191
|
+
source="conditional-definition-cuts",
|
|
1192
|
+
)
|
|
1193
|
+
if details
|
|
1194
|
+
else Analyticity.UNKNOWN
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
# Composition theorem: analytic primitives composed with analytic arguments.
|
|
1198
|
+
for arg in getattr(expr, "args", ()):
|
|
1199
|
+
if arg.has(var) and arg != var:
|
|
1200
|
+
a = function_analytic(arg, var, domain=S.Reals, assumptions=assumptions)
|
|
1201
|
+
if a == Analyticity.NOT_ANALYTIC:
|
|
1202
|
+
return (
|
|
1203
|
+
PropertyResult(Analyticity.NOT_ANALYTIC, source="composition")
|
|
1204
|
+
if details
|
|
1205
|
+
else Analyticity.NOT_ANALYTIC
|
|
1206
|
+
)
|
|
1207
|
+
if a == Analyticity.UNKNOWN:
|
|
1208
|
+
unknown = True
|
|
1209
|
+
entire = evaluate_property(
|
|
1210
|
+
expr, "Entire", mode="complex", var=var, assumptions=assumptions
|
|
1211
|
+
)
|
|
1212
|
+
local = evaluate_property(
|
|
1213
|
+
expr, "LocallyAnalytic", mode="real", var=var, assumptions=assumptions
|
|
1214
|
+
)
|
|
1215
|
+
if expr.is_polynomial(var) or entire.primary is S.true or local.primary is S.true:
|
|
1216
|
+
val = Analyticity.UNKNOWN if unknown else Analyticity.ANALYTIC
|
|
1217
|
+
return PropertyResult(val, source="registry+composition") if details else val
|
|
1218
|
+
if expr.is_rational_function(var) and not cuts:
|
|
1219
|
+
return (
|
|
1220
|
+
PropertyResult(Analyticity.ANALYTIC, source="rational")
|
|
1221
|
+
if details
|
|
1222
|
+
else Analyticity.ANALYTIC
|
|
1223
|
+
)
|
|
1224
|
+
if expr.is_Add or expr.is_Mul or (isinstance(expr, sp.Pow) and expr.exp.is_integer):
|
|
1225
|
+
vals = [
|
|
1226
|
+
function_analytic(a, var, domain=S.Reals, assumptions=assumptions)
|
|
1227
|
+
for a in expr.args
|
|
1228
|
+
if a.has(var)
|
|
1229
|
+
]
|
|
1230
|
+
if vals and all(v == Analyticity.ANALYTIC for v in vals):
|
|
1231
|
+
return (
|
|
1232
|
+
PropertyResult(Analyticity.ANALYTIC, source="analytic-closure")
|
|
1233
|
+
if details
|
|
1234
|
+
else Analyticity.ANALYTIC
|
|
1235
|
+
)
|
|
1236
|
+
if any(v == Analyticity.NOT_ANALYTIC for v in vals):
|
|
1237
|
+
return (
|
|
1238
|
+
PropertyResult(Analyticity.NOT_ANALYTIC, source="analytic-closure")
|
|
1239
|
+
if details
|
|
1240
|
+
else Analyticity.NOT_ANALYTIC
|
|
1241
|
+
)
|
|
1242
|
+
return (
|
|
1243
|
+
PropertyResult(Analyticity.UNKNOWN, source="insufficient-evidence")
|
|
1244
|
+
if details
|
|
1245
|
+
else Analyticity.UNKNOWN
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
def function_meromorphic(expr, var, *, assumptions=S.true, details=False):
|
|
1250
|
+
"""Classify meromorphicity on the complex plane/natural complex domain."""
|
|
1251
|
+
expr = sp.sympify(expr)
|
|
1252
|
+
var = sp.sympify(var)
|
|
1253
|
+
if expr.is_rational_function(var):
|
|
1254
|
+
return (
|
|
1255
|
+
PropertyResult(Meromorphicity.MEROMORPHIC, source="rational")
|
|
1256
|
+
if details
|
|
1257
|
+
else Meromorphicity.MEROMORPHIC
|
|
1258
|
+
)
|
|
1259
|
+
branch = function_branchpoints(expr, var, assumptions=assumptions)
|
|
1260
|
+
cuts = function_branchcuts(expr, var, assumptions=assumptions, mode="records")
|
|
1261
|
+
essential = function_essential_singularities(expr, var, assumptions=assumptions)
|
|
1262
|
+
bad = branch + cuts + essential
|
|
1263
|
+
definite = []
|
|
1264
|
+
conditional = []
|
|
1265
|
+
for item in bad:
|
|
1266
|
+
g = simplify_condition(item.guard, assumptions=assumptions)
|
|
1267
|
+
if g is S.true:
|
|
1268
|
+
definite.append(item)
|
|
1269
|
+
elif g is not S.false:
|
|
1270
|
+
conditional.append(item)
|
|
1271
|
+
if definite:
|
|
1272
|
+
result = PropertyResult(
|
|
1273
|
+
Meromorphicity.NOT_MEROMORPHIC,
|
|
1274
|
+
exceptions=tuple(definite + conditional),
|
|
1275
|
+
source="singularity-analysis",
|
|
1276
|
+
)
|
|
1277
|
+
return result if details else result.primary
|
|
1278
|
+
if conditional:
|
|
1279
|
+
bad_guard = simplify_condition(
|
|
1280
|
+
Or(*[item.guard for item in conditional]), assumptions=assumptions
|
|
1281
|
+
)
|
|
1282
|
+
cases = (
|
|
1283
|
+
ConditionalValue(Meromorphicity.NOT_MEROMORPHIC, bad_guard),
|
|
1284
|
+
ConditionalValue(Meromorphicity.MEROMORPHIC, sp.Not(bad_guard)),
|
|
1285
|
+
)
|
|
1286
|
+
result = PropertyResult(
|
|
1287
|
+
Meromorphicity.UNKNOWN,
|
|
1288
|
+
cases=cases,
|
|
1289
|
+
exceptions=tuple(conditional),
|
|
1290
|
+
source="conditional-singularity-analysis",
|
|
1291
|
+
)
|
|
1292
|
+
return result
|
|
1293
|
+
reg = evaluate_property(
|
|
1294
|
+
expr, "Meromorphic", mode="complex", assumptions=assumptions
|
|
1295
|
+
)
|
|
1296
|
+
if reg.primary is S.true:
|
|
1297
|
+
return (
|
|
1298
|
+
PropertyResult(Meromorphicity.MEROMORPHIC, source="registry")
|
|
1299
|
+
if details
|
|
1300
|
+
else Meromorphicity.MEROMORPHIC
|
|
1301
|
+
)
|
|
1302
|
+
if reg.primary is S.false:
|
|
1303
|
+
return (
|
|
1304
|
+
PropertyResult(Meromorphicity.NOT_MEROMORPHIC, source="registry")
|
|
1305
|
+
if details
|
|
1306
|
+
else Meromorphicity.NOT_MEROMORPHIC
|
|
1307
|
+
)
|
|
1308
|
+
entire = evaluate_property(expr, "Entire", mode="complex", assumptions=assumptions)
|
|
1309
|
+
if entire.primary is S.true:
|
|
1310
|
+
return (
|
|
1311
|
+
PropertyResult(Meromorphicity.MEROMORPHIC, source="entire")
|
|
1312
|
+
if details
|
|
1313
|
+
else Meromorphicity.MEROMORPHIC
|
|
1314
|
+
)
|
|
1315
|
+
if (
|
|
1316
|
+
function_analytic(expr, var, domain=S.Complexes, assumptions=assumptions)
|
|
1317
|
+
== Analyticity.ANALYTIC
|
|
1318
|
+
):
|
|
1319
|
+
# Holomorphic on its natural domain and with no branch/essential
|
|
1320
|
+
# obstruction above: any remaining isolated singularities are poles.
|
|
1321
|
+
return (
|
|
1322
|
+
PropertyResult(Meromorphicity.MEROMORPHIC, source="analytic-with-poles")
|
|
1323
|
+
if details
|
|
1324
|
+
else Meromorphicity.MEROMORPHIC
|
|
1325
|
+
)
|
|
1326
|
+
# Closure of meromorphic functions under arithmetic.
|
|
1327
|
+
if expr.is_Add or expr.is_Mul or (isinstance(expr, sp.Pow) and expr.exp.is_integer):
|
|
1328
|
+
vals = []
|
|
1329
|
+
for arg in expr.args:
|
|
1330
|
+
if arg.has(var):
|
|
1331
|
+
vals.append(function_meromorphic(arg, var, assumptions=assumptions))
|
|
1332
|
+
if vals and all(v == Meromorphicity.MEROMORPHIC for v in vals):
|
|
1333
|
+
return (
|
|
1334
|
+
PropertyResult(Meromorphicity.MEROMORPHIC, source="closure")
|
|
1335
|
+
if details
|
|
1336
|
+
else Meromorphicity.MEROMORPHIC
|
|
1337
|
+
)
|
|
1338
|
+
if any(v == Meromorphicity.NOT_MEROMORPHIC for v in vals):
|
|
1339
|
+
return (
|
|
1340
|
+
PropertyResult(Meromorphicity.NOT_MEROMORPHIC, source="closure")
|
|
1341
|
+
if details
|
|
1342
|
+
else Meromorphicity.NOT_MEROMORPHIC
|
|
1343
|
+
)
|
|
1344
|
+
return (
|
|
1345
|
+
PropertyResult(Meromorphicity.UNKNOWN, source="insufficient-evidence")
|
|
1346
|
+
if details
|
|
1347
|
+
else Meromorphicity.UNKNOWN
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def _condition_to_set(condition, var):
|
|
1352
|
+
"""Convert a real condition using the shared structural set converter."""
|
|
1353
|
+
if condition is None:
|
|
1354
|
+
return None
|
|
1355
|
+
|
|
1356
|
+
# Import lazily to avoid an import cycle through real_analysis.smoothness.
|
|
1357
|
+
from .real_analysis.common import condition_to_set
|
|
1358
|
+
|
|
1359
|
+
converted = condition_to_set(condition, var)
|
|
1360
|
+
if converted is not None:
|
|
1361
|
+
return converted
|
|
1362
|
+
try:
|
|
1363
|
+
return sp.solveset(condition, var, domain=S.Reals)
|
|
1364
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
1365
|
+
return None
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
def function_continuous(
|
|
1369
|
+
expr,
|
|
1370
|
+
var,
|
|
1371
|
+
*,
|
|
1372
|
+
domain=S.Reals,
|
|
1373
|
+
assumptions=S.true,
|
|
1374
|
+
details=False,
|
|
1375
|
+
_domain_ready=False,
|
|
1376
|
+
):
|
|
1377
|
+
"""Test continuity on the function's natural real domain by default."""
|
|
1378
|
+
expr = sp.sympify(expr)
|
|
1379
|
+
vv = _variable_tuple(var)
|
|
1380
|
+
if len(vv) > 1:
|
|
1381
|
+
val = _multivariate_structural_continuity(expr, vv)
|
|
1382
|
+
result = PropertyResult(val, source="multivariate-structural-continuity")
|
|
1383
|
+
return result if details else val
|
|
1384
|
+
var = vv[0]
|
|
1385
|
+
domain_cond = (
|
|
1386
|
+
sp.sympify(domain)
|
|
1387
|
+
if _domain_ready
|
|
1388
|
+
else (
|
|
1389
|
+
_real_domain_condition(expr, var)
|
|
1390
|
+
if domain in (S.Reals, "real", "reals", "R", "RR")
|
|
1391
|
+
else (
|
|
1392
|
+
domain.as_relational(var)
|
|
1393
|
+
if isinstance(domain, sp.Set)
|
|
1394
|
+
else sp.sympify(domain)
|
|
1395
|
+
)
|
|
1396
|
+
)
|
|
1397
|
+
)
|
|
1398
|
+
if not _domain_ready and domain_cond is not None:
|
|
1399
|
+
split = evaluate_parameter_strata(
|
|
1400
|
+
domain_cond,
|
|
1401
|
+
var,
|
|
1402
|
+
lambda branch_domain: function_continuous(
|
|
1403
|
+
expr,
|
|
1404
|
+
var,
|
|
1405
|
+
domain=branch_domain,
|
|
1406
|
+
assumptions=assumptions,
|
|
1407
|
+
details=details,
|
|
1408
|
+
_domain_ready=True,
|
|
1409
|
+
),
|
|
1410
|
+
source="continuity-parameter-stratification",
|
|
1411
|
+
)
|
|
1412
|
+
if split is not None:
|
|
1413
|
+
return split
|
|
1414
|
+
discontinuities = function_discontinuities(expr, var, assumptions=assumptions)
|
|
1415
|
+
actual = []
|
|
1416
|
+
uncertain = []
|
|
1417
|
+
for d in discontinuities:
|
|
1418
|
+
hit = _condition_intersects_domain(
|
|
1419
|
+
And(d.condition, d.guard), domain_cond, assumptions
|
|
1420
|
+
)
|
|
1421
|
+
if hit is True:
|
|
1422
|
+
actual.append(d)
|
|
1423
|
+
elif hit is None:
|
|
1424
|
+
uncertain.append(d)
|
|
1425
|
+
if actual:
|
|
1426
|
+
result = PropertyResult(
|
|
1427
|
+
Continuity.DISCONTINUOUS,
|
|
1428
|
+
exceptions=tuple(actual),
|
|
1429
|
+
source="discontinuity-analysis",
|
|
1430
|
+
)
|
|
1431
|
+
return result if details else result.primary
|
|
1432
|
+
if isinstance(expr, sp.Piecewise) and not uncertain:
|
|
1433
|
+
from .real_analysis.piecewise import piecewise_branches
|
|
1434
|
+
|
|
1435
|
+
branches = piecewise_branches(expr, var, domain_cond)
|
|
1436
|
+
branch_values = [
|
|
1437
|
+
function_continuous(
|
|
1438
|
+
branch.expression,
|
|
1439
|
+
var,
|
|
1440
|
+
domain=branch.condition,
|
|
1441
|
+
assumptions=assumptions,
|
|
1442
|
+
_domain_ready=True,
|
|
1443
|
+
)
|
|
1444
|
+
for branch in branches
|
|
1445
|
+
if branch.expression.has(var)
|
|
1446
|
+
]
|
|
1447
|
+
if all(v == Continuity.CONTINUOUS for v in branch_values):
|
|
1448
|
+
return (
|
|
1449
|
+
PropertyResult(Continuity.CONTINUOUS, source="piecewise-analysis")
|
|
1450
|
+
if details
|
|
1451
|
+
else Continuity.CONTINUOUS
|
|
1452
|
+
)
|
|
1453
|
+
natural_set = _condition_to_set(domain_cond, var)
|
|
1454
|
+
if natural_set is not None:
|
|
1455
|
+
try:
|
|
1456
|
+
cset = continuous_domain(expr, var, S.Reals)
|
|
1457
|
+
if cset == natural_set:
|
|
1458
|
+
return (
|
|
1459
|
+
PropertyResult(
|
|
1460
|
+
Continuity.CONTINUOUS, source="sympy.continuous_domain"
|
|
1461
|
+
)
|
|
1462
|
+
if details
|
|
1463
|
+
else Continuity.CONTINUOUS
|
|
1464
|
+
)
|
|
1465
|
+
missing = natural_set - cset
|
|
1466
|
+
if missing is not S.EmptySet and missing != S.EmptySet:
|
|
1467
|
+
result = PropertyResult(
|
|
1468
|
+
Continuity.DISCONTINUOUS,
|
|
1469
|
+
exceptions=(missing,),
|
|
1470
|
+
source="sympy.continuous_domain",
|
|
1471
|
+
)
|
|
1472
|
+
return result if details else result.primary
|
|
1473
|
+
except NotImplementedError:
|
|
1474
|
+
pass
|
|
1475
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
1476
|
+
pass
|
|
1477
|
+
|
|
1478
|
+
# Analytic functions are continuous on their real analytic domain.
|
|
1479
|
+
analytic = function_analytic(expr, var, domain=domain, assumptions=assumptions)
|
|
1480
|
+
if analytic == Analyticity.ANALYTIC:
|
|
1481
|
+
return (
|
|
1482
|
+
PropertyResult(Continuity.CONTINUOUS, source="analyticity")
|
|
1483
|
+
if details
|
|
1484
|
+
else Continuity.CONTINUOUS
|
|
1485
|
+
)
|
|
1486
|
+
if expr.func is sp.Abs:
|
|
1487
|
+
return (
|
|
1488
|
+
PropertyResult(Continuity.CONTINUOUS, source="Abs")
|
|
1489
|
+
if details
|
|
1490
|
+
else Continuity.CONTINUOUS
|
|
1491
|
+
)
|
|
1492
|
+
return (
|
|
1493
|
+
PropertyResult(Continuity.UNKNOWN, source="insufficient-evidence")
|
|
1494
|
+
if details
|
|
1495
|
+
else Continuity.UNKNOWN
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
# ---------------------------------------------------------------------------
|
|
1500
|
+
# Structural singularity traversal
|
|
1501
|
+
# ---------------------------------------------------------------------------
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
@dataclass(frozen=True)
|
|
1505
|
+
class SingularityAnalysis:
|
|
1506
|
+
branch_cuts: tuple[Singularity, ...] = ()
|
|
1507
|
+
definition_cuts: tuple[Singularity, ...] = ()
|
|
1508
|
+
poles: tuple[Singularity, ...] = ()
|
|
1509
|
+
essential: tuple[Singularity, ...] = ()
|
|
1510
|
+
branch_points: tuple[Singularity, ...] = ()
|
|
1511
|
+
removable: tuple[Singularity, ...] = ()
|
|
1512
|
+
discontinuities: tuple[Singularity, ...] = ()
|
|
1513
|
+
domain_exterior: sp.Basic = S.false
|
|
1514
|
+
complete: bool = False
|
|
1515
|
+
unknown_heads: tuple[str, ...] = ()
|
|
1516
|
+
source: str = "structural-singularity-traversal"
|
|
1517
|
+
|
|
1518
|
+
def all_records(self):
|
|
1519
|
+
return _unique(
|
|
1520
|
+
self.branch_cuts
|
|
1521
|
+
+ self.definition_cuts
|
|
1522
|
+
+ self.poles
|
|
1523
|
+
+ self.essential
|
|
1524
|
+
+ self.branch_points
|
|
1525
|
+
+ self.removable
|
|
1526
|
+
+ self.discontinuities
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1529
|
+
def condition(self):
|
|
1530
|
+
conds = []
|
|
1531
|
+
if self.domain_exterior is not S.false:
|
|
1532
|
+
conds.append(self.domain_exterior)
|
|
1533
|
+
for s in self.all_records():
|
|
1534
|
+
conds.append(And(s.condition, s.guard))
|
|
1535
|
+
return Or(*conds) if conds else S.false
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
def _conditioned(records, cond):
|
|
1539
|
+
if cond is S.true:
|
|
1540
|
+
return tuple(records)
|
|
1541
|
+
out = []
|
|
1542
|
+
for r in records:
|
|
1543
|
+
g = simplify_condition(And(r.guard, cond))
|
|
1544
|
+
if g is not S.false:
|
|
1545
|
+
out.append(Singularity(r.kind, r.condition, g, r.multiplicity, r.source))
|
|
1546
|
+
return tuple(out)
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
def _domain_exterior_condition(expr, var, real=True):
|
|
1550
|
+
"""Boundary/exterior contribution of the supported natural domain."""
|
|
1551
|
+
try:
|
|
1552
|
+
from .api import function_domain
|
|
1553
|
+
|
|
1554
|
+
d = function_domain(expr, var, domain=S.Reals if real else S.Complexes)
|
|
1555
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
1556
|
+
return None
|
|
1557
|
+
if isinstance(d, PropertyResult):
|
|
1558
|
+
return None
|
|
1559
|
+
d = sp.sympify(d)
|
|
1560
|
+
if d is S.true:
|
|
1561
|
+
return S.false
|
|
1562
|
+
if d is S.false:
|
|
1563
|
+
return S.true
|
|
1564
|
+
try:
|
|
1565
|
+
return sp.simplify_logic(sp.Not(d))
|
|
1566
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
1567
|
+
return sp.Not(d)
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
def _primitive_known(expr, var):
|
|
1571
|
+
if expr.is_Atom:
|
|
1572
|
+
return True
|
|
1573
|
+
if expr.is_Add or expr.is_Mul or isinstance(expr, (sp.Pow, sp.Piecewise)):
|
|
1574
|
+
return True
|
|
1575
|
+
if expr.func in (
|
|
1576
|
+
sp.exp,
|
|
1577
|
+
sp.log,
|
|
1578
|
+
sp.sin,
|
|
1579
|
+
sp.cos,
|
|
1580
|
+
sp.tan,
|
|
1581
|
+
sp.cot,
|
|
1582
|
+
sp.sinh,
|
|
1583
|
+
sp.cosh,
|
|
1584
|
+
sp.tanh,
|
|
1585
|
+
sp.asin,
|
|
1586
|
+
sp.acos,
|
|
1587
|
+
sp.atan,
|
|
1588
|
+
sp.asinh,
|
|
1589
|
+
sp.acosh,
|
|
1590
|
+
sp.atanh,
|
|
1591
|
+
sp.Abs,
|
|
1592
|
+
sp.sign,
|
|
1593
|
+
sp.erf,
|
|
1594
|
+
sp.erfc,
|
|
1595
|
+
sp.LambertW,
|
|
1596
|
+
sp.besselj,
|
|
1597
|
+
sp.bessely,
|
|
1598
|
+
sp.gamma,
|
|
1599
|
+
):
|
|
1600
|
+
return True
|
|
1601
|
+
try:
|
|
1602
|
+
fam = evaluate_property(expr, "LocallyAnalytic", var=var, mode="complex")
|
|
1603
|
+
if fam.primary is not None or fam.cases:
|
|
1604
|
+
return True
|
|
1605
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
1606
|
+
pass
|
|
1607
|
+
return False
|
|
1608
|
+
|
|
1609
|
+
|
|
1610
|
+
def _function_singularity_analysis_impl(
|
|
1611
|
+
expr, var, *, domain=S.Complexes, assumptions=S.true, ignore_unknown=False
|
|
1612
|
+
):
|
|
1613
|
+
"""Traverse an expression and return a typed, condition-aware singularity certificate.
|
|
1614
|
+
|
|
1615
|
+
Unlike :func:`function_singularities`, this records whether traversal was
|
|
1616
|
+
complete, preserves Piecewise branch guards, includes natural-domain
|
|
1617
|
+
exterior conditions, and promotes poles of an inner expression to
|
|
1618
|
+
essential singularities when composed through a transcendental entire
|
|
1619
|
+
function with an essential singularity at infinity.
|
|
1620
|
+
"""
|
|
1621
|
+
expr = sp.sympify(expr)
|
|
1622
|
+
var = sp.sympify(var)
|
|
1623
|
+
complex_mode = domain in (S.Complexes, "complex", "complexes", "C", "CC")
|
|
1624
|
+
bc = []
|
|
1625
|
+
dc = []
|
|
1626
|
+
po = []
|
|
1627
|
+
es = []
|
|
1628
|
+
bp = []
|
|
1629
|
+
rm = []
|
|
1630
|
+
di = []
|
|
1631
|
+
unknown = []
|
|
1632
|
+
transcendental_entire = {sp.exp, sp.sin, sp.cos, sp.sinh, sp.cosh, sp.erf, sp.erfc}
|
|
1633
|
+
|
|
1634
|
+
def visit(e, guard=S.true, inside_essential=False):
|
|
1635
|
+
if not getattr(e, "has", lambda *_: False)(var):
|
|
1636
|
+
return
|
|
1637
|
+
# Piecewise values are traversed only under their active branch guard.
|
|
1638
|
+
if isinstance(e, sp.Piecewise):
|
|
1639
|
+
covered = S.false
|
|
1640
|
+
for value, cond in e.args:
|
|
1641
|
+
active = (
|
|
1642
|
+
sp.Not(covered) if cond is S.true else And(cond, sp.Not(covered))
|
|
1643
|
+
)
|
|
1644
|
+
active = simplify_condition(And(guard, active), assumptions=assumptions)
|
|
1645
|
+
if active is not S.false:
|
|
1646
|
+
visit(value, active, inside_essential)
|
|
1647
|
+
covered = Or(covered, cond)
|
|
1648
|
+
for boundary in _piecewise_boundaries(e):
|
|
1649
|
+
dc.append(
|
|
1650
|
+
Singularity(
|
|
1651
|
+
SingularityKind.DEFINITION_CUT,
|
|
1652
|
+
boundary,
|
|
1653
|
+
guard,
|
|
1654
|
+
source="piecewise-boundary",
|
|
1655
|
+
)
|
|
1656
|
+
)
|
|
1657
|
+
verdict = _piecewise_discontinuous_at(e, var, boundary)
|
|
1658
|
+
if verdict is not False:
|
|
1659
|
+
di.append(
|
|
1660
|
+
Singularity(
|
|
1661
|
+
SingularityKind.DISCONTINUITY,
|
|
1662
|
+
boundary,
|
|
1663
|
+
guard,
|
|
1664
|
+
source="piecewise-boundary",
|
|
1665
|
+
)
|
|
1666
|
+
)
|
|
1667
|
+
return
|
|
1668
|
+
|
|
1669
|
+
# Collect local information at this node, but condition it by traversal guard.
|
|
1670
|
+
if e.is_Function or isinstance(e, sp.Pow):
|
|
1671
|
+
if complex_mode:
|
|
1672
|
+
bc.extend(
|
|
1673
|
+
_conditioned(
|
|
1674
|
+
function_branchcuts(
|
|
1675
|
+
e, var, assumptions=assumptions, mode="records"
|
|
1676
|
+
),
|
|
1677
|
+
guard,
|
|
1678
|
+
)
|
|
1679
|
+
)
|
|
1680
|
+
bp.extend(
|
|
1681
|
+
_conditioned(
|
|
1682
|
+
function_branchpoints(e, var, assumptions=assumptions), guard
|
|
1683
|
+
)
|
|
1684
|
+
)
|
|
1685
|
+
dc.extend(
|
|
1686
|
+
_conditioned(
|
|
1687
|
+
function_definition_cuts(
|
|
1688
|
+
e,
|
|
1689
|
+
var,
|
|
1690
|
+
mode="complex" if complex_mode else "real",
|
|
1691
|
+
assumptions=assumptions,
|
|
1692
|
+
),
|
|
1693
|
+
guard,
|
|
1694
|
+
)
|
|
1695
|
+
)
|
|
1696
|
+
localp = _conditioned(
|
|
1697
|
+
function_poles(e, var, assumptions=assumptions), guard
|
|
1698
|
+
)
|
|
1699
|
+
if inside_essential:
|
|
1700
|
+
es.extend(
|
|
1701
|
+
Singularity(
|
|
1702
|
+
SingularityKind.ESSENTIAL,
|
|
1703
|
+
p.condition,
|
|
1704
|
+
p.guard,
|
|
1705
|
+
p.multiplicity,
|
|
1706
|
+
"outer-essential-at-infinity",
|
|
1707
|
+
)
|
|
1708
|
+
for p in localp
|
|
1709
|
+
)
|
|
1710
|
+
else:
|
|
1711
|
+
po.extend(localp)
|
|
1712
|
+
es.extend(
|
|
1713
|
+
_conditioned(
|
|
1714
|
+
function_essential_singularities(e, var, assumptions=assumptions),
|
|
1715
|
+
guard,
|
|
1716
|
+
)
|
|
1717
|
+
)
|
|
1718
|
+
rm.extend(_conditioned(function_removable_singularities(e, var), guard))
|
|
1719
|
+
if not complex_mode:
|
|
1720
|
+
di.extend(
|
|
1721
|
+
_conditioned(
|
|
1722
|
+
function_discontinuities(e, var, assumptions=assumptions), guard
|
|
1723
|
+
)
|
|
1724
|
+
)
|
|
1725
|
+
if not _primitive_known(e, var):
|
|
1726
|
+
unknown.append(getattr(e.func, "__name__", str(e.func)))
|
|
1727
|
+
if not ignore_unknown:
|
|
1728
|
+
return
|
|
1729
|
+
|
|
1730
|
+
outer_essential = inside_essential or (
|
|
1731
|
+
getattr(e, "func", None) in transcendental_entire
|
|
1732
|
+
)
|
|
1733
|
+
args = (
|
|
1734
|
+
[v for v, _ in e.args]
|
|
1735
|
+
if isinstance(e, sp.Piecewise)
|
|
1736
|
+
else getattr(e, "args", ())
|
|
1737
|
+
)
|
|
1738
|
+
for a in args:
|
|
1739
|
+
if getattr(a, "has", lambda *_: False)(var):
|
|
1740
|
+
visit(a, guard, outer_essential)
|
|
1741
|
+
|
|
1742
|
+
visit(expr)
|
|
1743
|
+
dex = _domain_exterior_condition(expr, var, not complex_mode)
|
|
1744
|
+
complete = dex is not None and (ignore_unknown or not unknown)
|
|
1745
|
+
return SingularityAnalysis(
|
|
1746
|
+
branch_cuts=_unique(bc),
|
|
1747
|
+
definition_cuts=_unique(dc),
|
|
1748
|
+
poles=_unique(po),
|
|
1749
|
+
essential=_unique(es),
|
|
1750
|
+
branch_points=_unique(bp),
|
|
1751
|
+
removable=_unique(rm),
|
|
1752
|
+
discontinuities=_unique(di),
|
|
1753
|
+
domain_exterior=S.false if dex is None else dex,
|
|
1754
|
+
complete=complete,
|
|
1755
|
+
unknown_heads=tuple(sorted(set(unknown))),
|
|
1756
|
+
)
|
|
1757
|
+
|
|
1758
|
+
|
|
1759
|
+
def function_singularity_analysis(
|
|
1760
|
+
expr, var, *, domain=S.Complexes, assumptions=S.true, ignore_unknown=False
|
|
1761
|
+
):
|
|
1762
|
+
"""Cached aggregate singularity analysis with assumptions in the cache key."""
|
|
1763
|
+
from .proof_cache import property_proof_context
|
|
1764
|
+
|
|
1765
|
+
return property_proof_context(expr, var, assumptions=assumptions).singularities(
|
|
1766
|
+
domain=domain, ignore_unknown=ignore_unknown
|
|
1767
|
+
)
|