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,373 @@
1
+ """Lazy shared cache for reusable exact property proofs.
2
+
3
+ `PropertyProofContext` is the single internal entry point for proof primitives.
4
+ Every slot remains lazy: constructing a context does no mathematical work.
5
+ Recursive algorithms still call their uncached implementations where doing so
6
+ avoids cache-key overhead on transient subexpressions.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import operator
12
+ from dataclasses import dataclass
13
+ from functools import lru_cache
14
+
15
+ import sympy as sp
16
+ from sympy import S
17
+ from sympy.core.function import PoleError
18
+ from sympy.core.sympify import SympifyError
19
+
20
+
21
+ def _basic(value):
22
+ if value is True or value is None:
23
+ return S.true
24
+ if value is False:
25
+ return S.false
26
+ if isinstance(value, sp.Set):
27
+ return value
28
+ try:
29
+ return sp.sympify(value)
30
+ except (SympifyError, TypeError):
31
+ return value
32
+
33
+
34
+ def _normalize_relation(rel):
35
+ """Cheap deterministic relation normalization for cache keys only."""
36
+ if not isinstance(rel, sp.core.relational.Relational):
37
+ return rel
38
+ lhs, rhs = rel.lhs, rel.rhs
39
+ # Prefer a symbol/expression containing symbols on the left and zero or a
40
+ # simpler constant on the right. Do not call simplify/QE here.
41
+ if not lhs.free_symbols and rhs.free_symbols:
42
+ return rel.reversed
43
+ return rel
44
+
45
+
46
+ def normalize_assumptions(value):
47
+ """Canonicalize Boolean syntax without expensive logical reasoning."""
48
+ value = _basic(value)
49
+ if value in (S.true, S.false) or isinstance(value, sp.Set):
50
+ return value
51
+ if isinstance(value, (sp.And, sp.Or)):
52
+ head = value.func
53
+ flat = []
54
+ for arg in value.args:
55
+ q = normalize_assumptions(arg)
56
+ if isinstance(q, head):
57
+ flat.extend(q.args)
58
+ else:
59
+ flat.append(q)
60
+ ident = S.true if head is sp.And else S.false
61
+ annih = S.false if head is sp.And else S.true
62
+ if annih in flat:
63
+ return annih
64
+ flat = [q for q in flat if q != ident]
65
+ unique = {q: q for q in flat}
66
+ ordered = tuple(sorted(unique.values(), key=sp.default_sort_key))
67
+ if not ordered:
68
+ return ident
69
+ return head(*ordered, evaluate=False)
70
+ if isinstance(value, sp.Not):
71
+ q = normalize_assumptions(value.args[0])
72
+ return sp.Not(q, evaluate=False)
73
+ return _normalize_relation(value)
74
+
75
+
76
+ def normalize_domain(value):
77
+ if isinstance(value, sp.Set):
78
+ return value
79
+ return normalize_assumptions(value)
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class PropertyProofKey:
84
+ expression: sp.Basic
85
+ variable: sp.Basic
86
+ domain: object = S.true
87
+ assumptions: sp.Basic = S.true
88
+
89
+
90
+ def proof_key(expr, var, *, domain=S.true, assumptions=S.true):
91
+ return PropertyProofKey(
92
+ sp.sympify(expr),
93
+ sp.sympify(var),
94
+ normalize_domain(domain),
95
+ normalize_assumptions(assumptions),
96
+ )
97
+
98
+
99
+ @lru_cache(maxsize=2048)
100
+ def _derivative_cached(expr, var, order):
101
+ return sp.diff(expr, var, order)
102
+
103
+
104
+ @lru_cache(maxsize=1024)
105
+ def _factored_derivative_cached(expr, var, order):
106
+ return sp.factor(_derivative_cached(expr, var, order))
107
+
108
+
109
+ def _derivative_order(order):
110
+ """Return a concrete nonnegative derivative order without truncation."""
111
+ if isinstance(order, bool):
112
+ raise ValueError("order must be a nonnegative integer")
113
+ try:
114
+ value = operator.index(order)
115
+ except TypeError as exc:
116
+ raise ValueError("order must be a nonnegative integer") from exc
117
+ if value < 0:
118
+ raise ValueError("order must be a nonnegative integer")
119
+ return value
120
+
121
+
122
+ def _uncached_after_key_error(cache_fn, args):
123
+ """Retry an LRU-backed call only when its arguments are unhashable."""
124
+ try:
125
+ hash(args)
126
+ except TypeError:
127
+ return cache_fn.__wrapped__(*args)
128
+ raise
129
+
130
+
131
+ def cached_derivative(expr, var, *, order=1, factored=False):
132
+ expr = sp.sympify(expr)
133
+ var = sp.sympify(var)
134
+ order = _derivative_order(order)
135
+ fn = _factored_derivative_cached if factored else _derivative_cached
136
+ args = (expr, var, order)
137
+ try:
138
+ return fn(*args)
139
+ except TypeError:
140
+ return _uncached_after_key_error(fn, args)
141
+
142
+
143
+ @lru_cache(maxsize=1024)
144
+ def _domain_cached(expr, var, domain_kind, stratified):
145
+ from .real_analysis.domain import _function_domain_impl
146
+
147
+ dom = S.Reals if domain_kind == "real" else S.Complexes
148
+ return _function_domain_impl(expr, var, domain=dom, stratified=stratified)
149
+
150
+
151
+ def cached_domain(expr, var, *, domain=S.Reals, stratified=False):
152
+ if domain in (S.Reals, "real", "reals", "R", "RR"):
153
+ kind = "real"
154
+ elif domain in (S.Complexes, "complex", "complexes", "C", "CC"):
155
+ kind = "complex"
156
+ else:
157
+ raise ValueError("domain must be Reals or Complexes")
158
+ return _domain_cached(sp.sympify(expr), sp.sympify(var), kind, bool(stratified))
159
+
160
+
161
+ @lru_cache(maxsize=1024)
162
+ def _sign_cached(expr, var, domain, strict_inequalities):
163
+ from .real_analysis.sign import _function_sign_public_impl
164
+
165
+ return _function_sign_public_impl(
166
+ expr, var, domain=domain, strict_inequalities=strict_inequalities
167
+ )
168
+
169
+
170
+ def cached_sign(expr, var, *, domain=S.true, strict_inequalities=True):
171
+ key = proof_key(expr, var, domain=domain)
172
+ args = (key.expression, key.variable, key.domain, bool(strict_inequalities))
173
+ try:
174
+ return _sign_cached(*args)
175
+ except TypeError:
176
+ try:
177
+ hash(args)
178
+ except TypeError:
179
+ from .real_analysis.sign import _function_sign_public_impl
180
+
181
+ return _function_sign_public_impl(
182
+ key.expression,
183
+ key.variable,
184
+ domain=key.domain,
185
+ strict_inequalities=strict_inequalities,
186
+ )
187
+ raise
188
+
189
+
190
+ @lru_cache(maxsize=1024)
191
+ def _endpoint_limit_cached(expr, var, point, direction, attained):
192
+ try:
193
+ if point is S.NegativeInfinity:
194
+ return sp.limit(expr, var, -sp.oo)
195
+ if point is S.Infinity:
196
+ return sp.limit(expr, var, sp.oo)
197
+ if attained:
198
+ return sp.simplify(expr.subs(var, point))
199
+ return sp.limit(expr, var, point, dir=direction)
200
+ except (PoleError, TypeError, ValueError, NotImplementedError):
201
+ return None
202
+
203
+
204
+ def cached_endpoint_limit(expr, var, point, *, direction="+", attained=False):
205
+ expression = sp.sympify(expr)
206
+ variable = sp.sympify(var)
207
+ point = _basic(point)
208
+ args = (expression, variable, point, str(direction), bool(attained))
209
+ try:
210
+ return _endpoint_limit_cached(*args)
211
+ except TypeError:
212
+ return _uncached_after_key_error(_endpoint_limit_cached, args)
213
+
214
+
215
+ @lru_cache(maxsize=1024)
216
+ def _zero_geometry_cached(expr, var, derivative_order, domain):
217
+ from .transcendental.zero_geometry import _zero_geometry_impl
218
+
219
+ return _zero_geometry_impl(
220
+ expr, var, derivative_order=derivative_order, domain=domain
221
+ )
222
+
223
+
224
+ def cached_zero_geometry(
225
+ expr, var, *, derivative_order=0, domain=S.Reals, assumptions=S.true
226
+ ):
227
+ key = proof_key(expr, var, domain=domain, assumptions=assumptions)
228
+ order = _derivative_order(derivative_order)
229
+ args = (key.expression, key.variable, order, key.domain)
230
+ try:
231
+ return _zero_geometry_cached(*args)
232
+ except TypeError:
233
+ return _uncached_after_key_error(_zero_geometry_cached, args)
234
+
235
+
236
+ @lru_cache(maxsize=512)
237
+ def _singularity_cached(expr, var, domain, assumptions, ignore_unknown):
238
+ from .singularities import _function_singularity_analysis_impl
239
+
240
+ return _function_singularity_analysis_impl(
241
+ expr, var, domain=domain, assumptions=assumptions, ignore_unknown=ignore_unknown
242
+ )
243
+
244
+
245
+ def cached_singularity_analysis(
246
+ expr, var, *, domain=S.Complexes, assumptions=S.true, ignore_unknown=False
247
+ ):
248
+ key = proof_key(expr, var, domain=domain, assumptions=assumptions)
249
+ args = (
250
+ key.expression,
251
+ key.variable,
252
+ key.domain,
253
+ key.assumptions,
254
+ bool(ignore_unknown),
255
+ )
256
+ try:
257
+ return _singularity_cached(*args)
258
+ except TypeError:
259
+ return _uncached_after_key_error(_singularity_cached, args)
260
+
261
+
262
+ @lru_cache(maxsize=1024)
263
+ def _proof_context_cached(expr, var, domain, assumptions):
264
+ return PropertyProofContext(expr, var, domain, assumptions)
265
+
266
+
267
+ @dataclass(frozen=True)
268
+ class PropertyProofContext:
269
+ """Lazy normalized proof context shared by all global property APIs."""
270
+
271
+ expression: sp.Basic
272
+ variable: sp.Basic
273
+ domain: object = S.true
274
+ assumptions: sp.Basic = S.true
275
+
276
+ def derivative(self, order=1, *, factored=False):
277
+ return cached_derivative(
278
+ self.expression, self.variable, order=order, factored=factored
279
+ )
280
+
281
+ def natural_domain(self, *, complex_domain=False, stratified=False):
282
+ """Return the cached natural real or complex domain."""
283
+ target = S.Complexes if complex_domain else S.Reals
284
+ return cached_domain(
285
+ self.expression,
286
+ self.variable,
287
+ domain=target,
288
+ stratified=stratified,
289
+ )
290
+
291
+ def sign(self, *, domain=None, strict_inequalities=True):
292
+ return cached_sign(
293
+ self.expression,
294
+ self.variable,
295
+ domain=self.domain if domain is None else domain,
296
+ strict_inequalities=strict_inequalities,
297
+ )
298
+
299
+ def endpoint_limit(self, point, *, direction="+", attained=False):
300
+ return cached_endpoint_limit(
301
+ self.expression,
302
+ self.variable,
303
+ point,
304
+ direction=direction,
305
+ attained=attained,
306
+ )
307
+
308
+ def zero_geometry(self, *, derivative_order=0, domain=None):
309
+ return cached_zero_geometry(
310
+ self.expression,
311
+ self.variable,
312
+ derivative_order=derivative_order,
313
+ domain=self.domain if domain is None else domain,
314
+ assumptions=self.assumptions,
315
+ )
316
+
317
+ def singularities(self, *, domain=S.Complexes, ignore_unknown=False):
318
+ return cached_singularity_analysis(
319
+ self.expression,
320
+ self.variable,
321
+ domain=domain,
322
+ assumptions=self.assumptions,
323
+ ignore_unknown=ignore_unknown,
324
+ )
325
+
326
+ def monotonic_partition(self, *, domain=None):
327
+ from .real_analysis.monotonic_certificates import monotonic_partition
328
+
329
+ return monotonic_partition(
330
+ self.expression,
331
+ self.variable,
332
+ domain=self.domain if domain is None else domain,
333
+ )
334
+
335
+
336
+ def property_proof_context(expr, var, *, domain=S.true, assumptions=S.true):
337
+ key = proof_key(expr, var, domain=domain, assumptions=assumptions)
338
+ args = (key.expression, key.variable, key.domain, key.assumptions)
339
+ try:
340
+ return _proof_context_cached(*args)
341
+ except TypeError:
342
+ try:
343
+ hash(args)
344
+ except TypeError:
345
+ return PropertyProofContext(*args)
346
+ raise
347
+
348
+
349
+ def clear_property_proof_cache():
350
+ _derivative_cached.cache_clear()
351
+ _factored_derivative_cached.cache_clear()
352
+ _domain_cached.cache_clear()
353
+ _sign_cached.cache_clear()
354
+ _endpoint_limit_cached.cache_clear()
355
+ _zero_geometry_cached.cache_clear()
356
+ _singularity_cached.cache_clear()
357
+ _proof_context_cached.cache_clear()
358
+ from .real_analysis.monotonic_certificates import _monotonic_partition_on_set
359
+
360
+ _monotonic_partition_on_set.cache_clear()
361
+
362
+
363
+ def property_proof_cache_info():
364
+ return {
365
+ "derivative": _derivative_cached.cache_info(),
366
+ "factored_derivative": _factored_derivative_cached.cache_info(),
367
+ "domain": _domain_cached.cache_info(),
368
+ "sign": _sign_cached.cache_info(),
369
+ "endpoint_limit": _endpoint_limit_cached.cache_info(),
370
+ "zero_geometry": _zero_geometry_cached.cache_info(),
371
+ "singularity": _singularity_cached.cache_info(),
372
+ "context": _proof_context_cached.cache_info(),
373
+ }
@@ -0,0 +1,52 @@
1
+ from .convexity import function_convexity
2
+ from .domain import function_domain
3
+ from .monotonicity import function_monotonicity
4
+ from .range import function_range
5
+ from .sign import function_sign
6
+
7
+ __all__ = [
8
+ "InverseSolutionCertificate",
9
+ "OscillationCertificate",
10
+ "PartitionRange",
11
+ "SmoothnessResult",
12
+ "certified_monotonic_inverse",
13
+ "clear_monotonic_partition_cache",
14
+ "function_bijective",
15
+ "function_boundedness",
16
+ "function_convexity",
17
+ "function_domain",
18
+ "function_injective",
19
+ "function_monotonicity",
20
+ "function_oscillation",
21
+ "function_range",
22
+ "function_sign",
23
+ "function_smoothness",
24
+ "function_surjective",
25
+ "function_zeros",
26
+ "injectivity_from_monotonic_partition",
27
+ "range_from_monotonic_partition",
28
+ "solve_monotonic_inverse",
29
+ ]
30
+
31
+ from .boundedness import function_boundedness
32
+ from .mapping import function_bijective, function_injective, function_surjective
33
+ from .monotonic_certificates import (
34
+ IndexedMonotonicPartition,
35
+ InverseSolutionCertificate,
36
+ MonotonicDecomposition,
37
+ MonotonicInterval,
38
+ MonotonicPartition,
39
+ NonMonotonicityWitness,
40
+ PartitionRange,
41
+ certified_monotonic_inverse,
42
+ clear_monotonic_partition_cache,
43
+ injectivity_from_monotonic_partition,
44
+ monotonic_decomposition,
45
+ monotonic_partition,
46
+ quick_nonmonotonicity_witness,
47
+ range_from_monotonic_partition,
48
+ solve_monotonic_inverse,
49
+ )
50
+ from .oscillation import OscillationCertificate, function_oscillation
51
+ from .smoothness import SmoothnessResult, function_smoothness
52
+ from .zeros import function_zeros
@@ -0,0 +1,217 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import sympy as sp
6
+ from sympy import S
7
+
8
+ from ..exceptions import SYMBOLIC_QUERY_ERRORS
9
+ from ..results import ConditionalValue, Convexity, Monotonicity, Sign
10
+ from ..stratification import implication_guard, normalize_cases
11
+ from .common import condition_to_set
12
+ from .monotonicity import function_monotonicity
13
+ from .sign import function_sign
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ConvexityCertificate:
18
+ """Replayable evidence for one ordinary convexity classification."""
19
+
20
+ classification: Convexity
21
+ domain: object
22
+ method: str
23
+ modulus: sp.Expr | None = None
24
+ proof: tuple = ()
25
+
26
+
27
+ def parameterized_power_convexity(expr, var, domain):
28
+ """Exact parameter strata for x**a on a positive real domain."""
29
+ if expr.func is not sp.Pow or expr.base != var:
30
+ return None
31
+ a = expr.exp
32
+ if not (a.free_symbols - {var}) and a.is_number:
33
+ return None
34
+ ds = condition_to_set(domain, var)
35
+ # This stratification is valid only where x>0.
36
+ positive_domain = False
37
+ if isinstance(ds, sp.Interval):
38
+ positive_domain = (ds.start.is_nonnegative is True and ds.start != 0) or (
39
+ ds.start == 0 and ds.left_open
40
+ )
41
+ if not positive_domain:
42
+ # Rich parameter domains can contain Contains/NotContains predicates,
43
+ # which SymPy's LRA-backed ask() does not accept. Use the package's
44
+ # three-valued implication reducer instead of passing arbitrary domain
45
+ # formulae into ask().
46
+ positive_domain = implication_guard(domain, var > 0, var) is S.true
47
+ if not positive_domain and domain != (var > 0):
48
+ return None
49
+ cases = (
50
+ ConditionalValue(Convexity.STRICTLY_CONVEX, sp.Or(a < 0, a > 1)),
51
+ ConditionalValue(Convexity.STRICTLY_CONCAVE, sp.And(a > 0, a < 1)),
52
+ ConditionalValue(Convexity.AFFINE, sp.Or(sp.Eq(a, 0), sp.Eq(a, 1))),
53
+ )
54
+ return normalize_cases(cases, source="power-parameter-stratification")
55
+
56
+
57
+ def parameterized_exp_convexity(expr, var):
58
+ if expr.func is not sp.exp:
59
+ return None
60
+ g = sp.expand(expr.args[0])
61
+ try:
62
+ p = sp.Poly(g, var)
63
+ except SYMBOLIC_QUERY_ERRORS:
64
+ return None
65
+ if p.degree() != 1:
66
+ return None
67
+ a = p.coeff_monomial(var)
68
+ if not a.free_symbols:
69
+ return None
70
+ return normalize_cases(
71
+ (
72
+ ConditionalValue(Convexity.STRICTLY_CONVEX, sp.Ne(a, 0)),
73
+ ConditionalValue(Convexity.AFFINE, sp.Eq(a, 0)),
74
+ ),
75
+ source="exp-affine-parameter-stratification",
76
+ )
77
+
78
+
79
+ def strong_convexity(expr, var, domain):
80
+ """Prove strong convexity/concavity by an exact second-derivative bound."""
81
+ try:
82
+ from ..proof_cache import cached_derivative
83
+
84
+ d2 = sp.simplify(cached_derivative(expr, var, order=2))
85
+ except SYMBOLIC_QUERY_ERRORS:
86
+ return None
87
+ if d2 == 0:
88
+ return None
89
+ ds = domain if isinstance(domain, sp.Set) else condition_to_set(domain, var)
90
+ if not isinstance(ds, sp.Interval):
91
+ return None
92
+ # Constant Hessian is the clean exact case.
93
+ if not d2.has(var):
94
+ if d2.is_positive is True:
95
+ return Convexity.STRONGLY_CONVEX, ConvexityCertificate(
96
+ Convexity.STRONGLY_CONVEX, ds, "second-derivative-bound", d2, (d2,)
97
+ )
98
+ if d2.is_negative is True:
99
+ return Convexity.STRONGLY_CONCAVE, ConvexityCertificate(
100
+ Convexity.STRONGLY_CONCAVE, ds, "second-derivative-bound", -d2, (d2,)
101
+ )
102
+ # Exact infimum/supremum on an interval. Avoid numerical optimization.
103
+ try:
104
+ from sympy.calculus.util import maximum, minimum
105
+
106
+ lo = minimum(d2, var, ds)
107
+ if lo is not None and lo is not -sp.oo and sp.ask(sp.Q.positive(lo)) is True:
108
+ return Convexity.STRONGLY_CONVEX, ConvexityCertificate(
109
+ Convexity.STRONGLY_CONVEX,
110
+ ds,
111
+ "second-derivative-bound",
112
+ sp.simplify(lo),
113
+ (d2, lo),
114
+ )
115
+ hi = maximum(d2, var, ds)
116
+ if hi is not None and hi is not sp.oo and sp.ask(sp.Q.negative(hi)) is True:
117
+ return Convexity.STRONGLY_CONCAVE, ConvexityCertificate(
118
+ Convexity.STRONGLY_CONCAVE,
119
+ ds,
120
+ "second-derivative-bound",
121
+ sp.simplify(-hi),
122
+ (d2, hi),
123
+ )
124
+ except SYMBOLIC_QUERY_ERRORS:
125
+ pass
126
+ return None
127
+
128
+
129
+ def convexity_implications(primary):
130
+ """Return ordinary convexity/concavity properties implied by ``primary``.
131
+
132
+ The ordering lives in one place so default strongest-class results and
133
+ ``all_properties=True`` cannot drift apart across scalar and multivariate
134
+ code paths.
135
+ """
136
+ table = {
137
+ Convexity.STRONGLY_CONVEX: (Convexity.STRICTLY_CONVEX, Convexity.CONVEX),
138
+ Convexity.STRICTLY_CONVEX: (Convexity.CONVEX,),
139
+ Convexity.CONVEX: (),
140
+ Convexity.STRONGLY_CONCAVE: (Convexity.STRICTLY_CONCAVE, Convexity.CONCAVE),
141
+ Convexity.STRICTLY_CONCAVE: (Convexity.CONCAVE,),
142
+ Convexity.CONCAVE: (),
143
+ Convexity.AFFINE: (Convexity.CONVEX, Convexity.CONCAVE),
144
+ }
145
+ return table.get(primary, ())
146
+
147
+
148
+ def secondary_convexity_properties(expr, var, domain, primary):
149
+ props = list(convexity_implications(primary))
150
+ evidence = []
151
+ # Positive f: log-convexity/concavity reduces exactly to convexity of log(f).
152
+ s = function_sign(expr, var, domain=domain)
153
+ if s is Sign.POSITIVE:
154
+ try:
155
+ from ..proof_cache import cached_derivative
156
+
157
+ d2log = sp.simplify(cached_derivative(sp.log(expr), var, order=2))
158
+ ls = function_sign(d2log, var, domain=domain)
159
+ if ls in (Sign.POSITIVE, Sign.NONNEGATIVE, Sign.ZERO):
160
+ props.append(Convexity.LOG_CONVEX)
161
+ if ls in (Sign.NEGATIVE, Sign.NONPOSITIVE, Sign.ZERO):
162
+ props.append(Convexity.LOG_CONCAVE)
163
+ except SYMBOLIC_QUERY_ERRORS:
164
+ pass
165
+ # Every monotone univariate function is both quasiconvex and quasiconcave.
166
+ m = function_monotonicity(expr, var, domain=domain)
167
+ if m in (
168
+ Monotonicity.STRICTLY_INCREASING,
169
+ Monotonicity.NONDECREASING,
170
+ Monotonicity.STRICTLY_DECREASING,
171
+ Monotonicity.NONINCREASING,
172
+ Monotonicity.CONSTANT,
173
+ ):
174
+ props.extend((Convexity.QUASICONVEX, Convexity.QUASICONCAVE))
175
+ # A differentiable function with derivative of one strict sign is both
176
+ # pseudoconvex and pseudoconcave on an interval.
177
+ try:
178
+ ds = condition_to_set(domain, var)
179
+ if isinstance(ds, sp.Interval):
180
+ from ..proof_cache import cached_derivative
181
+
182
+ sg = function_sign(
183
+ cached_derivative(expr, var, order=1), var, domain=domain
184
+ )
185
+ if sg in (Sign.POSITIVE, Sign.NEGATIVE):
186
+ props.extend((Convexity.PSEUDOCONVEX, Convexity.PSEUDOCONCAVE))
187
+ except SYMBOLIC_QUERY_ERRORS:
188
+ pass
189
+ # deterministic order / dedupe
190
+ out = []
191
+ for p in props:
192
+ if p not in out and p is not primary:
193
+ out.append(p)
194
+ return tuple(out), tuple(evidence)
195
+
196
+
197
+ def _positive_domain(domain, var):
198
+ ds = condition_to_set(domain, var)
199
+ if isinstance(ds, sp.Interval):
200
+ return ds.start.is_nonnegative is True and (ds.start != 0 or ds.left_open)
201
+ rel = var > 0
202
+ return domain == rel or (isinstance(domain, sp.And) and rel in domain.args)
203
+
204
+
205
+ def special_function_convexity(expr, var, domain):
206
+ condition_to_set(domain, var)
207
+ # Gamma is positive and strictly log-convex on (0, infinity); positivity
208
+ # plus strict log-convexity also implies strict ordinary convexity.
209
+ if expr.func is sp.gamma and expr.args == (var,) and _positive_domain(domain, var):
210
+ return (Convexity.STRICTLY_CONVEX, Convexity.CONVEX, Convexity.LOG_CONVEX)
211
+ # Modified Bessel K_nu(x): for real fixed order, catalogue facts already
212
+ # give decreasing/convex; expose that as a strong special-function hint.
213
+ if expr.func is sp.besselk and len(expr.args) == 2 and expr.args[1] == var:
214
+ nu = expr.args[0]
215
+ if nu.is_real is True and _positive_domain(domain, var):
216
+ return (Convexity.STRICTLY_CONVEX, Convexity.CONVEX, Convexity.LOG_CONVEX)
217
+ return ()