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,698 @@
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
8
+ from ..exceptions import SYMBOLIC_QUERY_ERRORS
9
+ from ..results import ConditionalValue, Convexity, Monotonicity, PropertyResult, Sign
10
+ from ..stratification import (
11
+ evaluate_parameter_strata,
12
+ normalize_cases,
13
+ semantic_condition,
14
+ )
15
+ from ..transcendental.zero_geometry import property_interval_decomposition
16
+ from .advanced_convexity import (
17
+ ConvexityCertificate,
18
+ convexity_implications,
19
+ parameterized_exp_convexity,
20
+ parameterized_power_convexity,
21
+ secondary_convexity_properties,
22
+ special_function_convexity,
23
+ strong_convexity,
24
+ )
25
+ from .common import (
26
+ combine_natural_domain_condition,
27
+ condition_to_set,
28
+ normalize_variables,
29
+ )
30
+ from .domain import function_domain
31
+ from .monotonicity import function_monotonicity
32
+ from .piecewise import piecewise_branches, piecewise_interface_limits
33
+ from .sign import function_sign
34
+ from .special_functions import parameter_guard, positive_axis_convexity_strata
35
+
36
+
37
+ def _from_code(value):
38
+ try:
39
+ code = int(str(value))
40
+ except SYMBOLIC_QUERY_ERRORS:
41
+ return Convexity.UNKNOWN
42
+ return {
43
+ 1: Convexity.STRICTLY_CONVEX,
44
+ 2: Convexity.CONVEX,
45
+ -1: Convexity.STRICTLY_CONCAVE,
46
+ -2: Convexity.CONCAVE,
47
+ 0: Convexity.NEITHER,
48
+ }.get(code, Convexity.UNKNOWN)
49
+
50
+
51
+ def _registry_convexity(expr, var):
52
+ r = evaluate_property(expr, "GlobalConvexity", var=var, mode="real")
53
+ pguard = parameter_guard(expr, var, "GlobalConvexity")
54
+ if r.primary is not None:
55
+ v = _from_code(r.primary)
56
+ if pguard is S.true:
57
+ return v
58
+ return normalize_cases(
59
+ (ConditionalValue(v, pguard),), source="registry-convexity-stratification"
60
+ )
61
+ if r.cases:
62
+ return normalize_cases(
63
+ (
64
+ ConditionalValue(
65
+ _from_code(c.value), sp.And(semantic_condition(c.condition), pguard)
66
+ )
67
+ for c in r.cases
68
+ ),
69
+ source="registry-convexity-stratification",
70
+ )
71
+ return Convexity.UNKNOWN
72
+
73
+
74
+ def _normalize_semialg(value):
75
+ return _semialg.normalize_convexity(value)
76
+
77
+
78
+ def _sum_closure(expr, var, domain, strict):
79
+ classes = [
80
+ function_convexity(a, var, domain=domain, strict_inequalities=strict)
81
+ for a in expr.args
82
+ if a.has(var)
83
+ ]
84
+ if not classes:
85
+ return Convexity.AFFINE
86
+ cv = (Convexity.STRICTLY_CONVEX, Convexity.CONVEX, Convexity.AFFINE)
87
+ cc = (Convexity.STRICTLY_CONCAVE, Convexity.CONCAVE, Convexity.AFFINE)
88
+ if all(c in cv for c in classes):
89
+ return (
90
+ Convexity.STRICTLY_CONVEX
91
+ if any(c is Convexity.STRICTLY_CONVEX for c in classes)
92
+ else Convexity.CONVEX
93
+ )
94
+ if all(c in cc for c in classes):
95
+ return (
96
+ Convexity.STRICTLY_CONCAVE
97
+ if any(c is Convexity.STRICTLY_CONCAVE for c in classes)
98
+ else Convexity.CONCAVE
99
+ )
100
+ return Convexity.UNKNOWN
101
+
102
+
103
+ def _composition(expr, var, domain, strict):
104
+ varying = [(i, a) for i, a in enumerate(expr.args) if a.has(var)]
105
+ if len(varying) != 1 or expr.func in (sp.Add, sp.Mul, sp.Pow, sp.Piecewise):
106
+ return Convexity.UNKNOWN
107
+ i, inner = varying[0]
108
+ X = sp.Dummy("X", real=True)
109
+ args = list(expr.args)
110
+ args[i] = X
111
+ outer = expr.func(*args)
112
+ oc = _registry_convexity(outer, X)
113
+ if outer.func is sp.exp and oc is Convexity.UNKNOWN:
114
+ oc = Convexity.STRICTLY_CONVEX
115
+ om = function_monotonicity(outer, X)
116
+ ic = function_convexity(inner, var, domain=domain, strict_inequalities=strict)
117
+ inc = om in (Monotonicity.STRICTLY_INCREASING, Monotonicity.NONDECREASING)
118
+ dec = om in (Monotonicity.STRICTLY_DECREASING, Monotonicity.NONINCREASING)
119
+ if (
120
+ oc in (Convexity.STRICTLY_CONVEX, Convexity.CONVEX)
121
+ and inc
122
+ and ic in (Convexity.STRICTLY_CONVEX, Convexity.CONVEX, Convexity.AFFINE)
123
+ ):
124
+ return (
125
+ Convexity.STRICTLY_CONVEX
126
+ if oc is Convexity.STRICTLY_CONVEX or ic is Convexity.STRICTLY_CONVEX
127
+ else Convexity.CONVEX
128
+ )
129
+ if (
130
+ oc in (Convexity.STRICTLY_CONCAVE, Convexity.CONCAVE)
131
+ and inc
132
+ and ic in (Convexity.STRICTLY_CONCAVE, Convexity.CONCAVE, Convexity.AFFINE)
133
+ ):
134
+ return (
135
+ Convexity.STRICTLY_CONCAVE
136
+ if oc is Convexity.STRICTLY_CONCAVE or ic is Convexity.STRICTLY_CONCAVE
137
+ else Convexity.CONCAVE
138
+ )
139
+ if (
140
+ oc in (Convexity.STRICTLY_CONVEX, Convexity.CONVEX)
141
+ and dec
142
+ and ic in (Convexity.STRICTLY_CONCAVE, Convexity.CONCAVE)
143
+ ):
144
+ return Convexity.CONVEX
145
+ if (
146
+ oc in (Convexity.STRICTLY_CONCAVE, Convexity.CONCAVE)
147
+ and dec
148
+ and ic in (Convexity.STRICTLY_CONVEX, Convexity.CONVEX)
149
+ ):
150
+ return Convexity.CONCAVE
151
+ return Convexity.UNKNOWN
152
+
153
+
154
+ def _convexity_from_second_sign(v):
155
+ if v is Sign.POSITIVE:
156
+ return Convexity.STRICTLY_CONVEX
157
+ if v is Sign.NEGATIVE:
158
+ return Convexity.STRICTLY_CONCAVE
159
+ if v is Sign.NONNEGATIVE:
160
+ return Convexity.CONVEX
161
+ if v is Sign.NONPOSITIVE:
162
+ return Convexity.CONCAVE
163
+ if v is Sign.ZERO:
164
+ return Convexity.AFFINE
165
+ if v is Sign.SIGN_CHANGING:
166
+ return Convexity.NEITHER
167
+ return Convexity.UNKNOWN
168
+
169
+
170
+ def _flip_convexity(v):
171
+ return {
172
+ Convexity.STRONGLY_CONVEX: Convexity.STRONGLY_CONCAVE,
173
+ Convexity.STRICTLY_CONVEX: Convexity.STRICTLY_CONCAVE,
174
+ Convexity.CONVEX: Convexity.CONCAVE,
175
+ Convexity.AFFINE: Convexity.AFFINE,
176
+ Convexity.STRONGLY_CONCAVE: Convexity.STRONGLY_CONVEX,
177
+ Convexity.STRICTLY_CONCAVE: Convexity.STRICTLY_CONVEX,
178
+ Convexity.CONCAVE: Convexity.CONVEX,
179
+ }.get(v, Convexity.UNKNOWN)
180
+
181
+
182
+ def _multivariate_constant_hessian_convexity(expr, variables):
183
+ """Exact classification for expressions with a constant symmetric Hessian.
184
+
185
+ This intentionally handles only the zero-cost exact case. Variable
186
+ Hessians remain on the established scalar/semialgebraic paths rather than
187
+ triggering expensive multivariate inequality solving here.
188
+ """
189
+ vv = tuple(variables)
190
+ try:
191
+ H = sp.hessian(expr, vv)
192
+ except SYMBOLIC_QUERY_ERRORS:
193
+ return Convexity.UNKNOWN
194
+ if any(entry.has(*vv) for entry in H):
195
+ return Convexity.UNKNOWN
196
+ if sp.zeros(len(vv)) == H:
197
+ return Convexity.AFFINE
198
+ try:
199
+ eigs = H.eigenvals()
200
+ except SYMBOLIC_QUERY_ERRORS:
201
+ eigs = {}
202
+ if eigs:
203
+ vals = list(eigs)
204
+ if all(v.is_positive is True for v in vals):
205
+ return Convexity.STRONGLY_CONVEX
206
+ if all(v.is_negative is True for v in vals):
207
+ return Convexity.STRONGLY_CONCAVE
208
+ if all(v.is_nonnegative is True for v in vals):
209
+ return Convexity.CONVEX
210
+ if all(v.is_nonpositive is True for v in vals):
211
+ return Convexity.CONCAVE
212
+ if any(v.is_positive is True for v in vals) and any(
213
+ v.is_negative is True for v in vals
214
+ ):
215
+ return Convexity.NEITHER
216
+ # Matrix definiteness predicates are exact/tri-state; never use floating
217
+ # eigenvalue signs to turn an undecided symbolic proof into a certificate.
218
+ try:
219
+ if H.is_positive_definite is True:
220
+ return Convexity.STRONGLY_CONVEX
221
+ if H.is_negative_definite is True:
222
+ return Convexity.STRONGLY_CONCAVE
223
+ if H.is_positive_semidefinite is True:
224
+ return Convexity.CONVEX
225
+ if H.is_negative_semidefinite is True:
226
+ return Convexity.CONCAVE
227
+ except SYMBOLIC_QUERY_ERRORS:
228
+ pass
229
+ return Convexity.UNKNOWN
230
+
231
+
232
+ def _piecewise_convexity(expr, var, domain, strict):
233
+ """Combine convex branches after checking value and slope interfaces."""
234
+ if not isinstance(expr, sp.Piecewise):
235
+ return Convexity.UNKNOWN
236
+ branches = piecewise_branches(expr, var, domain)
237
+ if not branches:
238
+ return Convexity.UNKNOWN
239
+ classes = [
240
+ function_convexity(
241
+ branch.expression,
242
+ var,
243
+ domain=branch.condition,
244
+ strict_inequalities=strict,
245
+ )
246
+ for branch in branches
247
+ ]
248
+ if any(isinstance(value, PropertyResult) for value in classes):
249
+ return Convexity.UNKNOWN
250
+ convex = {
251
+ Convexity.STRONGLY_CONVEX,
252
+ Convexity.STRICTLY_CONVEX,
253
+ Convexity.CONVEX,
254
+ Convexity.AFFINE,
255
+ }
256
+ concave = {
257
+ Convexity.STRONGLY_CONCAVE,
258
+ Convexity.STRICTLY_CONCAVE,
259
+ Convexity.CONCAVE,
260
+ Convexity.AFFINE,
261
+ }
262
+ orientation = (
263
+ Convexity.CONVEX
264
+ if all(value in convex for value in classes)
265
+ else Convexity.CONCAVE
266
+ if all(value in concave for value in classes)
267
+ else Convexity.UNKNOWN
268
+ )
269
+ if orientation is Convexity.UNKNOWN:
270
+ return orientation
271
+ values = piecewise_interface_limits(branches, var)
272
+ slopes = piecewise_interface_limits(branches, var, derivative_order=1)
273
+ if values is None or slopes is None:
274
+ return orientation if len(branches) == 1 else Convexity.UNKNOWN
275
+ for (_, left_value, right_value), (_, left_slope, right_slope) in zip(
276
+ values, slopes, strict=False
277
+ ):
278
+ if sp.simplify(left_value - right_value) != 0:
279
+ return Convexity.NEITHER
280
+ jump = sp.simplify(right_slope - left_slope)
281
+ if orientation is Convexity.CONVEX:
282
+ if jump.is_nonnegative is not True:
283
+ return (
284
+ Convexity.NEITHER if jump.is_negative is True else Convexity.UNKNOWN
285
+ )
286
+ elif jump.is_nonpositive is not True:
287
+ return Convexity.NEITHER if jump.is_positive is True else Convexity.UNKNOWN
288
+ return orientation
289
+
290
+
291
+ def _convexity_property_result(
292
+ primary,
293
+ domain,
294
+ source,
295
+ *,
296
+ all_properties=(),
297
+ evidence=(),
298
+ ):
299
+ """Build one normalized detailed convexity result and proof certificate."""
300
+ proof = tuple(evidence)
301
+ if primary is not Convexity.UNKNOWN and not any(
302
+ isinstance(item, ConvexityCertificate) for item in proof
303
+ ):
304
+ proof = (
305
+ ConvexityCertificate(primary, domain, source, proof=proof),
306
+ *proof,
307
+ )
308
+ return PropertyResult(
309
+ primary,
310
+ all_properties=tuple(all_properties),
311
+ source=source,
312
+ evidence=proof,
313
+ )
314
+
315
+
316
+ def _semialgebraic_convexity_stage(
317
+ expr, var, assumptions, *, strict_inequalities, options
318
+ ):
319
+ """Return a decisive exact semialgebraic convexity class or ``None``."""
320
+ if not _semialg.eligible_problem(expr, var, domain=assumptions):
321
+ return None
322
+ try:
323
+ raw = _semialg.function_convexity(
324
+ expr,
325
+ var,
326
+ domain=assumptions,
327
+ strict_inequalities=strict_inequalities,
328
+ **options,
329
+ )
330
+ except SYMBOLIC_QUERY_ERRORS:
331
+ return None
332
+ value = _normalize_semialg(raw)
333
+ return value if value is not Convexity.UNKNOWN else None
334
+
335
+
336
+ def function_convexity(
337
+ expr,
338
+ var,
339
+ *,
340
+ domain=True,
341
+ strict_inequalities=True,
342
+ all_properties=False,
343
+ details=False,
344
+ intervals=False,
345
+ _domain_ready=False,
346
+ **kwargs,
347
+ ):
348
+ """Classify real convexity with strong/log/quasi/pseudo refinements.
349
+
350
+ The default is the strongest canonical ordinary convexity/concavity class.
351
+ With ``all_properties=True`` a PropertyResult includes additional proved
352
+ log-, quasi-, and pseudo-convexity/concavity properties.
353
+ """
354
+ expr = sp.sympify(expr)
355
+ if isinstance(var, (tuple, list, sp.Tuple)):
356
+ variables = normalize_variables(var)
357
+ if not any(expr.has(v) for v in variables):
358
+ primary = Convexity.AFFINE
359
+ else:
360
+ primary = _multivariate_constant_hessian_convexity(expr, variables)
361
+ if primary is not Convexity.UNKNOWN:
362
+ extra = convexity_implications(primary)
363
+ return (
364
+ _convexity_property_result(
365
+ primary,
366
+ S.Reals,
367
+ "constant-hessian",
368
+ all_properties=extra,
369
+ )
370
+ if (all_properties or details or intervals)
371
+ else primary
372
+ )
373
+ return (
374
+ _convexity_property_result(
375
+ Convexity.UNKNOWN, S.Reals, "multivariate-variable-hessian"
376
+ )
377
+ if (all_properties or details or intervals)
378
+ else Convexity.UNKNOWN
379
+ )
380
+ var = sp.sympify(var)
381
+ if not expr.has(var):
382
+ primary = Convexity.AFFINE
383
+ return (
384
+ _convexity_property_result(
385
+ primary,
386
+ S.Reals,
387
+ "constant",
388
+ all_properties=(Convexity.CONVEX, Convexity.CONCAVE),
389
+ )
390
+ if (all_properties or details)
391
+ else primary
392
+ )
393
+ natural = function_domain(expr, var)
394
+ assumptions = (
395
+ sp.sympify(domain)
396
+ if _domain_ready
397
+ else combine_natural_domain_condition(natural, domain, var)
398
+ )
399
+ if not _domain_ready:
400
+ split = evaluate_parameter_strata(
401
+ assumptions,
402
+ var,
403
+ lambda branch_domain: function_convexity(
404
+ expr,
405
+ var,
406
+ domain=branch_domain,
407
+ strict_inequalities=strict_inequalities,
408
+ all_properties=all_properties,
409
+ details=details,
410
+ intervals=intervals,
411
+ _domain_ready=True,
412
+ **kwargs,
413
+ ),
414
+ source="convexity-parameter-stratification",
415
+ )
416
+ if split is not None:
417
+ return split
418
+ ds_for_geometry = condition_to_set(assumptions, var)
419
+ requested_set = (
420
+ condition_to_set(domain, var)
421
+ if domain not in (True, None, S.true, S.Reals)
422
+ else None
423
+ )
424
+ # Global convexity/concavity is defined on a convex domain. Validate an
425
+ # explicit requested domain independently of natural-domain bookkeeping so
426
+ # recognized convex restrictions (for example x > 0 for gamma(x)) keep
427
+ # their stronger catalogue/special-function proofs.
428
+ if isinstance(requested_set, sp.Union) or isinstance(ds_for_geometry, sp.Union):
429
+ primary = Convexity.NEITHER
430
+ return (
431
+ _convexity_property_result(primary, assumptions, "nonconvex-domain")
432
+ if (all_properties or details or intervals)
433
+ else primary
434
+ )
435
+ if domain not in (True, None, S.true, S.Reals) and not isinstance(
436
+ requested_set, sp.Set
437
+ ):
438
+ primary = Convexity.UNKNOWN
439
+ return (
440
+ _convexity_property_result(
441
+ primary, assumptions, "unresolved-domain-convexity"
442
+ )
443
+ if (all_properties or details or intervals)
444
+ else primary
445
+ )
446
+ geom_domain = (
447
+ ds_for_geometry
448
+ if isinstance(ds_for_geometry, sp.Set)
449
+ else requested_set
450
+ if isinstance(requested_set, sp.Set)
451
+ else S.Reals
452
+ )
453
+ piecewise = _piecewise_convexity(expr, var, assumptions, strict_inequalities)
454
+ if piecewise is not Convexity.UNKNOWN:
455
+ if all_properties or details or intervals:
456
+ return _convexity_property_result(
457
+ piecewise,
458
+ assumptions,
459
+ "piecewise-analysis",
460
+ all_properties=convexity_implications(piecewise),
461
+ )
462
+ return piecewise
463
+ curvature_decomposition = (
464
+ property_interval_decomposition(
465
+ expr,
466
+ var,
467
+ derivative_order=2,
468
+ domain=geom_domain,
469
+ positive_value=Convexity.STRICTLY_CONVEX,
470
+ negative_value=Convexity.STRICTLY_CONCAVE,
471
+ )
472
+ if (intervals or details)
473
+ else None
474
+ )
475
+
476
+ # Certified second-derivative zero geometry can establish alternating
477
+ # convex/concave cells even when symbolic sign simplification cannot.
478
+ raw_curv = property_interval_decomposition(
479
+ expr,
480
+ var,
481
+ derivative_order=2,
482
+ domain=geom_domain,
483
+ positive_value=Convexity.STRICTLY_CONVEX,
484
+ negative_value=Convexity.STRICTLY_CONCAVE,
485
+ )
486
+ if (
487
+ raw_curv.complete
488
+ and raw_curv.alternating
489
+ and raw_curv.boundary_geometry
490
+ and geom_domain is S.Reals
491
+ ):
492
+ if intervals or details or all_properties:
493
+ return _convexity_property_result(
494
+ Convexity.NEITHER,
495
+ assumptions,
496
+ "second-derivative-zero-geometry",
497
+ evidence=(raw_curv,),
498
+ )
499
+ return Convexity.NEITHER
500
+
501
+ # Parameter strata are returned rather than collapsed to Unknown.
502
+ strat = parameterized_power_convexity(
503
+ expr, var, assumptions
504
+ ) or parameterized_exp_convexity(expr, var)
505
+ if strat is not None:
506
+ return strat
507
+ try:
508
+ positive_axis = (
509
+ sp.simplify_logic(sp.Implies(semantic_condition(assumptions), var > 0))
510
+ is S.true
511
+ )
512
+ except SYMBOLIC_QUERY_ERRORS:
513
+ positive_axis = False
514
+ if positive_axis:
515
+ sfacts = positive_axis_convexity_strata(expr, var)
516
+ if sfacts:
517
+ out = normalize_cases(
518
+ (ConditionalValue(v, g) for v, g, *_ in sfacts),
519
+ source="special-function-convexity-stratification",
520
+ )
521
+ if out.primary is None:
522
+ return out
523
+
524
+ registry_hint = Convexity.UNKNOWN
525
+ primary = Convexity.UNKNOWN
526
+ source = "insufficient-evidence"
527
+ evidence = ()
528
+
529
+ backend = _semialgebraic_convexity_stage(
530
+ expr,
531
+ var,
532
+ assumptions,
533
+ strict_inequalities=strict_inequalities,
534
+ options=kwargs,
535
+ )
536
+ if backend is not None:
537
+ primary = backend
538
+ source = "semialg"
539
+
540
+ if primary is Convexity.UNKNOWN and domain is True:
541
+ registry_hint = _registry_convexity(expr, var)
542
+ if isinstance(registry_hint, PropertyResult):
543
+ # Parameterized registry facts are already exact strata.
544
+ return registry_hint
545
+ if registry_hint not in (Convexity.UNKNOWN, Convexity.NEITHER):
546
+ primary = registry_hint
547
+ source = "registry"
548
+ elif expr.func is sp.exp:
549
+ primary = Convexity.STRICTLY_CONVEX
550
+ source = "primitive"
551
+
552
+ if primary is Convexity.UNKNOWN and expr.func is sp.Add:
553
+ c = _sum_closure(expr, var, assumptions, strict_inequalities)
554
+ if c is not Convexity.UNKNOWN:
555
+ primary = c
556
+ source = "sum-closure"
557
+ if primary is Convexity.UNKNOWN and expr.func is sp.Mul:
558
+ const, dep = expr.as_independent(var, as_Add=False)
559
+ if const != 1 and dep != expr:
560
+ c = function_convexity(
561
+ dep, var, domain=assumptions, strict_inequalities=strict_inequalities
562
+ )
563
+ sg = function_sign(const, var)
564
+ if isinstance(c, PropertyResult) or isinstance(sg, PropertyResult):
565
+ ccases = (
566
+ c.cases
567
+ if isinstance(c, PropertyResult)
568
+ else (ConditionalValue(c, S.true),)
569
+ )
570
+ scases = (
571
+ sg.cases
572
+ if isinstance(sg, PropertyResult)
573
+ else (ConditionalValue(sg, S.true),)
574
+ )
575
+ cases = []
576
+ for cv in ccases:
577
+ for sv in scases:
578
+ val = (
579
+ cv.value
580
+ if sv.value is Sign.POSITIVE
581
+ else _flip_convexity(cv.value)
582
+ if sv.value is Sign.NEGATIVE
583
+ else Convexity.AFFINE
584
+ if sv.value is Sign.ZERO
585
+ else Convexity.UNKNOWN
586
+ )
587
+ if val is not Convexity.UNKNOWN:
588
+ cases.append(
589
+ ConditionalValue(
590
+ val, sp.And(cv.condition, sv.condition)
591
+ )
592
+ )
593
+ if cases:
594
+ return normalize_cases(
595
+ cases, source="scalar-convexity-stratification"
596
+ )
597
+ else:
598
+ if sg is Sign.POSITIVE:
599
+ primary = c
600
+ source = "scalar-closure"
601
+ elif sg is Sign.NEGATIVE:
602
+ primary = _flip_convexity(c)
603
+ source = "scalar-closure"
604
+ elif sg is Sign.ZERO:
605
+ primary = Convexity.AFFINE
606
+ source = "scalar-closure"
607
+
608
+ if primary is Convexity.UNKNOWN:
609
+ c = _composition(expr, var, assumptions, strict_inequalities)
610
+ if c is not Convexity.UNKNOWN:
611
+ primary = c
612
+ source = "composition"
613
+
614
+ if primary is Convexity.UNKNOWN:
615
+ try:
616
+ from ..proof_cache import cached_derivative
617
+
618
+ d2 = cached_derivative(expr, var, order=2)
619
+ except SYMBOLIC_QUERY_ERRORS:
620
+ d2 = None
621
+ if d2 == 0:
622
+ primary = Convexity.AFFINE
623
+ source = "second-derivative"
624
+ elif d2 is not None:
625
+ sg = function_sign(d2, var, domain=assumptions, strict_inequalities=True)
626
+ if isinstance(sg, PropertyResult):
627
+ out = normalize_cases(
628
+ (
629
+ ConditionalValue(
630
+ _convexity_from_second_sign(c.value), c.condition
631
+ )
632
+ for c in sg.cases
633
+ if _convexity_from_second_sign(c.value) is not Convexity.UNKNOWN
634
+ ),
635
+ source="second-derivative-stratification",
636
+ )
637
+ if out.cases:
638
+ return out
639
+ primary = _convexity_from_second_sign(sg)
640
+ if primary is not Convexity.UNKNOWN:
641
+ source = "second-derivative"
642
+ if primary is Convexity.UNKNOWN and registry_hint is Convexity.NEITHER:
643
+ primary = Convexity.NEITHER
644
+ source = "registry"
645
+
646
+ # Upgrade an ordinary classification when a positive curvature bound is
647
+ # available. If an upstream backend already proved strong convexity, compute
648
+ # the local certificate only when the caller requested proof details.
649
+ ordinary = (
650
+ Convexity.STRICTLY_CONVEX,
651
+ Convexity.CONVEX,
652
+ Convexity.STRICTLY_CONCAVE,
653
+ Convexity.CONCAVE,
654
+ )
655
+ strong = (Convexity.STRONGLY_CONVEX, Convexity.STRONGLY_CONCAVE)
656
+ need_certificate = (all_properties or details or intervals) and primary in strong
657
+ if primary in ordinary or need_certificate:
658
+ strong_result = strong_convexity(expr, var, assumptions)
659
+ if strong_result is not None:
660
+ proved_class, certificate = strong_result
661
+ if primary in ordinary or proved_class is primary:
662
+ primary = proved_class
663
+ evidence = (certificate,)
664
+ source = "strong-convexity-bound"
665
+
666
+ specials = special_function_convexity(expr, var, assumptions)
667
+ if primary is Convexity.UNKNOWN:
668
+ for q in specials:
669
+ if q in (
670
+ Convexity.STRONGLY_CONVEX,
671
+ Convexity.STRICTLY_CONVEX,
672
+ Convexity.CONVEX,
673
+ Convexity.STRONGLY_CONCAVE,
674
+ Convexity.STRICTLY_CONCAVE,
675
+ Convexity.CONCAVE,
676
+ ):
677
+ primary = q
678
+ source = "special-function"
679
+ break
680
+
681
+ if all_properties or details or intervals:
682
+ extra, _ = secondary_convexity_properties(expr, var, assumptions, primary)
683
+ for q in specials:
684
+ if q is not primary and q not in extra:
685
+ extra = (*extra, q)
686
+ if (
687
+ curvature_decomposition is not None
688
+ and curvature_decomposition.boundary_geometry
689
+ ):
690
+ evidence = (*evidence, curvature_decomposition)
691
+ return _convexity_property_result(
692
+ primary,
693
+ assumptions,
694
+ source,
695
+ all_properties=extra,
696
+ evidence=evidence,
697
+ )
698
+ return primary