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