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,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)
|