funcprops 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- funcprops/__init__.py +234 -0
- funcprops/_semialg.py +246 -0
- funcprops/api.py +70 -0
- funcprops/branchcuts/__init__.py +141 -0
- funcprops/branchcuts/computation.py +664 -0
- funcprops/branchcuts/core.py +1204 -0
- funcprops/branchcuts/post_simplify.py +461 -0
- funcprops/conditions.py +124 -0
- funcprops/data/catalogue.json +127449 -0
- funcprops/data/dlmf_audit.json +3382 -0
- funcprops/dlmf.py +29 -0
- funcprops/engine.py +324 -0
- funcprops/exceptions.py +29 -0
- funcprops/proof_cache.py +373 -0
- funcprops/real_analysis/__init__.py +52 -0
- funcprops/real_analysis/advanced_convexity.py +217 -0
- funcprops/real_analysis/boundedness.py +168 -0
- funcprops/real_analysis/common.py +244 -0
- funcprops/real_analysis/convexity.py +698 -0
- funcprops/real_analysis/differentiability.py +112 -0
- funcprops/real_analysis/domain.py +188 -0
- funcprops/real_analysis/mapping.py +374 -0
- funcprops/real_analysis/monotonic_certificates.py +867 -0
- funcprops/real_analysis/monotonicity.py +673 -0
- funcprops/real_analysis/oscillation.py +151 -0
- funcprops/real_analysis/piecewise.py +174 -0
- funcprops/real_analysis/range.py +552 -0
- funcprops/real_analysis/sign.py +561 -0
- funcprops/real_analysis/smoothness.py +268 -0
- funcprops/real_analysis/special_functions.py +515 -0
- funcprops/real_analysis/structural_properties.py +558 -0
- funcprops/real_analysis/zeros.py +327 -0
- funcprops/registry.py +274 -0
- funcprops/results.py +239 -0
- funcprops/singularities.py +1767 -0
- funcprops/stratification.py +494 -0
- funcprops/sympy_compare.py +63 -0
- funcprops/transcendental/__init__.py +140 -0
- funcprops/transcendental/algebraize.py +201 -0
- funcprops/transcendental/analytic.py +261 -0
- funcprops/transcendental/complex_roots.py +542 -0
- funcprops/transcendental/exp_fourier.py +221 -0
- funcprops/transcendental/extrema.py +821 -0
- funcprops/transcendental/periodic.py +222 -0
- funcprops/transcendental/proof.py +104 -0
- funcprops/transcendental/real_interval.py +128 -0
- funcprops/transcendental/real_sign.py +117 -0
- funcprops/transcendental/reduce.py +288 -0
- funcprops/transcendental/roots.py +1380 -0
- funcprops/transcendental/zero_geometry.py +1197 -0
- funcprops/verification.py +410 -0
- funcprops-0.1.0.dist-info/METADATA +208 -0
- funcprops-0.1.0.dist-info/RECORD +56 -0
- funcprops-0.1.0.dist-info/WHEEL +5 -0
- funcprops-0.1.0.dist-info/licenses/LICENSE +674 -0
- funcprops-0.1.0.dist-info/top_level.txt +1 -0
funcprops/proof_cache.py
ADDED
|
@@ -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 ()
|