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,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sympy as sp
|
|
4
|
+
|
|
5
|
+
from ..results import ConditionalValue, Continuity, Differentiability, PropertyResult
|
|
6
|
+
from ..stratification import normalize_cases
|
|
7
|
+
from .smoothness import function_smoothness
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _differentiability_implications(
|
|
11
|
+
primary: Differentiability,
|
|
12
|
+
) -> tuple[Differentiability, ...]:
|
|
13
|
+
"""Return strictly weaker differentiability properties implied by ``primary``."""
|
|
14
|
+
if primary is Differentiability.SMOOTH:
|
|
15
|
+
return (Differentiability.CK, Differentiability.DIFFERENTIABLE)
|
|
16
|
+
if primary is Differentiability.CK:
|
|
17
|
+
return (Differentiability.DIFFERENTIABLE,)
|
|
18
|
+
return ()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _classify_smoothness(smoothness, order):
|
|
22
|
+
"""Map a regularity certificate to the public differentiability class."""
|
|
23
|
+
verified = smoothness.differentiability_order
|
|
24
|
+
if order is not None:
|
|
25
|
+
if verified is sp.oo or verified == sp.oo:
|
|
26
|
+
return Differentiability.SMOOTH
|
|
27
|
+
if isinstance(verified, int) and verified >= order:
|
|
28
|
+
return (
|
|
29
|
+
Differentiability.DIFFERENTIABLE if order == 1 else Differentiability.CK
|
|
30
|
+
)
|
|
31
|
+
if smoothness.continuity is Continuity.DISCONTINUOUS or (
|
|
32
|
+
smoothness.smooth is False
|
|
33
|
+
and isinstance(verified, int)
|
|
34
|
+
and verified < order
|
|
35
|
+
):
|
|
36
|
+
return Differentiability.NOT_DIFFERENTIABLE
|
|
37
|
+
return Differentiability.UNKNOWN
|
|
38
|
+
if verified is sp.oo or verified == sp.oo or smoothness.smooth is True:
|
|
39
|
+
return Differentiability.SMOOTH
|
|
40
|
+
if smoothness.continuity is Continuity.DISCONTINUOUS:
|
|
41
|
+
return Differentiability.NOT_DIFFERENTIABLE
|
|
42
|
+
if isinstance(verified, int) and verified >= 2:
|
|
43
|
+
return Differentiability.CK
|
|
44
|
+
if verified == 1:
|
|
45
|
+
return Differentiability.DIFFERENTIABLE
|
|
46
|
+
if smoothness.smooth is False and verified == 0:
|
|
47
|
+
return Differentiability.NOT_DIFFERENTIABLE
|
|
48
|
+
return Differentiability.UNKNOWN
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def function_differentiability(
|
|
52
|
+
expr,
|
|
53
|
+
var,
|
|
54
|
+
*,
|
|
55
|
+
domain=True,
|
|
56
|
+
order=None,
|
|
57
|
+
all_properties=False,
|
|
58
|
+
details=False,
|
|
59
|
+
max_order=6,
|
|
60
|
+
):
|
|
61
|
+
"""Classify differentiability on the requested real domain.
|
|
62
|
+
|
|
63
|
+
Unlike a syntactic ``diff`` test, this query requires the relevant
|
|
64
|
+
derivatives to be continuous on the whole requested domain. ``order=k``
|
|
65
|
+
asks whether the function is at least :math:`C^k`; without an order the
|
|
66
|
+
strongest certified class is returned. Parameter strata are preserved.
|
|
67
|
+
"""
|
|
68
|
+
expr = sp.sympify(expr)
|
|
69
|
+
var = sp.sympify(var)
|
|
70
|
+
if order is not None:
|
|
71
|
+
if not isinstance(order, int) or isinstance(order, bool) or order < 1:
|
|
72
|
+
raise ValueError("order must be a positive integer or None")
|
|
73
|
+
probe_order = max(max_order, order)
|
|
74
|
+
else:
|
|
75
|
+
probe_order = max_order
|
|
76
|
+
if (
|
|
77
|
+
not isinstance(probe_order, int)
|
|
78
|
+
or isinstance(probe_order, bool)
|
|
79
|
+
or probe_order < 1
|
|
80
|
+
):
|
|
81
|
+
raise ValueError("max_order must be a positive integer")
|
|
82
|
+
|
|
83
|
+
# Differentiability is a derivative-based property. Populate the shared raw
|
|
84
|
+
# derivative cache before the higher-level smoothness analysis so callers
|
|
85
|
+
# consistently reuse the unfactored derivative proof object. Keep
|
|
86
|
+
# factorization lazy: several structural smoothness rules (for example Abs)
|
|
87
|
+
# can otherwise bypass the derivative layer entirely.
|
|
88
|
+
from ..proof_cache import cached_derivative
|
|
89
|
+
|
|
90
|
+
cached_derivative(expr, var, order=1)
|
|
91
|
+
smoothness = function_smoothness(expr, var, domain=domain, max_order=probe_order)
|
|
92
|
+
if isinstance(smoothness, PropertyResult):
|
|
93
|
+
out = normalize_cases(
|
|
94
|
+
(
|
|
95
|
+
ConditionalValue(_classify_smoothness(c.value, order), c.condition)
|
|
96
|
+
for c in smoothness.cases
|
|
97
|
+
),
|
|
98
|
+
source="differentiability-parameter-stratification",
|
|
99
|
+
evidence=smoothness.evidence,
|
|
100
|
+
)
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
primary = _classify_smoothness(smoothness, order)
|
|
104
|
+
if all_properties or details:
|
|
105
|
+
return PropertyResult(
|
|
106
|
+
primary=primary,
|
|
107
|
+
all_properties=_differentiability_implications(primary),
|
|
108
|
+
source="smoothness-analysis",
|
|
109
|
+
exceptions=smoothness.exceptional_loci,
|
|
110
|
+
evidence=(smoothness,),
|
|
111
|
+
)
|
|
112
|
+
return primary
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sympy as sp
|
|
4
|
+
from sympy import S
|
|
5
|
+
|
|
6
|
+
from .. import _semialg
|
|
7
|
+
from ..engine import evaluate_property, simplify_condition
|
|
8
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
9
|
+
from ..results import PropertyResult
|
|
10
|
+
from ..stratification import (
|
|
11
|
+
semantic_condition,
|
|
12
|
+
stratify_boolean_by_parameters,
|
|
13
|
+
)
|
|
14
|
+
from .common import is_semialgebraic_expression
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _pow_real_domain(expr, var):
|
|
18
|
+
base, exponent = expr.args
|
|
19
|
+
inner = _function_domain_impl(base, var)
|
|
20
|
+
if exponent.is_Integer:
|
|
21
|
+
if exponent.is_negative:
|
|
22
|
+
return sp.And(inner, sp.Ne(base, 0))
|
|
23
|
+
return inner
|
|
24
|
+
if exponent.is_Rational:
|
|
25
|
+
p, q = int(exponent.p), int(exponent.q)
|
|
26
|
+
if q % 2 == 0:
|
|
27
|
+
# Even root: nonnegative base. A negative exponent also excludes 0.
|
|
28
|
+
rel = base > 0 if p < 0 else base >= 0
|
|
29
|
+
return sp.And(inner, rel)
|
|
30
|
+
# Odd root is real for all real bases, but negative powers exclude zero.
|
|
31
|
+
return sp.And(inner, sp.Ne(base, 0)) if p < 0 else inner
|
|
32
|
+
# Irrational real powers use the principal real branch only for positive base.
|
|
33
|
+
if exponent.is_real is True:
|
|
34
|
+
return sp.And(inner, base > 0)
|
|
35
|
+
return inner
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _function_domain_impl(expr, var, *, domain=S.Reals, stratified=False):
|
|
39
|
+
"""Largest supported real/complex domain condition for ``expr`` in ``var``.
|
|
40
|
+
|
|
41
|
+
funcprops owns transcendental structural rules. semialg is invoked only for
|
|
42
|
+
expressions that are structurally semialgebraic.
|
|
43
|
+
"""
|
|
44
|
+
expr = sp.sympify(expr)
|
|
45
|
+
var = sp.sympify(var)
|
|
46
|
+
real_mode = domain in (S.Reals, "real", "reals", "R", "RR")
|
|
47
|
+
complex_mode = domain in (S.Complexes, "complex", "complexes", "C", "CC")
|
|
48
|
+
if not (real_mode or complex_mode):
|
|
49
|
+
raise ValueError("domain must be Reals or Complexes")
|
|
50
|
+
if expr == var or not expr.has(var):
|
|
51
|
+
return S.true
|
|
52
|
+
|
|
53
|
+
if real_mode and isinstance(expr, sp.Piecewise):
|
|
54
|
+
clauses = []
|
|
55
|
+
covered = S.false
|
|
56
|
+
for value, cond in expr.args:
|
|
57
|
+
active = (
|
|
58
|
+
sp.And(cond, sp.Not(covered)) if cond is not S.true else sp.Not(covered)
|
|
59
|
+
)
|
|
60
|
+
clauses.append(
|
|
61
|
+
sp.And(active, _function_domain_impl(value, var, domain=S.Reals))
|
|
62
|
+
)
|
|
63
|
+
covered = sp.Or(covered, cond)
|
|
64
|
+
return sp.simplify(sp.Or(*clauses)) if clauses else S.false
|
|
65
|
+
|
|
66
|
+
if real_mode and expr.func is sp.Pow:
|
|
67
|
+
if is_semialgebraic_expression(expr, var):
|
|
68
|
+
try:
|
|
69
|
+
return sp.sympify(_semialg.function_domain(expr, var))
|
|
70
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
71
|
+
pass
|
|
72
|
+
# Symbolic exponents have genuinely parameter-dependent real domains
|
|
73
|
+
# (integer, negative-integer, and noninteger cases). The catalogue is
|
|
74
|
+
# substantially more informative than the conservative x>0 fallback.
|
|
75
|
+
if expr.exp.free_symbols:
|
|
76
|
+
reg = evaluate_property(expr, "RealDomain", var=var, mode="real")
|
|
77
|
+
if reg.primary is not None:
|
|
78
|
+
dom = semantic_condition(reg.primary)
|
|
79
|
+
# SymPy defines x**0 as 1 even at x=0; the catalogue-derived
|
|
80
|
+
# source treats 0^0 as indeterminate. SymPy semantics take
|
|
81
|
+
# precedence in funcprops.
|
|
82
|
+
return sp.Or(sp.Eq(expr.exp, 0), dom)
|
|
83
|
+
return simplify_condition(_pow_real_domain(expr, var))
|
|
84
|
+
|
|
85
|
+
# Ordinary arithmetic is defined wherever its varying operands are.
|
|
86
|
+
# This exact fallback keeps basic domain reasoning independent of semialg.
|
|
87
|
+
if real_mode and expr.func in (sp.Add, sp.Mul):
|
|
88
|
+
ds = []
|
|
89
|
+
for a in expr.args:
|
|
90
|
+
if a.has(var):
|
|
91
|
+
d = _function_domain_impl(a, var, domain=S.Reals)
|
|
92
|
+
if isinstance(d, PropertyResult):
|
|
93
|
+
return d
|
|
94
|
+
ds.append(d)
|
|
95
|
+
return simplify_condition(sp.And(*ds)) if ds else S.true
|
|
96
|
+
|
|
97
|
+
# Trigonometric reciprocal/pole domains are clearer and more useful in
|
|
98
|
+
# structural form than the equivalent integer-membership catalogue rule.
|
|
99
|
+
if (
|
|
100
|
+
real_mode
|
|
101
|
+
and expr.func in (sp.tan, sp.sec, sp.cot, sp.csc)
|
|
102
|
+
and len(expr.args) == 1
|
|
103
|
+
):
|
|
104
|
+
a = expr.args[0]
|
|
105
|
+
inner = (
|
|
106
|
+
_function_domain_impl(a, var, domain=S.Reals)
|
|
107
|
+
if a.has(var) and a != var
|
|
108
|
+
else S.true
|
|
109
|
+
)
|
|
110
|
+
denom = sp.cos(a) if expr.func in (sp.tan, sp.sec) else sp.sin(a)
|
|
111
|
+
return simplify_condition(sp.And(inner, sp.Ne(denom, 0)))
|
|
112
|
+
|
|
113
|
+
constraints = []
|
|
114
|
+
|
|
115
|
+
# semialg is an exact backend for semialgebraic expressions only.
|
|
116
|
+
if real_mode and is_semialgebraic_expression(expr, var):
|
|
117
|
+
try:
|
|
118
|
+
constraints.append(sp.sympify(_semialg.function_domain(expr, var)))
|
|
119
|
+
except _semialg.SemialgUnavailable:
|
|
120
|
+
pass
|
|
121
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
mode = "real" if real_mode else "complex"
|
|
125
|
+
prop = "RealDomain" if real_mode else "ComplexDomain"
|
|
126
|
+
reg = evaluate_property(expr, prop, var=var, mode=mode)
|
|
127
|
+
if reg.primary is not None:
|
|
128
|
+
constraints.append(sp.sympify(reg.primary))
|
|
129
|
+
elif reg.cases:
|
|
130
|
+
# Preserve parameter-dependent domains as a Boolean piecewise equivalent:
|
|
131
|
+
# OR(guard AND domain-value). Unknown default stays unasserted.
|
|
132
|
+
terms = []
|
|
133
|
+
for case in reg.cases:
|
|
134
|
+
if case.value in (S.true, S.false) or isinstance(
|
|
135
|
+
case.value, (sp.logic.boolalg.Boolean, sp.core.relational.Relational)
|
|
136
|
+
):
|
|
137
|
+
terms.append(sp.And(case.condition, case.value))
|
|
138
|
+
if terms:
|
|
139
|
+
constraints.append(sp.Or(*terms))
|
|
140
|
+
|
|
141
|
+
# Explicit elementary rules not represented as standalone SymPy heads in the registry.
|
|
142
|
+
if real_mode:
|
|
143
|
+
if expr.func is sp.exp:
|
|
144
|
+
constraints.append(S.true)
|
|
145
|
+
elif expr.func is sp.log and len(expr.args) == 1:
|
|
146
|
+
constraints.append(expr.args[0] > 0)
|
|
147
|
+
elif expr.func is sp.asin or expr.func is sp.acos:
|
|
148
|
+
a = expr.args[0]
|
|
149
|
+
constraints.append(sp.And(a >= -1, a <= 1))
|
|
150
|
+
elif expr.func is sp.acosh:
|
|
151
|
+
constraints.append(expr.args[0] >= 1)
|
|
152
|
+
elif expr.func is sp.atanh:
|
|
153
|
+
a = expr.args[0]
|
|
154
|
+
constraints.append(sp.And(a > -1, a < 1))
|
|
155
|
+
|
|
156
|
+
# Every varying argument must itself be defined.
|
|
157
|
+
for arg in getattr(expr, "args", ()):
|
|
158
|
+
if arg == var or not getattr(arg, "has", lambda *_: False)(var):
|
|
159
|
+
continue
|
|
160
|
+
d = _function_domain_impl(
|
|
161
|
+
arg, var, domain=S.Reals if real_mode else S.Complexes
|
|
162
|
+
)
|
|
163
|
+
if not isinstance(d, PropertyResult):
|
|
164
|
+
constraints.append(sp.sympify(d))
|
|
165
|
+
|
|
166
|
+
if not constraints:
|
|
167
|
+
return PropertyResult(primary="unknown_domain", source="funcprops")
|
|
168
|
+
try:
|
|
169
|
+
result = simplify_condition(sp.And(*constraints))
|
|
170
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
171
|
+
result = sp.And(*constraints)
|
|
172
|
+
if stratified and real_mode:
|
|
173
|
+
return stratify_boolean_by_parameters(
|
|
174
|
+
result, var, source="domain-parameter-stratification"
|
|
175
|
+
)
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def function_domain(expr, var, *, domain=S.Reals, stratified=False):
|
|
180
|
+
"""Cached public wrapper around the lazy real/complex domain prover."""
|
|
181
|
+
from ..proof_cache import property_proof_context
|
|
182
|
+
|
|
183
|
+
complex_mode = domain in (S.Complexes, "complex", "complexes", "C", "CC")
|
|
184
|
+
if not (complex_mode or domain in (S.Reals, "real", "reals", "R", "RR")):
|
|
185
|
+
raise ValueError("domain must be Reals or Complexes")
|
|
186
|
+
return property_proof_context(expr, var).natural_domain(
|
|
187
|
+
complex_domain=complex_mode, stratified=stratified
|
|
188
|
+
)
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sympy as sp
|
|
4
|
+
from sympy import S
|
|
5
|
+
|
|
6
|
+
from .. import _semialg
|
|
7
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
8
|
+
from ..results import ConditionalValue, Monotonicity, PropertyResult, TruthValue
|
|
9
|
+
from ..stratification import (
|
|
10
|
+
combine_truth_results,
|
|
11
|
+
exact_set_equal,
|
|
12
|
+
guarded_result,
|
|
13
|
+
normalize_cases,
|
|
14
|
+
)
|
|
15
|
+
from ..transcendental.periodic import positive_period
|
|
16
|
+
from .common import (
|
|
17
|
+
combine_natural_domain_condition,
|
|
18
|
+
condition_to_set,
|
|
19
|
+
is_semialgebraic_condition,
|
|
20
|
+
is_semialgebraic_expression,
|
|
21
|
+
normalize_variables,
|
|
22
|
+
)
|
|
23
|
+
from .domain import function_domain
|
|
24
|
+
from .monotonic_certificates import (
|
|
25
|
+
injectivity_from_monotonic_partition,
|
|
26
|
+
monotonic_partition,
|
|
27
|
+
)
|
|
28
|
+
from .monotonicity import function_monotonicity
|
|
29
|
+
from .range import function_range
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _combined_domain(expr, var, domain):
|
|
33
|
+
return combine_natural_domain_condition(function_domain(expr, var), domain, var)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _semialg_mapping_result(expr, var, domain, *, codomain=True):
|
|
37
|
+
"""Return semialg mapping metadata when the whole problem is semialgebraic."""
|
|
38
|
+
if not (
|
|
39
|
+
is_semialgebraic_expression(expr, var)
|
|
40
|
+
and is_semialgebraic_condition(domain, var)
|
|
41
|
+
):
|
|
42
|
+
return None
|
|
43
|
+
# semialg's mapping-properties query is currently nonparametric. Preserve
|
|
44
|
+
# funcprops' guarded parameter strata instead of quantifying/collapsing free
|
|
45
|
+
# coefficients as part of an ordinary mapping query.
|
|
46
|
+
problem_symbols = sp.sympify(expr).free_symbols | sp.sympify(domain).free_symbols
|
|
47
|
+
if problem_symbols - {var}:
|
|
48
|
+
return None
|
|
49
|
+
y = sp.Dummy("_semialg_y", real=True)
|
|
50
|
+
target = True
|
|
51
|
+
if codomain not in (S.Reals, True, None, S.true):
|
|
52
|
+
if isinstance(codomain, sp.Set):
|
|
53
|
+
try:
|
|
54
|
+
target = codomain.as_relational(y)
|
|
55
|
+
except (TypeError, ValueError, NotImplementedError):
|
|
56
|
+
return None
|
|
57
|
+
else:
|
|
58
|
+
target = sp.sympify(codomain).xreplace({var: y})
|
|
59
|
+
if not is_semialgebraic_condition(target, y):
|
|
60
|
+
return None
|
|
61
|
+
try:
|
|
62
|
+
return _semialg.function_mapping_properties(
|
|
63
|
+
expr, var, domain=domain, codomain=target, image_variable=y
|
|
64
|
+
)
|
|
65
|
+
except (_semialg.SemialgUnavailable, *SYMBOLIC_QUERY_ERRORS):
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _truth_from_backend(value):
|
|
70
|
+
return _semialg.normalize_truth(value)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _domain_at_most_one_point(dom, var):
|
|
74
|
+
ds = condition_to_set(dom, var)
|
|
75
|
+
if ds is S.EmptySet:
|
|
76
|
+
return True
|
|
77
|
+
if isinstance(ds, sp.FiniteSet):
|
|
78
|
+
return len(ds) <= 1
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _injectivity_from_monotonicity(m, dom, var):
|
|
83
|
+
if m in (Monotonicity.STRICTLY_INCREASING, Monotonicity.STRICTLY_DECREASING):
|
|
84
|
+
return TruthValue.TRUE
|
|
85
|
+
if m is Monotonicity.CONSTANT:
|
|
86
|
+
return (
|
|
87
|
+
TruthValue.TRUE if _domain_at_most_one_point(dom, var) else TruthValue.FALSE
|
|
88
|
+
)
|
|
89
|
+
return TruthValue.UNKNOWN
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _multivariate_affine_map(expr, variables):
|
|
93
|
+
"""Return (A,b) for an affine map, else None.
|
|
94
|
+
|
|
95
|
+
``expr`` may be scalar or a tuple/list/Matrix. Coefficients may contain
|
|
96
|
+
parameters but must not depend on the mapped variables.
|
|
97
|
+
"""
|
|
98
|
+
vv = tuple(variables)
|
|
99
|
+
if isinstance(expr, sp.MatrixBase):
|
|
100
|
+
outs = tuple(expr)
|
|
101
|
+
elif isinstance(expr, (tuple, list, sp.Tuple)):
|
|
102
|
+
outs = tuple(map(sp.sympify, expr))
|
|
103
|
+
else:
|
|
104
|
+
outs = (sp.sympify(expr),)
|
|
105
|
+
rows = []
|
|
106
|
+
offsets = []
|
|
107
|
+
for f in outs:
|
|
108
|
+
try:
|
|
109
|
+
row = [sp.simplify(sp.diff(f, v)) for v in vv]
|
|
110
|
+
if any(a.has(*vv) for a in row):
|
|
111
|
+
return None
|
|
112
|
+
b = sp.simplify(f - sum(a * v for a, v in zip(row, vv, strict=False)))
|
|
113
|
+
if b.has(*vv):
|
|
114
|
+
return None
|
|
115
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
116
|
+
return None
|
|
117
|
+
rows.append(row)
|
|
118
|
+
offsets.append(b)
|
|
119
|
+
return sp.Matrix(rows), sp.Matrix(offsets)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _full_rank_condition(A, rank):
|
|
123
|
+
"""Return the exact symbolic condition that ``A`` has at least ``rank``."""
|
|
124
|
+
from itertools import combinations
|
|
125
|
+
|
|
126
|
+
m, n = A.shape
|
|
127
|
+
if rank == 0:
|
|
128
|
+
return S.true
|
|
129
|
+
if rank > min(m, n):
|
|
130
|
+
return S.false
|
|
131
|
+
minors = []
|
|
132
|
+
for rows in combinations(range(m), rank):
|
|
133
|
+
for cols in combinations(range(n), rank):
|
|
134
|
+
det = sp.simplify(A.extract(rows, cols).det())
|
|
135
|
+
if det.is_zero is False:
|
|
136
|
+
return S.true
|
|
137
|
+
if det.is_zero is not True:
|
|
138
|
+
minors.append(sp.Ne(det, 0))
|
|
139
|
+
return sp.Or(*minors) if minors else S.false
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _rank_truth_result(A, required_rank, *, source):
|
|
143
|
+
condition = sp.simplify(_full_rank_condition(A, required_rank))
|
|
144
|
+
if condition is S.true:
|
|
145
|
+
return TruthValue.TRUE
|
|
146
|
+
if condition is S.false:
|
|
147
|
+
return TruthValue.FALSE
|
|
148
|
+
return normalize_cases(
|
|
149
|
+
(
|
|
150
|
+
ConditionalValue(TruthValue.TRUE, condition),
|
|
151
|
+
ConditionalValue(TruthValue.FALSE, sp.Not(condition)),
|
|
152
|
+
),
|
|
153
|
+
source=source,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _multivariate_mapping_truth(
|
|
158
|
+
expr, variables, *, mode, domain=True, codomain=S.Reals
|
|
159
|
+
):
|
|
160
|
+
# Exact support currently covers the natural full-real affine map. More
|
|
161
|
+
# complicated restricted multivariate domains must not be guessed.
|
|
162
|
+
if domain not in (True, None, S.true, S.Reals):
|
|
163
|
+
return TruthValue.UNKNOWN
|
|
164
|
+
aff = _multivariate_affine_map(expr, variables)
|
|
165
|
+
if aff is None:
|
|
166
|
+
return TruthValue.UNKNOWN
|
|
167
|
+
A, _ = aff
|
|
168
|
+
m, n = A.shape
|
|
169
|
+
if mode == "injective":
|
|
170
|
+
return _rank_truth_result(A, n, source="affine-rank-injectivity")
|
|
171
|
+
if mode == "surjective":
|
|
172
|
+
if codomain not in (S.Reals, True, None):
|
|
173
|
+
return TruthValue.UNKNOWN
|
|
174
|
+
return _rank_truth_result(A, m, source="affine-rank-surjectivity")
|
|
175
|
+
if mode == "bijective":
|
|
176
|
+
if m != n:
|
|
177
|
+
return TruthValue.FALSE
|
|
178
|
+
return _rank_truth_result(A, n, source="affine-rank-bijectivity")
|
|
179
|
+
return TruthValue.UNKNOWN
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _affine_output_transform(expr, var):
|
|
183
|
+
"""Return ``(scale, inner, offset)`` for a nontrivial affine output map."""
|
|
184
|
+
offset, dependent = expr.as_independent(var, as_Add=True)
|
|
185
|
+
scale, inner = dependent.as_independent(var, as_Add=False)
|
|
186
|
+
if inner == expr or (inner == dependent and scale == 1 and offset == 0):
|
|
187
|
+
return None
|
|
188
|
+
if scale.has(var) or offset.has(var) or not inner.has(var):
|
|
189
|
+
return None
|
|
190
|
+
return sp.simplify(scale), inner, sp.simplify(offset)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _injective_structural_closure(expr, var, domain, partition):
|
|
194
|
+
"""Apply exact injectivity-preserving composition and affine-output rules."""
|
|
195
|
+
transform = _affine_output_transform(expr, var)
|
|
196
|
+
if transform is not None:
|
|
197
|
+
scale, inner, _ = transform
|
|
198
|
+
if scale.is_nonzero is True:
|
|
199
|
+
return function_injective(inner, var, domain=domain, partition=partition)
|
|
200
|
+
if scale.is_zero is True:
|
|
201
|
+
return (
|
|
202
|
+
TruthValue.TRUE
|
|
203
|
+
if _domain_at_most_one_point(domain, var)
|
|
204
|
+
else TruthValue.FALSE
|
|
205
|
+
)
|
|
206
|
+
if scale.free_symbols:
|
|
207
|
+
nonzero = function_injective(inner, var, domain=domain, partition=partition)
|
|
208
|
+
zero = (
|
|
209
|
+
TruthValue.TRUE
|
|
210
|
+
if _domain_at_most_one_point(domain, var)
|
|
211
|
+
else TruthValue.FALSE
|
|
212
|
+
)
|
|
213
|
+
nz = guarded_result(
|
|
214
|
+
nonzero, sp.Ne(scale, 0), source="affine-output-injectivity"
|
|
215
|
+
)
|
|
216
|
+
z = guarded_result(
|
|
217
|
+
zero, sp.Eq(scale, 0), source="affine-output-injectivity"
|
|
218
|
+
)
|
|
219
|
+
return normalize_cases(
|
|
220
|
+
(*nz.cases, *z.cases), source="affine-output-injectivity"
|
|
221
|
+
)
|
|
222
|
+
# These outer real functions are strictly increasing on their natural real
|
|
223
|
+
# domains, so composition preserves and reflects injectivity of the inner map.
|
|
224
|
+
if (
|
|
225
|
+
expr.func in {sp.exp, sp.log, sp.sinh, sp.asinh, sp.atan}
|
|
226
|
+
and len(expr.args) == 1
|
|
227
|
+
):
|
|
228
|
+
inner = expr.args[0]
|
|
229
|
+
if inner != var and inner.has(var):
|
|
230
|
+
return function_injective(inner, var, domain=domain, partition=partition)
|
|
231
|
+
return TruthValue.UNKNOWN
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def function_injective(expr, var, *, domain=True, partition=None):
|
|
235
|
+
"""Classify injectivity on the requested domain using exact witnesses.
|
|
236
|
+
|
|
237
|
+
Scalar functions reuse monotonicity and monotonic partitions before trying
|
|
238
|
+
exact equation solving. Multivariate cases use only supported structural
|
|
239
|
+
criteria and otherwise return ``UNKNOWN``.
|
|
240
|
+
"""
|
|
241
|
+
vv = normalize_variables(var)
|
|
242
|
+
if len(vv) > 1:
|
|
243
|
+
return _multivariate_mapping_truth(expr, vv, mode="injective", domain=domain)
|
|
244
|
+
expr = sp.sympify(expr)
|
|
245
|
+
var = vv[0]
|
|
246
|
+
dom = _combined_domain(expr, var, domain)
|
|
247
|
+
backend = _semialg_mapping_result(expr, var, dom)
|
|
248
|
+
if backend is not None and backend.injective is not None:
|
|
249
|
+
return _truth_from_backend(backend.injective)
|
|
250
|
+
structural = _injective_structural_closure(expr, var, dom, partition)
|
|
251
|
+
if structural is not TruthValue.UNKNOWN:
|
|
252
|
+
return structural
|
|
253
|
+
m = function_monotonicity(expr, var, domain=dom, partition=partition)
|
|
254
|
+
if isinstance(m, PropertyResult):
|
|
255
|
+
cases = []
|
|
256
|
+
for c in m.cases:
|
|
257
|
+
v = _injectivity_from_monotonicity(c.value, dom, var)
|
|
258
|
+
if v is not TruthValue.UNKNOWN:
|
|
259
|
+
cases.append(ConditionalValue(v, c.condition))
|
|
260
|
+
if cases:
|
|
261
|
+
return normalize_cases(
|
|
262
|
+
cases, source="monotonicity-injectivity-stratification"
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
direct = _injectivity_from_monotonicity(m, dom, var)
|
|
266
|
+
if direct is not TruthValue.UNKNOWN:
|
|
267
|
+
return direct
|
|
268
|
+
|
|
269
|
+
# Reuse the exact monotonic partition instead of solving derivative zeros
|
|
270
|
+
# again. Same-direction strict cells cover stationary inflections; a
|
|
271
|
+
# certified direction reversal gives an exact noninjectivity proof.
|
|
272
|
+
part = (
|
|
273
|
+
partition
|
|
274
|
+
if partition is not None
|
|
275
|
+
and getattr(partition, "domain", None) == condition_to_set(dom, var)
|
|
276
|
+
else monotonic_partition(expr, var, domain=dom)
|
|
277
|
+
)
|
|
278
|
+
pv = injectivity_from_monotonic_partition(part)
|
|
279
|
+
if pv is not TruthValue.UNKNOWN:
|
|
280
|
+
return pv
|
|
281
|
+
if not expr.has(var):
|
|
282
|
+
return (
|
|
283
|
+
TruthValue.TRUE if _domain_at_most_one_point(dom, var) else TruthValue.FALSE
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Periodicity is an exact disproof only when the requested interval contains
|
|
287
|
+
# a pair x,x+p. Merely having width == p is insufficient for a half-open
|
|
288
|
+
# interval without additional continuity information.
|
|
289
|
+
p = positive_period(expr, var)
|
|
290
|
+
if p is not None:
|
|
291
|
+
ds = condition_to_set(dom, var)
|
|
292
|
+
if ds is S.Reals:
|
|
293
|
+
return TruthValue.FALSE
|
|
294
|
+
if isinstance(ds, sp.Interval) and ds.measure.is_number:
|
|
295
|
+
d = sp.simplify(ds.measure - p)
|
|
296
|
+
if d.is_positive is True:
|
|
297
|
+
return TruthValue.FALSE
|
|
298
|
+
if d.is_zero is True and not ds.left_open and not ds.right_open:
|
|
299
|
+
return TruthValue.FALSE
|
|
300
|
+
|
|
301
|
+
# Symmetry is a cheap exact disproof on symmetric domains, provided the
|
|
302
|
+
# domain contains some nonzero point.
|
|
303
|
+
ds = condition_to_set(dom, var)
|
|
304
|
+
symmetric = ds is S.Reals or (
|
|
305
|
+
isinstance(ds, sp.Interval) and sp.simplify(ds.start + ds.end) == 0
|
|
306
|
+
)
|
|
307
|
+
if symmetric:
|
|
308
|
+
try:
|
|
309
|
+
if sp.simplify(expr.xreplace({var: -var}) - expr) == 0 and expr.has(var):
|
|
310
|
+
if ds is S.Reals or (isinstance(ds, sp.Interval) and ds.measure != 0):
|
|
311
|
+
return TruthValue.FALSE
|
|
312
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
313
|
+
pass
|
|
314
|
+
return TruthValue.UNKNOWN
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _surjectivity_from_range(cond, y, target):
|
|
318
|
+
rs = condition_to_set(cond, y)
|
|
319
|
+
if rs is None:
|
|
320
|
+
return TruthValue.UNKNOWN
|
|
321
|
+
return exact_set_equal(rs, target)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def function_surjective(expr, var, *, domain=True, codomain=S.Reals):
|
|
325
|
+
vv = normalize_variables(var)
|
|
326
|
+
if len(vv) > 1:
|
|
327
|
+
return _multivariate_mapping_truth(
|
|
328
|
+
expr, vv, mode="surjective", domain=domain, codomain=codomain
|
|
329
|
+
)
|
|
330
|
+
expr = sp.sympify(expr)
|
|
331
|
+
var = vv[0]
|
|
332
|
+
dom = _combined_domain(expr, var, domain)
|
|
333
|
+
backend = _semialg_mapping_result(expr, var, dom, codomain=codomain)
|
|
334
|
+
if backend is not None and backend.surjective is not None:
|
|
335
|
+
return _truth_from_backend(backend.surjective)
|
|
336
|
+
y = sp.Dummy("_y", real=True)
|
|
337
|
+
target = codomain if isinstance(codomain, sp.Set) else condition_to_set(codomain, y)
|
|
338
|
+
if target is None:
|
|
339
|
+
return TruthValue.UNKNOWN
|
|
340
|
+
try:
|
|
341
|
+
r = function_range(expr, var, y, constraints=dom)
|
|
342
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
343
|
+
return TruthValue.UNKNOWN
|
|
344
|
+
if isinstance(r, PropertyResult):
|
|
345
|
+
return normalize_cases(
|
|
346
|
+
(
|
|
347
|
+
ConditionalValue(
|
|
348
|
+
_surjectivity_from_range(c.value, y, target), c.condition
|
|
349
|
+
)
|
|
350
|
+
for c in r.cases
|
|
351
|
+
),
|
|
352
|
+
primary=_surjectivity_from_range(r.primary, y, target)
|
|
353
|
+
if r.primary is not None
|
|
354
|
+
else None,
|
|
355
|
+
source="range-surjectivity-stratification",
|
|
356
|
+
)
|
|
357
|
+
return _surjectivity_from_range(r, y, target)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def function_bijective(expr, var, *, domain=True, codomain=S.Reals):
|
|
361
|
+
vv = normalize_variables(var)
|
|
362
|
+
if len(vv) > 1:
|
|
363
|
+
return _multivariate_mapping_truth(
|
|
364
|
+
expr, vv, mode="bijective", domain=domain, codomain=codomain
|
|
365
|
+
)
|
|
366
|
+
expr = sp.sympify(expr)
|
|
367
|
+
var = vv[0]
|
|
368
|
+
dom = _combined_domain(expr, var, domain)
|
|
369
|
+
backend = _semialg_mapping_result(expr, var, dom, codomain=codomain)
|
|
370
|
+
if backend is not None and backend.bijective is not None:
|
|
371
|
+
return _truth_from_backend(backend.bijective)
|
|
372
|
+
i = function_injective(expr, var, domain=dom)
|
|
373
|
+
s = function_surjective(expr, var, domain=dom, codomain=codomain)
|
|
374
|
+
return combine_truth_results(i, s, op="and", source="bijectivity-stratification")
|