funcprops 0.1.0__py3-none-any.whl

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