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,151 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
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 ..results import Oscillation
11
+ from ..stratification import evaluate_parameter_strata
12
+ from ..transcendental.periodic import positive_period
13
+ from .common import combine_natural_domain_condition, condition_to_set
14
+ from .domain import function_domain
15
+ from .monotonic_certificates import MonotonicPartition, monotonic_partition
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class OscillationCertificate:
20
+ classification: Oscillation
21
+ domain: sp.Set
22
+ partition: MonotonicPartition | None = None
23
+ turning_count: object = None
24
+ source: str = ""
25
+ evidence: tuple = ()
26
+
27
+
28
+ def function_oscillation(
29
+ expr,
30
+ var,
31
+ *,
32
+ domain=True,
33
+ details=False,
34
+ partition=None,
35
+ _domain_ready=False,
36
+ ):
37
+ """Classify real oscillation from the shared monotonic partition.
38
+
39
+ ``OSCILLATORY`` means a certified infinite sequence of monotonicity
40
+ reversals (or a nonconstant exact periodic function on an unbounded
41
+ interval). ``FINITELY_OSCILLATORY`` means a complete finite partition with
42
+ at least one direction reversal.
43
+ """
44
+ expr = sp.sympify(expr)
45
+ var = sp.sympify(var)
46
+ natural = function_domain(expr, var)
47
+ cond = (
48
+ sp.sympify(domain)
49
+ if _domain_ready
50
+ else combine_natural_domain_condition(natural, domain, var)
51
+ )
52
+ with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
53
+ cond = sp.simplify(cond)
54
+ if not _domain_ready:
55
+ split = evaluate_parameter_strata(
56
+ cond,
57
+ var,
58
+ lambda branch_domain: function_oscillation(
59
+ expr,
60
+ var,
61
+ domain=branch_domain,
62
+ details=details,
63
+ partition=None,
64
+ _domain_ready=True,
65
+ ),
66
+ source="oscillation-parameter-stratification",
67
+ )
68
+ if split is not None:
69
+ return split
70
+ ds = condition_to_set(cond, var)
71
+ if not isinstance(ds, sp.Set):
72
+ out = OscillationCertificate(
73
+ Oscillation.UNKNOWN, S.Reals, None, None, "unknown-domain"
74
+ )
75
+ return out if details else out.classification
76
+ if not expr.has(var):
77
+ out = OscillationCertificate(
78
+ Oscillation.NONOSCILLATORY, ds, None, 0, "constant"
79
+ )
80
+ return out if details else out.classification
81
+ part = (
82
+ partition
83
+ if partition is not None and getattr(partition, "domain", None) == ds
84
+ else monotonic_partition(expr, var, domain=ds)
85
+ )
86
+ if part.indexed is not None and part.indexed.boundary_geometry:
87
+ try:
88
+ from ..transcendental.zero_geometry import geometry_support_inside_domain
89
+
90
+ infinite_inside = any(
91
+ g.count is sp.oo and geometry_support_inside_domain(g, ds)
92
+ for g in part.indexed.boundary_geometry
93
+ )
94
+ except SYMBOLIC_QUERY_ERRORS:
95
+ infinite_inside = False
96
+ if infinite_inside:
97
+ out = OscillationCertificate(
98
+ Oscillation.OSCILLATORY,
99
+ ds,
100
+ part,
101
+ sp.oo,
102
+ "indexed-monotonic-partition",
103
+ (part,),
104
+ )
105
+ return out if details else out.classification
106
+ if part.complete and part.cells:
107
+ turns = part.direction_changes
108
+ cls = Oscillation.FINITELY_OSCILLATORY if turns else Oscillation.NONOSCILLATORY
109
+ out = OscillationCertificate(
110
+ cls, ds, part, turns, "finite-monotonic-partition", (part,)
111
+ )
112
+ return out if details else out.classification
113
+ # A certified global monotonic classification is a cheap exact proof of
114
+ # nonoscillation when critical-point partitioning itself is incomplete.
115
+ try:
116
+ from ..results import Monotonicity
117
+ from .monotonicity import function_monotonicity
118
+
119
+ m = function_monotonicity(expr, var, domain=cond, partition=part)
120
+ if m in (
121
+ Monotonicity.STRICTLY_INCREASING,
122
+ Monotonicity.NONDECREASING,
123
+ Monotonicity.CONSTANT,
124
+ Monotonicity.NONINCREASING,
125
+ Monotonicity.STRICTLY_DECREASING,
126
+ ):
127
+ out = OscillationCertificate(
128
+ Oscillation.NONOSCILLATORY,
129
+ ds,
130
+ part,
131
+ 0,
132
+ "global-monotonicity",
133
+ (m, part),
134
+ )
135
+ return out if details else out.classification
136
+ except SYMBOLIC_QUERY_ERRORS:
137
+ pass
138
+ p = positive_period(expr, var)
139
+ if (
140
+ p is not None
141
+ and isinstance(ds, sp.Interval)
142
+ and (ds.start is -sp.oo or ds.end is sp.oo)
143
+ ):
144
+ out = OscillationCertificate(
145
+ Oscillation.OSCILLATORY, ds, part, sp.oo, "exact-periodicity", (p, part)
146
+ )
147
+ return out if details else out.classification
148
+ out = OscillationCertificate(
149
+ Oscillation.UNKNOWN, ds, part, None, "incomplete-monotonic-partition", (part,)
150
+ )
151
+ return out if details else out.classification
@@ -0,0 +1,174 @@
1
+ """Shared branch-domain analysis for real piecewise expressions.
2
+
3
+ Property engines use the same effective-branch construction so branch guards,
4
+ user domains, and earlier Piecewise conditions are interpreted consistently.
5
+ The module contains only cheap exact combination rules; properties that need
6
+ interface derivatives or global image geometry remain responsible for those
7
+ additional proofs.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from itertools import pairwise
14
+
15
+ import sympy as sp
16
+ from sympy import S
17
+
18
+ from ..exceptions import SYMBOLIC_QUERY_ERRORS
19
+ from ..results import Boundedness, Monotonicity, Sign
20
+ from .common import condition_to_set
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class PiecewiseBranch:
25
+ """One expression branch together with its effective requested domain."""
26
+
27
+ expression: sp.Expr
28
+ condition: sp.Basic
29
+ domain: sp.Set | None
30
+
31
+
32
+ def piecewise_branches(expr, var, domain=S.true):
33
+ """Return nonempty effective branches of a ``Piecewise`` expression."""
34
+ expr = sp.sympify(expr)
35
+ var = sp.sympify(var)
36
+ if not isinstance(expr, sp.Piecewise):
37
+ return ()
38
+ covered = S.false
39
+ branches = []
40
+ for value, condition in expr.args:
41
+ try:
42
+ effective = sp.simplify_logic(
43
+ sp.And(sp.sympify(domain), condition, sp.Not(covered))
44
+ )
45
+ except SYMBOLIC_QUERY_ERRORS:
46
+ effective = sp.And(sp.sympify(domain), condition, sp.Not(covered))
47
+ covered = sp.Or(covered, condition)
48
+ if effective is S.false:
49
+ continue
50
+ domain_set = condition_to_set(effective, var)
51
+ if domain_set is S.EmptySet:
52
+ continue
53
+ branches.append(PiecewiseBranch(sp.sympify(value), effective, domain_set))
54
+ return tuple(branches)
55
+
56
+
57
+ def _scalar_value(result):
58
+ """Return an unconditional scalar result, or ``None`` for conditional data."""
59
+ from ..results import PropertyResult
60
+
61
+ if isinstance(result, PropertyResult):
62
+ if result.primary is not None:
63
+ return result.primary
64
+ return None
65
+ return result
66
+
67
+
68
+ def combine_piecewise_sign(values):
69
+ """Combine exact branch sign classifications into a global sign class."""
70
+ vals = tuple(_scalar_value(v) for v in values)
71
+ if not vals or any(v is None or v is Sign.UNKNOWN for v in vals):
72
+ return Sign.UNKNOWN
73
+ if all(v is Sign.ZERO for v in vals):
74
+ return Sign.ZERO
75
+ if all(v is Sign.POSITIVE for v in vals):
76
+ return Sign.POSITIVE
77
+ if all(v is Sign.NEGATIVE for v in vals):
78
+ return Sign.NEGATIVE
79
+ nonneg = {Sign.POSITIVE, Sign.NONNEGATIVE, Sign.ZERO}
80
+ nonpos = {Sign.NEGATIVE, Sign.NONPOSITIVE, Sign.ZERO}
81
+ if all(v in nonneg for v in vals):
82
+ return Sign.NONNEGATIVE
83
+ if all(v in nonpos for v in vals):
84
+ return Sign.NONPOSITIVE
85
+ if any(v in {Sign.POSITIVE, Sign.NONNEGATIVE} for v in vals) and any(
86
+ v in {Sign.NEGATIVE, Sign.NONPOSITIVE} for v in vals
87
+ ):
88
+ return Sign.SIGN_CHANGING
89
+ return Sign.UNKNOWN
90
+
91
+
92
+ def combine_piecewise_boundedness(values):
93
+ """Combine exact branch boundedness classes over a finite branch union."""
94
+ vals = tuple(_scalar_value(v) for v in values)
95
+ if not vals or any(v is None or v is Boundedness.UNKNOWN for v in vals):
96
+ return Boundedness.UNKNOWN
97
+ upper = {Boundedness.BOUNDED, Boundedness.BOUNDED_ABOVE}
98
+ lower = {Boundedness.BOUNDED, Boundedness.BOUNDED_BELOW}
99
+ above = all(v in upper for v in vals)
100
+ below = all(v in lower for v in vals)
101
+ if above and below:
102
+ return Boundedness.BOUNDED
103
+ if above:
104
+ return Boundedness.BOUNDED_ABOVE
105
+ if below:
106
+ return Boundedness.BOUNDED_BELOW
107
+ return Boundedness.UNBOUNDED
108
+
109
+
110
+ def combine_piecewise_monotonicity(values):
111
+ """Combine branch directions before interface ordering is checked.
112
+
113
+ A non-``UNKNOWN`` answer here means only that every branch has a compatible
114
+ direction. The caller must still prove that values do not jump in the
115
+ opposite direction at branch interfaces.
116
+ """
117
+ vals = tuple(_scalar_value(v) for v in values)
118
+ if not vals or any(v is None for v in vals):
119
+ return Monotonicity.UNKNOWN
120
+ inc = {Monotonicity.STRICTLY_INCREASING, Monotonicity.NONDECREASING}
121
+ dec = {Monotonicity.STRICTLY_DECREASING, Monotonicity.NONINCREASING}
122
+ if all(v is Monotonicity.CONSTANT for v in vals):
123
+ return Monotonicity.CONSTANT
124
+ if all(v in inc | {Monotonicity.CONSTANT} for v in vals):
125
+ return Monotonicity.NONDECREASING
126
+ if all(v in dec | {Monotonicity.CONSTANT} for v in vals):
127
+ return Monotonicity.NONINCREASING
128
+ if any(v in inc for v in vals) and any(v in dec for v in vals):
129
+ return Monotonicity.NOT_MONOTONE
130
+ return Monotonicity.UNKNOWN
131
+
132
+
133
+ def piecewise_interface_limits(branches, var, *, derivative_order=0):
134
+ """Return exact one-sided values at contiguous branch interfaces.
135
+
136
+ ``None`` is returned when branch domains are not ordered intervals, are not
137
+ contiguous, or an endpoint limit cannot be computed exactly.
138
+ """
139
+ if not branches or any(not isinstance(b.domain, sp.Interval) for b in branches):
140
+ return None
141
+ ordered = tuple(sorted(branches, key=lambda b: sp.default_sort_key(b.domain.start)))
142
+ from ..proof_cache import cached_derivative, cached_endpoint_limit
143
+
144
+ out = []
145
+ for left, right in pairwise(ordered):
146
+ boundary = left.domain.end
147
+ if sp.simplify(boundary - right.domain.start) != 0:
148
+ return None
149
+ left_expr = left.expression
150
+ right_expr = right.expression
151
+ if derivative_order:
152
+ try:
153
+ left_expr = cached_derivative(left_expr, var, order=derivative_order)
154
+ right_expr = cached_derivative(right_expr, var, order=derivative_order)
155
+ except SYMBOLIC_QUERY_ERRORS:
156
+ return None
157
+ left_value = cached_endpoint_limit(
158
+ left_expr,
159
+ var,
160
+ boundary,
161
+ direction="-",
162
+ attained=not left.domain.right_open,
163
+ )
164
+ right_value = cached_endpoint_limit(
165
+ right_expr,
166
+ var,
167
+ boundary,
168
+ direction="+",
169
+ attained=not right.domain.left_open,
170
+ )
171
+ if left_value is None or right_value is None:
172
+ return None
173
+ out.append((boundary, left_value, right_value))
174
+ return tuple(out)