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,168 @@
1
+ from __future__ import annotations
2
+
3
+ import sympy as sp
4
+ from sympy import S
5
+
6
+ from ..exceptions import SYMBOLIC_QUERY_ERRORS
7
+ from ..results import Boundedness, ConditionalValue, PropertyResult
8
+ from ..stratification import normalize_cases
9
+ from .common import combine_natural_domain_condition, condition_to_set
10
+ from .domain import function_domain
11
+ from .piecewise import (
12
+ combine_piecewise_boundedness,
13
+ piecewise_branches,
14
+ )
15
+ from .range import function_range
16
+
17
+
18
+ def _boundedness_from_range_condition(cond, y):
19
+ """Infer two-sided boundedness from an exact range representation.
20
+
21
+ Indexed extremum ranges already carry exact endpoint objects, so boundedness
22
+ depends only on whether either endpoint is infinite; no envelope reduction
23
+ is required here.
24
+ """
25
+ try:
26
+ from ..transcendental.extrema import ExtremumRange
27
+
28
+ if isinstance(cond, ExtremumRange):
29
+ if not cond.complete:
30
+ return Boundedness.UNKNOWN
31
+ lo, hi = cond.lower, cond.upper
32
+ below = lo is not S.NegativeInfinity and lo != S.NegativeInfinity
33
+ above = hi is not S.Infinity and hi != S.Infinity
34
+ if below and above:
35
+ return Boundedness.BOUNDED
36
+ if below:
37
+ return Boundedness.BOUNDED_BELOW
38
+ if above:
39
+ return Boundedness.BOUNDED_ABOVE
40
+ return Boundedness.UNBOUNDED
41
+ except SYMBOLIC_QUERY_ERRORS:
42
+ pass
43
+ # Parameter-dependent endpoints often cannot be converted by SymPy's
44
+ # ``as_set`` (e.g. 0 < y < gamma(a)). Infer one-/two-sided boundedness
45
+ # directly from exact relational structure before falling back to Sets.
46
+ c = sp.sympify(cond)
47
+ rels = c.args if isinstance(c, sp.And) else (c,)
48
+ lower = False
49
+ upper = False
50
+ for r in rels:
51
+ if not isinstance(r, sp.core.relational.Relational):
52
+ continue
53
+ try:
54
+ if r.lhs == y and not r.rhs.has(y):
55
+ if isinstance(r, (sp.StrictGreaterThan, sp.GreaterThan)):
56
+ lower = True
57
+ elif isinstance(r, (sp.StrictLessThan, sp.LessThan)):
58
+ upper = True
59
+ elif r.rhs == y and not r.lhs.has(y):
60
+ if isinstance(r, (sp.StrictLessThan, sp.LessThan)):
61
+ lower = True
62
+ elif isinstance(r, (sp.StrictGreaterThan, sp.GreaterThan)):
63
+ upper = True
64
+ except SYMBOLIC_QUERY_ERRORS:
65
+ pass
66
+ if lower and upper:
67
+ return Boundedness.BOUNDED
68
+ if lower:
69
+ return Boundedness.BOUNDED_BELOW
70
+ if upper:
71
+ return Boundedness.BOUNDED_ABOVE
72
+ s = condition_to_set(cond, y)
73
+ if s is None:
74
+ return Boundedness.UNKNOWN
75
+ if s is S.EmptySet:
76
+ # The empty-domain convention is vacuously bounded in both directions.
77
+ return Boundedness.BOUNDED
78
+ try:
79
+ lo = s.inf
80
+ hi = s.sup
81
+ except SYMBOLIC_QUERY_ERRORS:
82
+ return Boundedness.UNKNOWN
83
+ below = lo != S.NegativeInfinity
84
+ above = hi != S.Infinity
85
+ if below and above:
86
+ return Boundedness.BOUNDED
87
+ if below:
88
+ return Boundedness.BOUNDED_BELOW
89
+ if above:
90
+ return Boundedness.BOUNDED_ABOVE
91
+ return Boundedness.UNBOUNDED
92
+
93
+
94
+ def _structural_boundedness(expr, var, domain):
95
+ """Apply cheap closure rules before invoking a full range computation."""
96
+ if not expr.has(var):
97
+ return Boundedness.BOUNDED
98
+ if expr.func in (sp.sin, sp.cos, sp.tanh, sp.atan):
99
+ return Boundedness.BOUNDED
100
+ if isinstance(expr, sp.Piecewise):
101
+ branches = piecewise_branches(expr, var, domain)
102
+ if branches:
103
+ values = [
104
+ function_boundedness(branch.expression, var, domain=branch.condition)
105
+ for branch in branches
106
+ ]
107
+ return combine_piecewise_boundedness(values)
108
+ if expr.func is sp.Abs:
109
+ inner = function_boundedness(expr.args[0], var, domain=domain)
110
+ if inner is Boundedness.BOUNDED:
111
+ return Boundedness.BOUNDED
112
+ return Boundedness.BOUNDED_BELOW
113
+ if expr.func is sp.exp:
114
+ inner = function_boundedness(expr.args[0], var, domain=domain)
115
+ if inner in (Boundedness.BOUNDED, Boundedness.BOUNDED_ABOVE):
116
+ return Boundedness.BOUNDED
117
+ return Boundedness.BOUNDED_BELOW
118
+ if expr.is_Add:
119
+ values = [function_boundedness(a, var, domain=domain) for a in expr.args]
120
+ if not any(isinstance(v, PropertyResult) for v in values):
121
+ upper = {Boundedness.BOUNDED, Boundedness.BOUNDED_ABOVE}
122
+ lower = {Boundedness.BOUNDED, Boundedness.BOUNDED_BELOW}
123
+ above = all(v in upper for v in values)
124
+ below = all(v in lower for v in values)
125
+ if above and below:
126
+ return Boundedness.BOUNDED
127
+ if above:
128
+ return Boundedness.BOUNDED_ABOVE
129
+ if below:
130
+ return Boundedness.BOUNDED_BELOW
131
+ if expr.is_Mul:
132
+ values = [function_boundedness(a, var, domain=domain) for a in expr.args]
133
+ if values and all(v is Boundedness.BOUNDED for v in values):
134
+ return Boundedness.BOUNDED
135
+ return Boundedness.UNKNOWN
136
+
137
+
138
+ def function_boundedness(expr, var, *, domain=True):
139
+ expr = sp.sympify(expr)
140
+ var = sp.sympify(var)
141
+ y = sp.Dummy("_y", real=True)
142
+ structural = _structural_boundedness(expr, var, domain)
143
+ if structural is not Boundedness.UNKNOWN:
144
+ return structural
145
+ natural = function_domain(expr, var)
146
+ # function_range owns natural-domain intersection. Passing an already
147
+ # combined mixed parameter/domain formula can obscure a simple user domain
148
+ # (e.g. x>=0) and prevent endpoint-aware special-function rules from firing.
149
+ constraints = combine_natural_domain_condition(natural, domain, var)
150
+ try:
151
+ cond = function_range(expr, var, y, constraints=constraints)
152
+ except SYMBOLIC_QUERY_ERRORS:
153
+ return Boundedness.UNKNOWN
154
+ if isinstance(cond, PropertyResult):
155
+ out = normalize_cases(
156
+ (
157
+ ConditionalValue(
158
+ _boundedness_from_range_condition(c.value, y), c.condition
159
+ )
160
+ for c in cond.cases
161
+ ),
162
+ primary=_boundedness_from_range_condition(cond.primary, y)
163
+ if cond.primary is not None
164
+ else None,
165
+ source="range-boundedness-stratification",
166
+ )
167
+ return out if out.cases else Boundedness.UNKNOWN
168
+ return _boundedness_from_range_condition(cond, y)
@@ -0,0 +1,244 @@
1
+ from __future__ import annotations
2
+
3
+ import sympy as sp
4
+ from sympy import S
5
+
6
+ from ..exceptions import SYMBOLIC_QUERY_ERRORS
7
+
8
+
9
+ def is_semialgebraic_expression(expr: sp.Expr, var: sp.Symbol) -> bool:
10
+ """Conservative structural test for the subset handled by semialg.
11
+
12
+ Algebraic powers with rational exponent, Abs/Min/Max/Piecewise and ordinary
13
+ polynomial/rational arithmetic are accepted. Unknown Function heads are not.
14
+ """
15
+ expr = sp.sympify(expr)
16
+ if not expr.has(var):
17
+ return True
18
+ if expr.is_Atom:
19
+ return expr == var or expr.is_number or expr.is_Symbol
20
+ if expr.func in (sp.Add, sp.Mul):
21
+ return all(is_semialgebraic_expression(a, var) for a in expr.args)
22
+ if expr.func is sp.Pow:
23
+ b, e = expr.args
24
+ return bool(e.is_Rational) and is_semialgebraic_expression(b, var)
25
+ if expr.func in (sp.Abs, sp.Min, sp.Max):
26
+ return all(is_semialgebraic_expression(a, var) for a in expr.args)
27
+ if isinstance(expr, sp.Piecewise):
28
+ return all(
29
+ is_semialgebraic_expression(v, var) and is_semialgebraic_condition(c, var)
30
+ for v, c in expr.args
31
+ )
32
+ return False
33
+
34
+
35
+ def is_semialgebraic_condition(cond, var):
36
+ cond = sp.sympify(cond)
37
+ if cond in (S.true, S.false):
38
+ return True
39
+ if isinstance(cond, (sp.And, sp.Or)):
40
+ return all(is_semialgebraic_condition(a, var) for a in cond.args)
41
+ if isinstance(cond, sp.Not):
42
+ return is_semialgebraic_condition(cond.args[0], var)
43
+ if isinstance(cond, sp.core.relational.Relational):
44
+ return is_semialgebraic_expression(
45
+ cond.lhs, var
46
+ ) and is_semialgebraic_expression(cond.rhs, var)
47
+ return False
48
+
49
+
50
+ def combine_domain(user_domain, natural_domain):
51
+ if user_domain is None or user_domain is True or user_domain is S.true:
52
+ return sp.sympify(natural_domain)
53
+ if isinstance(user_domain, sp.Set):
54
+ return sp.And(sp.Contains(sp.Symbol("_fp_dummy"), user_domain), natural_domain)
55
+ return sp.simplify(sp.And(sp.sympify(user_domain), sp.sympify(natural_domain)))
56
+
57
+
58
+ def condition_to_set(condition, var):
59
+ """Convert ordinary univariate Boolean conditions to a SymPy Set.
60
+
61
+ SymPy's ``Relational.as_set()`` can refuse parameterized endpoints such as
62
+ ``x >= A`` even though the set is exactly ``[A, oo)`` with respect to x.
63
+ Handle those one-variable affine endpoint relations structurally first.
64
+ """
65
+ condition = sp.sympify(condition)
66
+ var = sp.sympify(var)
67
+ if condition is S.true:
68
+ return S.Reals
69
+ if condition is S.false:
70
+ return S.EmptySet
71
+ if (
72
+ isinstance(condition, sp.Contains)
73
+ and condition.args[0] == var
74
+ and isinstance(condition.args[1], sp.Set)
75
+ ):
76
+ return condition.args[1]
77
+ if isinstance(condition, sp.And):
78
+ pieces = [condition_to_set(a, var) for a in condition.args]
79
+ if all(isinstance(q, sp.Set) for q in pieces):
80
+ return sp.Intersection(*pieces)
81
+ if isinstance(condition, sp.Or):
82
+ pieces = [condition_to_set(a, var) for a in condition.args]
83
+ if all(isinstance(q, sp.Set) for q in pieces):
84
+ return sp.Union(*pieces)
85
+ if isinstance(condition, sp.core.relational.Relational):
86
+ lhs, rhs = condition.lhs, condition.rhs
87
+ if lhs == var and not rhs.has(var):
88
+ if isinstance(condition, sp.GreaterThan):
89
+ return sp.Interval(rhs, sp.oo)
90
+ if isinstance(condition, sp.StrictGreaterThan):
91
+ return sp.Interval.open(rhs, sp.oo)
92
+ if isinstance(condition, sp.LessThan):
93
+ return sp.Interval(-sp.oo, rhs)
94
+ if isinstance(condition, sp.StrictLessThan):
95
+ return sp.Interval.open(-sp.oo, rhs)
96
+ if isinstance(condition, sp.Equality):
97
+ return sp.FiniteSet(rhs)
98
+ if rhs == var and not lhs.has(var):
99
+ if isinstance(condition, sp.GreaterThan):
100
+ return sp.Interval(-sp.oo, lhs)
101
+ if isinstance(condition, sp.StrictGreaterThan):
102
+ return sp.Interval.open(-sp.oo, lhs)
103
+ if isinstance(condition, sp.LessThan):
104
+ return sp.Interval(lhs, sp.oo)
105
+ if isinstance(condition, sp.StrictLessThan):
106
+ return sp.Interval.open(lhs, sp.oo)
107
+ if isinstance(condition, sp.Equality):
108
+ return sp.FiniteSet(lhs)
109
+ try:
110
+ if isinstance(
111
+ condition, (sp.logic.boolalg.Boolean, sp.core.relational.Relational)
112
+ ):
113
+ return condition.as_set()
114
+ except (AttributeError, *SYMBOLIC_QUERY_ERRORS):
115
+ # Semantic catalogue predicates are BooleanFunction subclasses without
116
+ # SymPy's optional _eval_as_set implementation.
117
+ pass
118
+ return None
119
+
120
+
121
+ def set_to_condition(set_, y):
122
+ """Convert a real Set to a Boolean membership condition in ``y``."""
123
+ if set_ == S.Reals:
124
+ return S.true
125
+ if set_ == S.EmptySet:
126
+ return S.false
127
+ if isinstance(set_, sp.Interval):
128
+ pieces = []
129
+ if set_.start is not S.NegativeInfinity:
130
+ pieces.append(y > set_.start if set_.left_open else y >= set_.start)
131
+ if set_.end is not S.Infinity:
132
+ pieces.append(y < set_.end if set_.right_open else y <= set_.end)
133
+ return sp.And(*pieces) if pieces else S.true
134
+ if isinstance(set_, sp.FiniteSet):
135
+ return sp.Or(*(sp.Eq(y, v) for v in set_)) if set_ else S.false
136
+ if isinstance(set_, sp.Union):
137
+ return sp.Or(*(set_to_condition(a, y) for a in set_.args))
138
+ if isinstance(set_, sp.Complement):
139
+ a, b = set_.args
140
+ return sp.And(set_to_condition(a, y), sp.Not(set_to_condition(b, y)))
141
+ return sp.Contains(y, set_)
142
+
143
+
144
+ def interval_components(condition, var):
145
+ s = condition_to_set(condition, var)
146
+ if s is None:
147
+ return None
148
+ if isinstance(s, sp.Interval):
149
+ return (s,)
150
+ if isinstance(s, sp.Union) and all(isinstance(a, sp.Interval) for a in s.args):
151
+ return tuple(s.args)
152
+ if s == S.Reals:
153
+ return (sp.Interval(-sp.oo, sp.oo),)
154
+ return None
155
+
156
+
157
+ def is_full_natural_domain(requested_domain, natural_domain):
158
+ if (
159
+ requested_domain is True
160
+ or requested_domain is S.true
161
+ or requested_domain is None
162
+ ):
163
+ return True
164
+ try:
165
+ # Custom catalogue Boolean predicates must be lowered before generic
166
+ # SymPy Boolean simplification; otherwise unknown predicate heads can
167
+ # be simplified unsafely or merely syntactically.
168
+ from ..stratification import semantic_condition
169
+
170
+ req = semantic_condition(requested_domain)
171
+ nat = semantic_condition(natural_domain)
172
+ eq = sp.simplify(sp.simplify_logic(sp.Equivalent(req, nat)))
173
+ return eq is S.true
174
+ except SYMBOLIC_QUERY_ERRORS:
175
+ return False
176
+
177
+
178
+ def exact_order_real_points(points):
179
+ """Order exact real points only when every comparison is certified.
180
+
181
+ Returns ``None`` rather than using structural or floating-point ordering
182
+ when a pair cannot be compared exactly. This helper is shared by sign,
183
+ monotonic-partition, extrema, and inverse reasoning.
184
+ """
185
+ out = []
186
+ for q in points:
187
+ q = sp.sympify(q)
188
+ placed = False
189
+ for i, r in enumerate(out):
190
+ d = sp.simplify(q - r)
191
+ if d.is_negative is True:
192
+ out.insert(i, q)
193
+ placed = True
194
+ break
195
+ if d.is_positive is True:
196
+ continue
197
+ if d.is_zero is True:
198
+ placed = True
199
+ break
200
+ return None
201
+ if not placed:
202
+ out.append(q)
203
+ return tuple(out)
204
+
205
+
206
+ def combine_natural_domain_condition(natural_domain, user_domain, var):
207
+ """Combine a natural-domain result with a requested real domain safely.
208
+
209
+ A ``PropertyResult`` means the natural domain was not established as one
210
+ Boolean condition. In that case preserve the explicit user restriction,
211
+ or use all reals when no restriction was supplied; never pass the result
212
+ object itself into SymPy Boolean simplification.
213
+ """
214
+ var = sp.sympify(var)
215
+ nat = None if hasattr(natural_domain, "primary") else sp.sympify(natural_domain)
216
+ if user_domain is True or user_domain is None or user_domain is S.true:
217
+ return S.true if nat is None else nat
218
+ if isinstance(user_domain, sp.Set):
219
+ usr = sp.Contains(var, user_domain)
220
+ else:
221
+ usr = sp.sympify(user_domain)
222
+ if nat is None:
223
+ return usr
224
+ try:
225
+ return sp.simplify(sp.And(nat, usr))
226
+ except SYMBOLIC_QUERY_ERRORS:
227
+ return sp.And(nat, usr)
228
+
229
+
230
+ def normalize_variables(var):
231
+ """Return a canonical tuple of symbolic variables.
232
+
233
+ Public APIs that accept either one variable or a variable collection must
234
+ not sympify a tuple and then test ``expr.has(tuple)``: SymPy treats the
235
+ resulting Tuple as one atomic expression. This helper centralizes the
236
+ scalar/collection distinction without changing scalar call sites.
237
+ """
238
+ if isinstance(var, (tuple, list, sp.Tuple)):
239
+ vv = tuple(sp.sympify(v) for v in var)
240
+ else:
241
+ vv = (sp.sympify(var),)
242
+ if not vv:
243
+ raise ValueError("at least one variable is required")
244
+ return vv