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,515 @@
1
+ """DLMF-audited real-property facts for parameterized special functions.
2
+
3
+ The rules here are intentionally conservative. Each fact has provenance:
4
+ ``direct`` means DLMF states the property under essentially the same
5
+ hypotheses; ``derived`` means funcprops combines DLMF identities, endpoint
6
+ limits, or elementary differentiation without weakening the hypotheses.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ import sympy as sp
14
+ from sympy import S
15
+
16
+ from ..exceptions import SYMBOLIC_QUERY_ERRORS
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class DLMFFact:
21
+ family: str
22
+ property: str
23
+ section: str
24
+ url: str
25
+ status: str = "direct" # direct | derived
26
+ note: str = ""
27
+
28
+
29
+ DLMF_FACTS = (
30
+ DLMFFact(
31
+ "besseli",
32
+ "positive/increasing",
33
+ "10.37",
34
+ "https://dlmf.nist.gov/10.37",
35
+ note="fixed nu>=0, x>0",
36
+ ),
37
+ DLMFFact(
38
+ "besselk",
39
+ "positive/decreasing",
40
+ "10.37",
41
+ "https://dlmf.nist.gov/10.37",
42
+ note="fixed nu>=0, x>0",
43
+ ),
44
+ DLMFFact(
45
+ "besselk",
46
+ "order symmetry",
47
+ "10.27.3",
48
+ "https://dlmf.nist.gov/10.27.E3",
49
+ note="K_-nu(z)=K_nu(z)",
50
+ ),
51
+ DLMFFact(
52
+ "lowergamma",
53
+ "integral/domain",
54
+ "8.2.1",
55
+ "https://dlmf.nist.gov/8.2.E1",
56
+ note="Re(a)>0",
57
+ ),
58
+ DLMFFact("uppergamma", "integral/domain", "8.2.2", "https://dlmf.nist.gov/8.2.E2"),
59
+ DLMFFact(
60
+ "beta",
61
+ "Euler integral/domain",
62
+ "5.12.1",
63
+ "https://dlmf.nist.gov/5.12.E1",
64
+ note="Re(a)>0, Re(b)>0",
65
+ ),
66
+ DLMFFact(
67
+ "betainc",
68
+ "integral/domain/range",
69
+ "8.17.1",
70
+ "https://dlmf.nist.gov/8.17.E1",
71
+ note="a>0, b>0, 0<=x<=1",
72
+ ),
73
+ DLMFFact(
74
+ "betainc_regularized",
75
+ "normalization/domain/range",
76
+ "8.17.2",
77
+ "https://dlmf.nist.gov/8.17.E2",
78
+ note="a>0, b>0, 0<=x<=1",
79
+ ),
80
+ DLMFFact("gamma", "meromorphic/no zeros/poles", "5.2", "https://dlmf.nist.gov/5.2"),
81
+ DLMFFact("digamma", "meromorphic/poles", "5.2.2", "https://dlmf.nist.gov/5.2.E2"),
82
+ DLMFFact(
83
+ "hyper",
84
+ "series/termination/principal branch",
85
+ "16.2.1",
86
+ "https://dlmf.nist.gov/16.2.E1",
87
+ note="p<=q entire; p=q+1 principal branch cut [1,oo) unless terminating",
88
+ ),
89
+ DLMFFact(
90
+ "assoc_legendre",
91
+ "Ferrers zero distribution",
92
+ "14.16",
93
+ "https://dlmf.nist.gov/14.16",
94
+ note="integer-degree/order specialization gives n-|m| interior zeros on (-1,1)",
95
+ ),
96
+ DLMFFact(
97
+ "assoc_legendre",
98
+ "principal complex branch",
99
+ "14.21",
100
+ "https://dlmf.nist.gov/14.21",
101
+ note="principal branches use the standard cut to 1, with polynomial/degenerate exceptions handled separately",
102
+ ),
103
+ DLMFFact(
104
+ "elliptic_f",
105
+ "incomplete integral branch radicand",
106
+ "19.2.4",
107
+ "https://dlmf.nist.gov/19.2.E4",
108
+ note="principal sqrt of 1-k^2 sin^2(theta)",
109
+ ),
110
+ DLMFFact(
111
+ "elliptic_e",
112
+ "incomplete integral branch radicand",
113
+ "19.2.5",
114
+ "https://dlmf.nist.gov/19.2.E5",
115
+ note="principal sqrt of 1-k^2 sin^2(theta)",
116
+ ),
117
+ DLMFFact(
118
+ "elliptic_pi",
119
+ "incomplete third-kind path singularity",
120
+ "19.2.7",
121
+ "https://dlmf.nist.gov/19.2.E7",
122
+ note="parameter singularity requires path-aware treatment; endpoint-only tests are insufficient",
123
+ ),
124
+ DLMFFact(
125
+ "jacobi",
126
+ "zeros/interlacing",
127
+ "18.16.1",
128
+ "https://dlmf.nist.gov/18.16.E1",
129
+ note="n simple zeros in (-1,1) for alpha,beta>-1; consecutive degrees interlace by 18.2(vi)",
130
+ ),
131
+ DLMFFact(
132
+ "assoc_laguerre",
133
+ "zeros/interlacing",
134
+ "18.16.9",
135
+ "https://dlmf.nist.gov/18.16.E9",
136
+ note="n simple positive zeros for alpha>-1",
137
+ ),
138
+ DLMFFact(
139
+ "chebyshevt",
140
+ "trigonometric representation/zeros",
141
+ "18.5.1",
142
+ "https://dlmf.nist.gov/18.5.E1",
143
+ ),
144
+ DLMFFact(
145
+ "chebyshevu",
146
+ "trigonometric representation/zeros",
147
+ "18.5.2",
148
+ "https://dlmf.nist.gov/18.5.E2",
149
+ ),
150
+ DLMFFact(
151
+ "jn",
152
+ "positive zeros",
153
+ "10.58.1",
154
+ "https://dlmf.nist.gov/10.58.E1",
155
+ note="spherical Bessel zeros inherit ordinary Bessel zero structure",
156
+ ),
157
+ DLMFFact(
158
+ "yn",
159
+ "positive zeros",
160
+ "10.58.1",
161
+ "https://dlmf.nist.gov/10.58.E1",
162
+ note="spherical Bessel zeros inherit ordinary Bessel zero structure",
163
+ ),
164
+ DLMFFact(
165
+ "multigamma",
166
+ "domain/product",
167
+ "35.3.1,35.3.4",
168
+ "https://dlmf.nist.gov/35.3.E1",
169
+ note="Re(a)>(p-1)/2; product formula 35.3.E4",
170
+ ),
171
+ )
172
+
173
+
174
+ def dlmf_facts(family: str | None = None):
175
+ """Return executable and formula-audit DLMF facts.
176
+
177
+ The generated audit contributes one provenance record per exact formula or
178
+ narrowly scoped DLMF section. Helper/wrapper families are marked
179
+ ``derived`` rather than being misrepresented as DLMF-native functions.
180
+ """
181
+ facts = list(DLMF_FACTS)
182
+ try:
183
+ import json
184
+ from importlib.resources import files
185
+
186
+ data = json.loads(
187
+ files("funcprops")
188
+ .joinpath("data/dlmf_audit.json")
189
+ .read_text(encoding="utf-8")
190
+ )
191
+ seen = {(f.family, f.section) for f in facts}
192
+ for row in data.get("families", ()):
193
+ if row.get("status") not in {
194
+ "formula-verified-core",
195
+ "formula-verified-derived",
196
+ }:
197
+ continue
198
+ status = (
199
+ "derived" if row.get("verification_kind") == "derived" else "direct"
200
+ )
201
+ for ref in row.get("formula_refs", ()):
202
+ key = (row["family_id"], ref)
203
+ if key in seen:
204
+ continue
205
+ facts.append(
206
+ DLMFFact(
207
+ row["family_id"],
208
+ row.get("formula_verified_properties", "formula audit"),
209
+ ref,
210
+ f"https://dlmf.nist.gov/{ref}",
211
+ status=status,
212
+ note="formula-level catalogue audit",
213
+ )
214
+ )
215
+ seen.add(key)
216
+ except SYMBOLIC_QUERY_ERRORS:
217
+ pass
218
+ return tuple(f for f in facts if family is None or f.family == family)
219
+
220
+
221
+ def _real_guard(a):
222
+ return S.true if a.is_real is True else sp.Contains(a, S.Reals)
223
+
224
+
225
+ def _positive_axis(domain, var):
226
+ try:
227
+ from .common import condition_to_set
228
+
229
+ ds = condition_to_set(domain, var)
230
+ if (
231
+ isinstance(ds, sp.Interval)
232
+ and ds.start == 0
233
+ and ds.left_open
234
+ and ds.end is S.Infinity
235
+ ):
236
+ return True
237
+ except SYMBOLIC_QUERY_ERRORS:
238
+ pass
239
+ try:
240
+ return sp.simplify_logic(sp.Implies(domain, var > 0)) is S.true
241
+ except SYMBOLIC_QUERY_ERRORS:
242
+ return False
243
+
244
+
245
+ def parameter_guard(expr, var, prop):
246
+ expr = sp.sympify(expr)
247
+ var = sp.sympify(var)
248
+ if expr.func is sp.besselk and len(expr.args) == 2 and expr.args[1] == var:
249
+ nu = expr.args[0]
250
+ if prop in {
251
+ "Positive",
252
+ "NonNegative",
253
+ "Range",
254
+ "GlobalMonotonicity",
255
+ "GlobalConvexity",
256
+ }:
257
+ # 10.37 gives nu>=0; 10.27.3 extends K to all real order.
258
+ return _real_guard(nu)
259
+ if expr.func is sp.besseli and len(expr.args) == 2 and expr.args[1] == var:
260
+ nu = expr.args[0]
261
+ if prop in {"Positive", "NonNegative", "Range", "GlobalMonotonicity"}:
262
+ return sp.And(_real_guard(nu), nu >= 0)
263
+ return S.true
264
+
265
+
266
+ def positive_axis_domain_strata(expr, var):
267
+ """Sufficient exact real-domain strata on x>0.
268
+
269
+ These are not used to claim maximal domains outside the stated guards;
270
+ they provide parameter-conditioned branches that can be composed safely.
271
+ """
272
+ expr = sp.sympify(expr)
273
+ var = sp.sympify(var)
274
+ if (
275
+ expr.func in (sp.lowergamma, sp.uppergamma)
276
+ and len(expr.args) == 2
277
+ and expr.args[1] == var
278
+ ):
279
+ a = expr.args[0]
280
+ return ((var > 0, a > 0, "DLMF 8.2.1-8.2.2"),)
281
+ if expr.func is sp.beta and len(expr.args) == 2 and expr.args[1] == var:
282
+ a = expr.args[0]
283
+ return ((var > 0, a > 0, "DLMF 5.12.1 + Gamma quotient"),)
284
+ if expr.func is sp.besseli and len(expr.args) == 2 and expr.args[1] == var:
285
+ nu = expr.args[0]
286
+ return ((var > 0, sp.And(_real_guard(nu), nu >= 0), "DLMF 10.37"),)
287
+ if expr.func is sp.besselk and len(expr.args) == 2 and expr.args[1] == var:
288
+ nu = expr.args[0]
289
+ return ((var > 0, _real_guard(nu), "DLMF 10.37 + 10.27.3"),)
290
+ if (
291
+ expr.func in (sp.betainc, sp.betainc_regularized)
292
+ and len(expr.args) == 4
293
+ and expr.args[2] == 0
294
+ and expr.args[3] == var
295
+ ):
296
+ a, b = expr.args[:2]
297
+ return (
298
+ (sp.And(var >= 0, var <= 1), sp.And(a > 0, b > 0), "DLMF 8.17.1-8.17.2"),
299
+ )
300
+ return ()
301
+
302
+
303
+ def positive_axis_range_strata(expr, var, y):
304
+ """Exact ranges on x>0, including parameter-dependent endpoints."""
305
+ expr = sp.sympify(expr)
306
+ var = sp.sympify(var)
307
+ y = sp.sympify(y)
308
+ if expr.func is sp.besselk and len(expr.args) == 2 and expr.args[1] == var:
309
+ nu = expr.args[0]
310
+ return ((y > 0, _real_guard(nu), "DLMF 10.37 + 10.27.3 + endpoint limits"),)
311
+ if expr.func is sp.besseli and len(expr.args) == 2 and expr.args[1] == var:
312
+ nu = expr.args[0]
313
+ rg = _real_guard(nu)
314
+ # I_0(x) -> 1 as x->0+, while I_nu(x)->0 for nu>0; all diverge to
315
+ # +oo at +oo. The endpoint is not attained because x>0.
316
+ return (
317
+ (y > 1, sp.And(rg, sp.Eq(nu, 0)), "DLMF 10.37 + 10.30 limiting forms"),
318
+ (y > 0, sp.And(rg, nu > 0), "DLMF 10.37 + 10.30 limiting forms"),
319
+ )
320
+ if expr.func is sp.beta and len(expr.args) == 2 and expr.args[1] == var:
321
+ a = expr.args[0]
322
+ return ((y > 0, a > 0, "DLMF 5.12.1 + Gamma quotient limits"),)
323
+ if expr.func is sp.lowergamma and len(expr.args) == 2 and expr.args[1] == var:
324
+ a = expr.args[0]
325
+ return (
326
+ (sp.And(y > 0, y < sp.gamma(a)), a > 0, "DLMF 8.2.1 + endpoint limits"),
327
+ )
328
+ if expr.func is sp.uppergamma and len(expr.args) == 2 and expr.args[1] == var:
329
+ a = expr.args[0]
330
+ return (
331
+ (sp.And(y > 0, y < sp.gamma(a)), a > 0, "DLMF 8.2.2 + endpoint limits"),
332
+ )
333
+ return ()
334
+
335
+
336
+ def special_function_range_strata(expr, var, y, domain):
337
+ """Exact DLMF-backed ranges with domain-sensitive endpoint attainment."""
338
+ from .common import condition_to_set
339
+
340
+ expr = sp.sympify(expr)
341
+ var = sp.sympify(var)
342
+ y = sp.sympify(y)
343
+ ds = condition_to_set(domain, var)
344
+ pos_open = (
345
+ isinstance(ds, sp.Interval)
346
+ and ds.start == 0
347
+ and ds.left_open
348
+ and ds.end is S.Infinity
349
+ )
350
+ pos_closed = (
351
+ isinstance(ds, sp.Interval)
352
+ and ds.start == 0
353
+ and not ds.left_open
354
+ and ds.end is S.Infinity
355
+ )
356
+ unit_open = (
357
+ isinstance(ds, sp.Interval)
358
+ and ds.start == 0
359
+ and ds.end == 1
360
+ and ds.left_open
361
+ and ds.right_open
362
+ )
363
+ unit_closed = (
364
+ isinstance(ds, sp.Interval)
365
+ and ds.start == 0
366
+ and ds.end == 1
367
+ and not ds.left_open
368
+ and not ds.right_open
369
+ )
370
+
371
+ if (
372
+ expr.func is sp.besseli
373
+ and len(expr.args) == 2
374
+ and expr.args[1] == var
375
+ and (pos_open or pos_closed)
376
+ ):
377
+ nu = expr.args[0]
378
+ rg = _real_guard(nu)
379
+ if pos_open:
380
+ return (
381
+ (y > 1, sp.And(rg, sp.Eq(nu, 0)), "DLMF 10.37, 10.30, 10.40"),
382
+ (y > 0, sp.And(rg, nu > 0), "DLMF 10.37, 10.30, 10.40"),
383
+ )
384
+ return (
385
+ (y >= 1, sp.And(rg, sp.Eq(nu, 0)), "DLMF 10.37, 10.30, 10.40"),
386
+ (y >= 0, sp.And(rg, nu > 0), "DLMF 10.37, 10.30, 10.40"),
387
+ )
388
+
389
+ if (
390
+ expr.func in (sp.lowergamma, sp.uppergamma)
391
+ and len(expr.args) == 2
392
+ and expr.args[1] == var
393
+ and (pos_open or pos_closed)
394
+ ):
395
+ a = expr.args[0]
396
+ G = sp.gamma(a)
397
+ if expr.func is sp.lowergamma:
398
+ cond = sp.And(y > 0, y < G) if pos_open else sp.And(y >= 0, y < G)
399
+ else:
400
+ cond = sp.And(y > 0, y < G) if pos_open else sp.And(y > 0, y <= G)
401
+ return ((cond, a > 0, "DLMF 8.2.1-8.2.2 + endpoint limits"),)
402
+
403
+ if (
404
+ expr.func in (sp.betainc, sp.betainc_regularized)
405
+ and len(expr.args) == 4
406
+ and expr.args[2] == 0
407
+ and expr.args[3] == var
408
+ and (unit_open or unit_closed)
409
+ ):
410
+ a, b = expr.args[:2]
411
+ guard = sp.And(a > 0, b > 0)
412
+ endpoint = S.One if expr.func is sp.betainc_regularized else sp.beta(a, b)
413
+ cond = (
414
+ sp.And(y > 0, y < endpoint) if unit_open else sp.And(y >= 0, y <= endpoint)
415
+ )
416
+ return ((cond, guard, "DLMF 8.17.1-8.17.3"),)
417
+ return ()
418
+
419
+
420
+ def positive_axis_monotonicity_strata(expr, var):
421
+ from ..results import Monotonicity
422
+
423
+ expr = sp.sympify(expr)
424
+ var = sp.sympify(var)
425
+ if expr.func is sp.besselk and len(expr.args) == 2 and expr.args[1] == var:
426
+ return (
427
+ (
428
+ Monotonicity.STRICTLY_DECREASING,
429
+ _real_guard(expr.args[0]),
430
+ "DLMF 10.37 + 10.27.3",
431
+ ),
432
+ )
433
+ if expr.func is sp.besseli and len(expr.args) == 2 and expr.args[1] == var:
434
+ nu = expr.args[0]
435
+ return (
436
+ (
437
+ Monotonicity.STRICTLY_INCREASING,
438
+ sp.And(_real_guard(nu), nu >= 0),
439
+ "DLMF 10.37",
440
+ ),
441
+ )
442
+ if expr.func is sp.beta and len(expr.args) == 2 and expr.args[1] == var:
443
+ return (
444
+ (
445
+ Monotonicity.STRICTLY_DECREASING,
446
+ expr.args[0] > 0,
447
+ "derived from digamma monotonicity / beta quotient",
448
+ ),
449
+ )
450
+ if expr.func is sp.lowergamma and len(expr.args) == 2 and expr.args[1] == var:
451
+ return (
452
+ (
453
+ Monotonicity.STRICTLY_INCREASING,
454
+ expr.args[0] > 0,
455
+ "DLMF 8.2.1 integrand > 0",
456
+ ),
457
+ )
458
+ if expr.func is sp.uppergamma and len(expr.args) == 2 and expr.args[1] == var:
459
+ return (
460
+ (
461
+ Monotonicity.STRICTLY_DECREASING,
462
+ expr.args[0] > 0,
463
+ "DLMF 8.2.2 integrand > 0",
464
+ ),
465
+ )
466
+ return ()
467
+
468
+
469
+ def positive_axis_sign_strata(expr, var):
470
+ from ..results import Sign
471
+
472
+ expr = sp.sympify(expr)
473
+ var = sp.sympify(var)
474
+ if expr.func is sp.besselk and len(expr.args) == 2 and expr.args[1] == var:
475
+ return ((Sign.POSITIVE, _real_guard(expr.args[0]), "DLMF 10.37 + 10.27.3"),)
476
+ if expr.func is sp.besseli and len(expr.args) == 2 and expr.args[1] == var:
477
+ nu = expr.args[0]
478
+ return ((Sign.POSITIVE, sp.And(_real_guard(nu), nu >= 0), "DLMF 10.37"),)
479
+ if expr.func is sp.beta and len(expr.args) == 2 and expr.args[1] == var:
480
+ return ((Sign.POSITIVE, expr.args[0] > 0, "DLMF 5.12.1"),)
481
+ if (
482
+ expr.func in (sp.lowergamma, sp.uppergamma)
483
+ and len(expr.args) == 2
484
+ and expr.args[1] == var
485
+ ):
486
+ return ((Sign.POSITIVE, expr.args[0] > 0, "DLMF 8.2.1-8.2.2"),)
487
+ if expr.func is sp.polygamma and len(expr.args) == 2 and expr.args[1] == var:
488
+ n = expr.args[0]
489
+ pos = sp.And(sp.Contains(n, S.Integers), n >= 1, sp.Eq(sp.Mod(n, 2), 1))
490
+ neg = sp.And(sp.Contains(n, S.Integers), n >= 1, sp.Eq(sp.Mod(n, 2), 0))
491
+ return (
492
+ (Sign.POSITIVE, pos, "polygamma series"),
493
+ (Sign.NEGATIVE, neg, "polygamma series"),
494
+ )
495
+ return ()
496
+
497
+
498
+ def positive_axis_convexity_strata(expr, var):
499
+ from ..results import Convexity
500
+
501
+ expr = sp.sympify(expr)
502
+ var = sp.sympify(var)
503
+ if expr.func is sp.besselk and len(expr.args) == 2 and expr.args[1] == var:
504
+ return (
505
+ (
506
+ Convexity.STRICTLY_CONVEX,
507
+ _real_guard(expr.args[0]),
508
+ "DLMF-backed integral/complete monotonicity",
509
+ ),
510
+ )
511
+ if expr.func is sp.beta and len(expr.args) == 2 and expr.args[1] == var:
512
+ return (
513
+ (Convexity.STRICTLY_CONVEX, expr.args[0] > 0, "derived from beta quotient"),
514
+ )
515
+ return ()