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,221 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import sympy as sp
|
|
7
|
+
from sympy import S
|
|
8
|
+
|
|
9
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
10
|
+
from .real_sign import robust_real_sign
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ExponentialFourierCount:
|
|
15
|
+
expression: sp.Basic
|
|
16
|
+
variable: sp.Symbol
|
|
17
|
+
domain: sp.Interval
|
|
18
|
+
root_count: int | None
|
|
19
|
+
complete: bool
|
|
20
|
+
sequence: tuple[sp.Basic, ...] = ()
|
|
21
|
+
critical_points: tuple[sp.Basic, ...] = ()
|
|
22
|
+
evidence: tuple = ()
|
|
23
|
+
source: str = "normalized-exponential-derivative-sequence"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _affine_exp_term(term, var):
|
|
27
|
+
"""Return ``(rate, coefficient)`` for c*exp(rate*var+offset)."""
|
|
28
|
+
term = sp.expand_power_exp(term)
|
|
29
|
+
if not term.has(var):
|
|
30
|
+
return S.Zero, term
|
|
31
|
+
exps = tuple(term.atoms(sp.exp))
|
|
32
|
+
if len(exps) != 1:
|
|
33
|
+
return None
|
|
34
|
+
e = exps[0]
|
|
35
|
+
quotient = sp.simplify(term / e)
|
|
36
|
+
if quotient.has(var):
|
|
37
|
+
return None
|
|
38
|
+
try:
|
|
39
|
+
p = sp.Poly(sp.expand(e.args[0]), var)
|
|
40
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
41
|
+
return None
|
|
42
|
+
if p.degree() > 1:
|
|
43
|
+
return None
|
|
44
|
+
rate = sp.simplify(p.nth(1))
|
|
45
|
+
offset = sp.simplify(p.nth(0))
|
|
46
|
+
coeff = sp.simplify(quotient * sp.exp(offset))
|
|
47
|
+
if rate.has(var) or coeff.has(var):
|
|
48
|
+
return None
|
|
49
|
+
if rate.is_real is not True or coeff.is_real is False:
|
|
50
|
+
return None
|
|
51
|
+
return rate, coeff
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def exponential_polynomial_terms(expr, var):
|
|
55
|
+
"""Parse a restricted real exponential polynomial.
|
|
56
|
+
|
|
57
|
+
Supported inputs are finite sums of ``c*exp(a*x+b)`` and constants, with exact
|
|
58
|
+
real coefficients/rates. Equal rates are combined. This intentionally excludes
|
|
59
|
+
polynomial prefactors and nested exponentials.
|
|
60
|
+
"""
|
|
61
|
+
expr = sp.expand(sp.sympify(expr))
|
|
62
|
+
terms = {}
|
|
63
|
+
for term in sp.Add.make_args(expr):
|
|
64
|
+
item = _affine_exp_term(term, var)
|
|
65
|
+
if item is None:
|
|
66
|
+
return None
|
|
67
|
+
rate, coeff = item
|
|
68
|
+
if coeff.is_Float:
|
|
69
|
+
return None
|
|
70
|
+
terms[rate] = sp.simplify(terms.get(rate, S.Zero) + coeff)
|
|
71
|
+
terms = {a: c for a, c in terms.items() if robust_real_sign(c) != 0}
|
|
72
|
+
if not terms:
|
|
73
|
+
return ()
|
|
74
|
+
rates = list(terms)
|
|
75
|
+
try:
|
|
76
|
+
rates.sort(key=lambda q: float(sp.N(q, 50)))
|
|
77
|
+
except (TypeError, ValueError):
|
|
78
|
+
return None
|
|
79
|
+
for a, b in itertools.pairwise(rates):
|
|
80
|
+
if robust_real_sign(b - a) != 1:
|
|
81
|
+
return None
|
|
82
|
+
return tuple((a, terms[a]) for a in rates)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _normalized_derivative(expr, var):
|
|
86
|
+
terms = exponential_polynomial_terms(expr, var)
|
|
87
|
+
if terms is None or len(terms) <= 1:
|
|
88
|
+
return None
|
|
89
|
+
a0 = terms[0][0]
|
|
90
|
+
normalized = sp.expand(sp.exp(-a0 * var) * expr)
|
|
91
|
+
derivative = sp.simplify(sp.diff(normalized, var))
|
|
92
|
+
return normalized, derivative
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _finite_exact_roots(expr, var, domain):
|
|
96
|
+
try:
|
|
97
|
+
roots = sp.solveset(sp.Eq(expr, 0), var, domain=domain)
|
|
98
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
99
|
+
return None
|
|
100
|
+
if roots is S.EmptySet:
|
|
101
|
+
return ()
|
|
102
|
+
if not isinstance(roots, sp.FiniteSet):
|
|
103
|
+
return None
|
|
104
|
+
vals = []
|
|
105
|
+
for r in roots:
|
|
106
|
+
if r.is_real is False:
|
|
107
|
+
return None
|
|
108
|
+
contained = domain.contains(r)
|
|
109
|
+
if contained is S.false:
|
|
110
|
+
continue
|
|
111
|
+
if contained is not S.true:
|
|
112
|
+
return None
|
|
113
|
+
vals.append(r)
|
|
114
|
+
try:
|
|
115
|
+
vals.sort(key=lambda q: float(sp.N(q, 50)))
|
|
116
|
+
except (TypeError, ValueError):
|
|
117
|
+
return None
|
|
118
|
+
return tuple(vals)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _point_sign(expr, var, point, direction=None):
|
|
122
|
+
if point is S.NegativeInfinity or point is S.Infinity:
|
|
123
|
+
try:
|
|
124
|
+
value = sp.limit(expr, var, point, dir=direction or "+")
|
|
125
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
126
|
+
return None
|
|
127
|
+
else:
|
|
128
|
+
try:
|
|
129
|
+
value = sp.simplify(expr.subs(var, point))
|
|
130
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
131
|
+
return None
|
|
132
|
+
return robust_real_sign(value)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def exponential_fourier_count(expr, var, domain):
|
|
136
|
+
"""Certify the number of distinct real zeros of a restricted exp polynomial.
|
|
137
|
+
|
|
138
|
+
The proof is recursive. Multiplication by the positive factor exp(-a0*x)
|
|
139
|
+
preserves zeros; differentiating the normalized expression removes its lowest
|
|
140
|
+
exponential term. Exact derivative roots partition the interval into cells on
|
|
141
|
+
which the normalized expression is strictly monotone. Endpoint/critical-point
|
|
142
|
+
signs then count exactly one or zero roots per cell. The method declines to
|
|
143
|
+
certify whenever a derivative root or sign cannot be established exactly.
|
|
144
|
+
"""
|
|
145
|
+
expr = sp.sympify(expr)
|
|
146
|
+
var = sp.sympify(var)
|
|
147
|
+
if not isinstance(domain, sp.Interval):
|
|
148
|
+
return None
|
|
149
|
+
terms = exponential_polynomial_terms(expr, var)
|
|
150
|
+
if terms is None or not terms:
|
|
151
|
+
return None
|
|
152
|
+
if len(terms) == 1:
|
|
153
|
+
sign = robust_real_sign(terms[0][1])
|
|
154
|
+
if sign in (-1, 1):
|
|
155
|
+
return ExponentialFourierCount(expr, var, domain, 0, True, (expr,))
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
nd = _normalized_derivative(expr, var)
|
|
159
|
+
if nd is None:
|
|
160
|
+
return None
|
|
161
|
+
normalized, derivative = nd
|
|
162
|
+
derivative_roots = _finite_exact_roots(derivative, var, domain)
|
|
163
|
+
if derivative_roots is None:
|
|
164
|
+
return None
|
|
165
|
+
derivative_roots = tuple(
|
|
166
|
+
r for r in derivative_roots if r != domain.start and r != domain.end
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Recursively require the derivative sequence itself to be in the supported
|
|
170
|
+
# class. This is proof evidence that no derivative roots were missed.
|
|
171
|
+
child = exponential_fourier_count(derivative, var, domain)
|
|
172
|
+
if child is None or not child.complete or child.root_count != len(derivative_roots):
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
points = [domain.start, *derivative_roots, domain.end]
|
|
176
|
+
signs = []
|
|
177
|
+
for point in points:
|
|
178
|
+
if point == domain.start and domain.left_open:
|
|
179
|
+
s = _point_sign(normalized, var, point, "+")
|
|
180
|
+
elif point == domain.end and domain.right_open:
|
|
181
|
+
s = _point_sign(normalized, var, point, "-")
|
|
182
|
+
else:
|
|
183
|
+
s = _point_sign(normalized, var, point)
|
|
184
|
+
if s is None:
|
|
185
|
+
return None
|
|
186
|
+
signs.append(s)
|
|
187
|
+
|
|
188
|
+
attained = [
|
|
189
|
+
not domain.left_open,
|
|
190
|
+
*([True] * len(derivative_roots)),
|
|
191
|
+
not domain.right_open,
|
|
192
|
+
]
|
|
193
|
+
count = sum(
|
|
194
|
+
1
|
|
195
|
+
for s, is_attained in zip(signs, attained, strict=True)
|
|
196
|
+
if s == 0 and is_attained
|
|
197
|
+
)
|
|
198
|
+
# A critical-point root belongs to both neighboring closures; count it once and
|
|
199
|
+
# do not also count a sign-change root through that endpoint.
|
|
200
|
+
for left, right in itertools.pairwise(signs):
|
|
201
|
+
if left and right and left != right:
|
|
202
|
+
count += 1
|
|
203
|
+
|
|
204
|
+
sequence = (normalized, *child.sequence)
|
|
205
|
+
return ExponentialFourierCount(
|
|
206
|
+
expr,
|
|
207
|
+
var,
|
|
208
|
+
domain,
|
|
209
|
+
count,
|
|
210
|
+
True,
|
|
211
|
+
sequence,
|
|
212
|
+
derivative_roots,
|
|
213
|
+
evidence=(("signs", tuple(signs)), ("derivative", child)),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
__all__ = [
|
|
218
|
+
"ExponentialFourierCount",
|
|
219
|
+
"exponential_fourier_count",
|
|
220
|
+
"exponential_polynomial_terms",
|
|
221
|
+
]
|