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
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
|
|
5
|
+
import sympy as sp
|
|
6
|
+
from sympy import S
|
|
7
|
+
|
|
8
|
+
from .. import _semialg
|
|
9
|
+
from ..engine import evaluate_property
|
|
10
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
11
|
+
from ..results import ConditionalValue, Monotonicity, PropertyResult, Sign
|
|
12
|
+
from ..stratification import (
|
|
13
|
+
evaluate_parameter_strata,
|
|
14
|
+
normalize_cases,
|
|
15
|
+
semantic_condition,
|
|
16
|
+
)
|
|
17
|
+
from .common import (
|
|
18
|
+
combine_natural_domain_condition,
|
|
19
|
+
condition_to_set,
|
|
20
|
+
is_full_natural_domain,
|
|
21
|
+
)
|
|
22
|
+
from .domain import function_domain
|
|
23
|
+
from .monotonic_certificates import (
|
|
24
|
+
monotonic_decomposition,
|
|
25
|
+
monotonic_partition,
|
|
26
|
+
quick_nonmonotonicity_witness,
|
|
27
|
+
)
|
|
28
|
+
from .piecewise import (
|
|
29
|
+
combine_piecewise_monotonicity,
|
|
30
|
+
piecewise_branches,
|
|
31
|
+
piecewise_interface_limits,
|
|
32
|
+
)
|
|
33
|
+
from .sign import function_sign
|
|
34
|
+
from .special_functions import parameter_guard, positive_axis_monotonicity_strata
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _from_code(value):
|
|
38
|
+
try:
|
|
39
|
+
code = int(str(value))
|
|
40
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
41
|
+
return Monotonicity.UNKNOWN
|
|
42
|
+
return {
|
|
43
|
+
1: Monotonicity.STRICTLY_INCREASING,
|
|
44
|
+
2: Monotonicity.NONDECREASING,
|
|
45
|
+
-1: Monotonicity.STRICTLY_DECREASING,
|
|
46
|
+
-2: Monotonicity.NONINCREASING,
|
|
47
|
+
0: Monotonicity.NOT_MONOTONE,
|
|
48
|
+
}.get(code, Monotonicity.UNKNOWN)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _direct_registry(expr, var):
|
|
52
|
+
r = evaluate_property(expr, "GlobalMonotonicity", var=var, mode="real")
|
|
53
|
+
pguard = parameter_guard(expr, var, "GlobalMonotonicity")
|
|
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),),
|
|
60
|
+
source="registry-monotonicity-stratification",
|
|
61
|
+
)
|
|
62
|
+
if r.cases:
|
|
63
|
+
out = normalize_cases(
|
|
64
|
+
(
|
|
65
|
+
ConditionalValue(
|
|
66
|
+
_from_code(c.value), sp.And(semantic_condition(c.condition), pguard)
|
|
67
|
+
)
|
|
68
|
+
for c in r.cases
|
|
69
|
+
),
|
|
70
|
+
source="registry-monotonicity-stratification",
|
|
71
|
+
)
|
|
72
|
+
if out.cases:
|
|
73
|
+
return out
|
|
74
|
+
return Monotonicity.UNKNOWN
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _compose_monotonicity(expr, var, domain):
|
|
78
|
+
# g(h(x)) with one varying argument: combine outer and inner order.
|
|
79
|
+
varying = [
|
|
80
|
+
(i, a)
|
|
81
|
+
for i, a in enumerate(getattr(expr, "args", ()))
|
|
82
|
+
if getattr(a, "has", lambda *_: False)(var)
|
|
83
|
+
]
|
|
84
|
+
if len(varying) != 1 or expr.func in (sp.Add, sp.Mul, sp.Pow, sp.Piecewise):
|
|
85
|
+
return Monotonicity.UNKNOWN
|
|
86
|
+
i, inner = varying[0]
|
|
87
|
+
# A primitive f(x) is not a composition f(g(x)); treating it as f∘id can
|
|
88
|
+
# re-import an overbroad global catalogue fact on a restricted domain.
|
|
89
|
+
if inner == var:
|
|
90
|
+
return Monotonicity.UNKNOWN
|
|
91
|
+
X = sp.Dummy("X", real=True)
|
|
92
|
+
args = list(expr.args)
|
|
93
|
+
args[i] = X
|
|
94
|
+
outer = expr.func(*args)
|
|
95
|
+
om = _direct_registry(outer, X)
|
|
96
|
+
if om is Monotonicity.UNKNOWN and outer.func is sp.exp:
|
|
97
|
+
om = Monotonicity.STRICTLY_INCREASING
|
|
98
|
+
im = function_monotonicity(inner, var, domain=domain)
|
|
99
|
+
if isinstance(om, PropertyResult) or isinstance(im, PropertyResult):
|
|
100
|
+
return Monotonicity.UNKNOWN
|
|
101
|
+
inc = (Monotonicity.STRICTLY_INCREASING, Monotonicity.NONDECREASING)
|
|
102
|
+
dec = (Monotonicity.STRICTLY_DECREASING, Monotonicity.NONINCREASING)
|
|
103
|
+
if om in inc and im in inc:
|
|
104
|
+
return (
|
|
105
|
+
Monotonicity.STRICTLY_INCREASING
|
|
106
|
+
if om is inc[0] and im is inc[0]
|
|
107
|
+
else Monotonicity.NONDECREASING
|
|
108
|
+
)
|
|
109
|
+
if om in inc and im in dec:
|
|
110
|
+
return (
|
|
111
|
+
Monotonicity.STRICTLY_DECREASING
|
|
112
|
+
if om is inc[0] and im is dec[0]
|
|
113
|
+
else Monotonicity.NONINCREASING
|
|
114
|
+
)
|
|
115
|
+
if om in dec and im in inc:
|
|
116
|
+
return (
|
|
117
|
+
Monotonicity.STRICTLY_DECREASING
|
|
118
|
+
if om is dec[0] and im is inc[0]
|
|
119
|
+
else Monotonicity.NONINCREASING
|
|
120
|
+
)
|
|
121
|
+
if om in dec and im in dec:
|
|
122
|
+
return (
|
|
123
|
+
Monotonicity.STRICTLY_INCREASING
|
|
124
|
+
if om is dec[0] and im is dec[0]
|
|
125
|
+
else Monotonicity.NONDECREASING
|
|
126
|
+
)
|
|
127
|
+
return Monotonicity.UNKNOWN
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _closure_monotonicity(expr, var, domain):
|
|
131
|
+
"""Cheap sign-aware closure rules for sums, products and Min/Max."""
|
|
132
|
+
inc = (Monotonicity.STRICTLY_INCREASING, Monotonicity.NONDECREASING)
|
|
133
|
+
dec = (Monotonicity.STRICTLY_DECREASING, Monotonicity.NONINCREASING)
|
|
134
|
+
if expr.is_Add:
|
|
135
|
+
parts = [a for a in expr.args if a.has(var)]
|
|
136
|
+
if not parts:
|
|
137
|
+
return Monotonicity.CONSTANT
|
|
138
|
+
vals = [function_monotonicity(a, var, domain=domain) for a in parts]
|
|
139
|
+
if any(isinstance(v, PropertyResult) for v in vals):
|
|
140
|
+
return Monotonicity.UNKNOWN
|
|
141
|
+
if all(v in inc for v in vals):
|
|
142
|
+
return (
|
|
143
|
+
Monotonicity.STRICTLY_INCREASING
|
|
144
|
+
if any(v is inc[0] for v in vals)
|
|
145
|
+
else Monotonicity.NONDECREASING
|
|
146
|
+
)
|
|
147
|
+
if all(v in dec for v in vals):
|
|
148
|
+
return (
|
|
149
|
+
Monotonicity.STRICTLY_DECREASING
|
|
150
|
+
if any(v is dec[0] for v in vals)
|
|
151
|
+
else Monotonicity.NONINCREASING
|
|
152
|
+
)
|
|
153
|
+
if expr.func in (sp.Min, sp.Max):
|
|
154
|
+
vals = [
|
|
155
|
+
function_monotonicity(a, var, domain=domain)
|
|
156
|
+
for a in expr.args
|
|
157
|
+
if a.has(var)
|
|
158
|
+
]
|
|
159
|
+
if vals and not any(isinstance(v, PropertyResult) for v in vals):
|
|
160
|
+
if all(v in inc for v in vals):
|
|
161
|
+
return Monotonicity.NONDECREASING
|
|
162
|
+
if all(v in dec for v in vals):
|
|
163
|
+
return Monotonicity.NONINCREASING
|
|
164
|
+
if expr.is_Mul:
|
|
165
|
+
const, dep = expr.as_independent(var, as_Add=False)
|
|
166
|
+
parts = list(sp.Mul.make_args(dep))
|
|
167
|
+
if const.is_zero is True:
|
|
168
|
+
return Monotonicity.CONSTANT
|
|
169
|
+
# Product of positive factors moving in the same direction.
|
|
170
|
+
if parts:
|
|
171
|
+
vals = []
|
|
172
|
+
signs = []
|
|
173
|
+
for a in parts:
|
|
174
|
+
vals.append(function_monotonicity(a, var, domain=domain))
|
|
175
|
+
signs.append(function_sign(a, var, domain=domain))
|
|
176
|
+
if not any(isinstance(v, PropertyResult) for v in vals):
|
|
177
|
+
nonneg = all(q in (Sign.POSITIVE, Sign.NONNEGATIVE) for q in signs)
|
|
178
|
+
strictly_pos = all(q is Sign.POSITIVE for q in signs)
|
|
179
|
+
if nonneg and all(v in inc for v in vals):
|
|
180
|
+
# A merely nonnegative factor can vanish on an interval;
|
|
181
|
+
# then the product may have a plateau even if another
|
|
182
|
+
# factor is strictly increasing. Strictness is safe only
|
|
183
|
+
# when every factor is strictly positive.
|
|
184
|
+
out = (
|
|
185
|
+
Monotonicity.STRICTLY_INCREASING
|
|
186
|
+
if strictly_pos and any(v is inc[0] for v in vals)
|
|
187
|
+
else Monotonicity.NONDECREASING
|
|
188
|
+
)
|
|
189
|
+
if const.is_negative is True:
|
|
190
|
+
return (
|
|
191
|
+
Monotonicity.STRICTLY_DECREASING
|
|
192
|
+
if out is Monotonicity.STRICTLY_INCREASING
|
|
193
|
+
else Monotonicity.NONINCREASING
|
|
194
|
+
)
|
|
195
|
+
if const.is_positive is True:
|
|
196
|
+
return out
|
|
197
|
+
if nonneg and all(v in dec for v in vals):
|
|
198
|
+
out = (
|
|
199
|
+
Monotonicity.STRICTLY_DECREASING
|
|
200
|
+
if strictly_pos and any(v is dec[0] for v in vals)
|
|
201
|
+
else Monotonicity.NONINCREASING
|
|
202
|
+
)
|
|
203
|
+
if const.is_negative is True:
|
|
204
|
+
return (
|
|
205
|
+
Monotonicity.STRICTLY_INCREASING
|
|
206
|
+
if out is Monotonicity.STRICTLY_DECREASING
|
|
207
|
+
else Monotonicity.NONDECREASING
|
|
208
|
+
)
|
|
209
|
+
if const.is_positive is True:
|
|
210
|
+
return out
|
|
211
|
+
return Monotonicity.UNKNOWN
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _piecewise_monotonicity(expr, var, domain):
|
|
215
|
+
"""Classify finite interval branches and verify ordering at interfaces."""
|
|
216
|
+
if not isinstance(expr, sp.Piecewise):
|
|
217
|
+
return Monotonicity.UNKNOWN
|
|
218
|
+
branches = piecewise_branches(expr, var, domain)
|
|
219
|
+
if not branches:
|
|
220
|
+
return Monotonicity.UNKNOWN
|
|
221
|
+
values = [
|
|
222
|
+
function_monotonicity(b.expression, var, domain=b.condition) for b in branches
|
|
223
|
+
]
|
|
224
|
+
direction = combine_piecewise_monotonicity(values)
|
|
225
|
+
if direction in (Monotonicity.UNKNOWN, Monotonicity.NOT_MONOTONE):
|
|
226
|
+
return direction
|
|
227
|
+
interfaces = piecewise_interface_limits(branches, var)
|
|
228
|
+
if interfaces is None:
|
|
229
|
+
return direction if len(branches) == 1 else Monotonicity.UNKNOWN
|
|
230
|
+
for _, left_value, right_value in interfaces:
|
|
231
|
+
jump = sp.simplify(right_value - left_value)
|
|
232
|
+
if direction in (
|
|
233
|
+
Monotonicity.NONDECREASING,
|
|
234
|
+
Monotonicity.STRICTLY_INCREASING,
|
|
235
|
+
):
|
|
236
|
+
if jump.is_nonnegative is not True:
|
|
237
|
+
return (
|
|
238
|
+
Monotonicity.NOT_MONOTONE
|
|
239
|
+
if jump.is_negative is True
|
|
240
|
+
else Monotonicity.UNKNOWN
|
|
241
|
+
)
|
|
242
|
+
elif direction in (
|
|
243
|
+
Monotonicity.NONINCREASING,
|
|
244
|
+
Monotonicity.STRICTLY_DECREASING,
|
|
245
|
+
):
|
|
246
|
+
if jump.is_nonpositive is not True:
|
|
247
|
+
return (
|
|
248
|
+
Monotonicity.NOT_MONOTONE
|
|
249
|
+
if jump.is_positive is True
|
|
250
|
+
else Monotonicity.UNKNOWN
|
|
251
|
+
)
|
|
252
|
+
return direction
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _parameter_strata(expr, var, domain):
|
|
256
|
+
if expr.func is sp.Pow and expr.base == var:
|
|
257
|
+
a = expr.exp
|
|
258
|
+
if a.free_symbols and domain == (var > 0):
|
|
259
|
+
return normalize_cases(
|
|
260
|
+
(
|
|
261
|
+
ConditionalValue(Monotonicity.STRICTLY_INCREASING, a > 0),
|
|
262
|
+
ConditionalValue(Monotonicity.CONSTANT, sp.Eq(a, 0)),
|
|
263
|
+
ConditionalValue(Monotonicity.STRICTLY_DECREASING, a < 0),
|
|
264
|
+
),
|
|
265
|
+
source="power-parameter-stratification",
|
|
266
|
+
)
|
|
267
|
+
if expr.func is sp.exp:
|
|
268
|
+
g = sp.expand(expr.args[0])
|
|
269
|
+
try:
|
|
270
|
+
p = sp.Poly(g, var)
|
|
271
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
272
|
+
p = None
|
|
273
|
+
if p is not None and p.degree() == 1:
|
|
274
|
+
a = p.coeff_monomial(var)
|
|
275
|
+
if a.free_symbols:
|
|
276
|
+
return normalize_cases(
|
|
277
|
+
(
|
|
278
|
+
ConditionalValue(Monotonicity.STRICTLY_INCREASING, a > 0),
|
|
279
|
+
ConditionalValue(Monotonicity.CONSTANT, sp.Eq(a, 0)),
|
|
280
|
+
ConditionalValue(Monotonicity.STRICTLY_DECREASING, a < 0),
|
|
281
|
+
),
|
|
282
|
+
source="exp-affine-parameter-stratification",
|
|
283
|
+
)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _semialgebraic_monotonicity_stage(expr, var, assumptions, *, details, intervals):
|
|
288
|
+
"""Run the exact semialgebraic monotonicity stage, if eligible.
|
|
289
|
+
|
|
290
|
+
Interval requests deliberately continue to funcprops' shared partition
|
|
291
|
+
engine because callers rely on its boundary/zero-geometry certificates.
|
|
292
|
+
"""
|
|
293
|
+
if intervals or not _semialg.eligible_problem(expr, var, domain=assumptions):
|
|
294
|
+
return None
|
|
295
|
+
try:
|
|
296
|
+
raw = _semialg.function_monotonicity(
|
|
297
|
+
expr, var, domain=assumptions, return_result=bool(intervals or details)
|
|
298
|
+
)
|
|
299
|
+
except (_semialg.SemialgUnavailable, *SYMBOLIC_QUERY_ERRORS):
|
|
300
|
+
return None
|
|
301
|
+
value = _semialg.normalize_monotonicity(raw)
|
|
302
|
+
if value is Monotonicity.UNKNOWN:
|
|
303
|
+
return None
|
|
304
|
+
classification = getattr(raw, "classification", raw)
|
|
305
|
+
evidence = (raw,) if raw is not classification else ()
|
|
306
|
+
return value, evidence
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _classify_monotonicity(
|
|
310
|
+
expr,
|
|
311
|
+
var,
|
|
312
|
+
*,
|
|
313
|
+
domain=True,
|
|
314
|
+
intervals=False,
|
|
315
|
+
details=False,
|
|
316
|
+
partition=None,
|
|
317
|
+
_domain_ready=False,
|
|
318
|
+
):
|
|
319
|
+
"""Classify global monotonicity on the natural/requested real domain.
|
|
320
|
+
|
|
321
|
+
Expensive critical-point decomposition is deliberately delayed until cheap
|
|
322
|
+
registry, closure, and composition proofs have failed. When needed, the
|
|
323
|
+
shared :func:`monotonic_partition` certificate is reused by range,
|
|
324
|
+
injectivity, inverse, oscillation, and extrema consumers.
|
|
325
|
+
"""
|
|
326
|
+
expr = sp.sympify(expr)
|
|
327
|
+
var = sp.sympify(var)
|
|
328
|
+
if not expr.has(var):
|
|
329
|
+
return Monotonicity.CONSTANT
|
|
330
|
+
natural = function_domain(expr, var)
|
|
331
|
+
assumptions = (
|
|
332
|
+
sp.sympify(domain)
|
|
333
|
+
if _domain_ready
|
|
334
|
+
else combine_natural_domain_condition(natural, domain, var)
|
|
335
|
+
)
|
|
336
|
+
with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
|
|
337
|
+
assumptions = sp.simplify(assumptions)
|
|
338
|
+
if not _domain_ready:
|
|
339
|
+
split = evaluate_parameter_strata(
|
|
340
|
+
assumptions,
|
|
341
|
+
var,
|
|
342
|
+
lambda branch_domain: _classify_monotonicity(
|
|
343
|
+
expr,
|
|
344
|
+
var,
|
|
345
|
+
domain=branch_domain,
|
|
346
|
+
intervals=intervals,
|
|
347
|
+
details=details,
|
|
348
|
+
partition=None,
|
|
349
|
+
_domain_ready=True,
|
|
350
|
+
),
|
|
351
|
+
source="monotonicity-parameter-stratification",
|
|
352
|
+
)
|
|
353
|
+
if split is not None:
|
|
354
|
+
return split
|
|
355
|
+
ds = condition_to_set(assumptions, var)
|
|
356
|
+
geom_domain = ds if isinstance(ds, sp.Set) else S.Reals
|
|
357
|
+
|
|
358
|
+
def finish(value, source=None, evidence=()):
|
|
359
|
+
if intervals or details:
|
|
360
|
+
return PropertyResult(
|
|
361
|
+
value, source=source or "monotonicity", evidence=tuple(evidence)
|
|
362
|
+
)
|
|
363
|
+
return value
|
|
364
|
+
|
|
365
|
+
strata = _parameter_strata(expr, var, assumptions)
|
|
366
|
+
if strata is not None:
|
|
367
|
+
return strata
|
|
368
|
+
piecewise = _piecewise_monotonicity(expr, var, assumptions)
|
|
369
|
+
if piecewise is not Monotonicity.UNKNOWN:
|
|
370
|
+
return finish(piecewise, "piecewise-analysis")
|
|
371
|
+
try:
|
|
372
|
+
positive_axis = (
|
|
373
|
+
isinstance(ds, sp.Interval)
|
|
374
|
+
and ds.start == 0
|
|
375
|
+
and ds.left_open
|
|
376
|
+
and ds.end is S.Infinity
|
|
377
|
+
) or sp.simplify_logic(
|
|
378
|
+
sp.Implies(semantic_condition(assumptions), var > 0)
|
|
379
|
+
) is S.true
|
|
380
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
381
|
+
positive_axis = False
|
|
382
|
+
if positive_axis:
|
|
383
|
+
facts = positive_axis_monotonicity_strata(expr, var)
|
|
384
|
+
if facts:
|
|
385
|
+
out = normalize_cases(
|
|
386
|
+
(ConditionalValue(v, g) for v, g, *_ in facts),
|
|
387
|
+
source="special-function-monotonicity-stratification",
|
|
388
|
+
)
|
|
389
|
+
if out.primary is not None:
|
|
390
|
+
return finish(
|
|
391
|
+
out.primary, "special-function-monotonicity-stratification"
|
|
392
|
+
)
|
|
393
|
+
return out
|
|
394
|
+
|
|
395
|
+
# Stored GlobalMonotonicity describes the whole natural domain only.
|
|
396
|
+
if domain is True or is_full_natural_domain(assumptions, natural):
|
|
397
|
+
m = _direct_registry(expr, var)
|
|
398
|
+
if m is not Monotonicity.UNKNOWN:
|
|
399
|
+
if intervals or details:
|
|
400
|
+
part = (
|
|
401
|
+
partition
|
|
402
|
+
if partition is not None
|
|
403
|
+
and getattr(partition, "domain", None) == geom_domain
|
|
404
|
+
else monotonic_partition(expr, var, domain=geom_domain)
|
|
405
|
+
)
|
|
406
|
+
ev = (
|
|
407
|
+
(part.property_decomposition,)
|
|
408
|
+
if part.property_decomposition is not None
|
|
409
|
+
else ()
|
|
410
|
+
) + ((part,) if part.complete else ())
|
|
411
|
+
return finish(m, "registry-monotonicity", ev)
|
|
412
|
+
return m
|
|
413
|
+
|
|
414
|
+
closure = _closure_monotonicity(expr, var, assumptions)
|
|
415
|
+
if closure is not Monotonicity.UNKNOWN:
|
|
416
|
+
return finish(closure, "closure-properties")
|
|
417
|
+
|
|
418
|
+
backend = _semialgebraic_monotonicity_stage(
|
|
419
|
+
expr, var, assumptions, details=details, intervals=intervals
|
|
420
|
+
)
|
|
421
|
+
if backend is not None:
|
|
422
|
+
value, backend_evidence = backend
|
|
423
|
+
return finish(value, "semialg", backend_evidence)
|
|
424
|
+
|
|
425
|
+
dec = monotonic_decomposition(expr, var, strict=True, continuous=True)
|
|
426
|
+
if dec is not None and dec.inner != expr and dec.inner.has(var):
|
|
427
|
+
try:
|
|
428
|
+
valid = sp.simplify_logic(sp.Implies(assumptions, dec.condition))
|
|
429
|
+
if valid is S.true:
|
|
430
|
+
im = function_monotonicity(dec.inner, var, domain=assumptions)
|
|
431
|
+
else:
|
|
432
|
+
im = PropertyResult(None)
|
|
433
|
+
if not isinstance(im, PropertyResult):
|
|
434
|
+
inc = (Monotonicity.STRICTLY_INCREASING, Monotonicity.NONDECREASING)
|
|
435
|
+
decm = (Monotonicity.STRICTLY_DECREASING, Monotonicity.NONINCREASING)
|
|
436
|
+
if im in inc + decm:
|
|
437
|
+
increasing = im in inc
|
|
438
|
+
strict = im in (inc[0], decm[0]) and dec.strict
|
|
439
|
+
if dec.direction < 0:
|
|
440
|
+
increasing = not increasing
|
|
441
|
+
out = (
|
|
442
|
+
Monotonicity.STRICTLY_INCREASING
|
|
443
|
+
if increasing and strict
|
|
444
|
+
else Monotonicity.NONDECREASING
|
|
445
|
+
if increasing
|
|
446
|
+
else Monotonicity.STRICTLY_DECREASING
|
|
447
|
+
if strict
|
|
448
|
+
else Monotonicity.NONINCREASING
|
|
449
|
+
)
|
|
450
|
+
return finish(out, "monotonic-decomposition", (dec,))
|
|
451
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
452
|
+
pass
|
|
453
|
+
|
|
454
|
+
m = _compose_monotonicity(expr, var, assumptions)
|
|
455
|
+
if m is not Monotonicity.UNKNOWN:
|
|
456
|
+
return finish(m, "composition")
|
|
457
|
+
|
|
458
|
+
def from_sign(v):
|
|
459
|
+
if v is Sign.POSITIVE:
|
|
460
|
+
return Monotonicity.STRICTLY_INCREASING
|
|
461
|
+
if v is Sign.NEGATIVE:
|
|
462
|
+
return Monotonicity.STRICTLY_DECREASING
|
|
463
|
+
if v is Sign.NONNEGATIVE:
|
|
464
|
+
return Monotonicity.NONDECREASING
|
|
465
|
+
if v is Sign.NONPOSITIVE:
|
|
466
|
+
return Monotonicity.NONINCREASING
|
|
467
|
+
if v is Sign.ZERO:
|
|
468
|
+
return Monotonicity.CONSTANT
|
|
469
|
+
if v is Sign.SIGN_CHANGING:
|
|
470
|
+
return Monotonicity.NOT_MONOTONE
|
|
471
|
+
return Monotonicity.UNKNOWN
|
|
472
|
+
|
|
473
|
+
def from_partition(part):
|
|
474
|
+
ev = (
|
|
475
|
+
(part.property_decomposition,)
|
|
476
|
+
if part.property_decomposition is not None
|
|
477
|
+
else ()
|
|
478
|
+
) + (part,)
|
|
479
|
+
if part.indexed is not None and part.indexed.boundary_geometry:
|
|
480
|
+
# Completeness of the entire requested domain is not needed to
|
|
481
|
+
# disprove monotonicity: an infinite alternating critical family
|
|
482
|
+
# contained inside the domain is already a certificate.
|
|
483
|
+
try:
|
|
484
|
+
from ..transcendental.zero_geometry import (
|
|
485
|
+
geometry_support_inside_domain,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
if any(
|
|
489
|
+
geometry_support_inside_domain(g, part.domain)
|
|
490
|
+
for g in part.indexed.boundary_geometry
|
|
491
|
+
):
|
|
492
|
+
return finish(
|
|
493
|
+
Monotonicity.NOT_MONOTONE, "indexed-monotonic-partition", ev
|
|
494
|
+
)
|
|
495
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
496
|
+
pass
|
|
497
|
+
if not part.complete:
|
|
498
|
+
return None
|
|
499
|
+
if part.cells:
|
|
500
|
+
dirs = {c.direction for c in part.cells}
|
|
501
|
+
if dirs == {Monotonicity.STRICTLY_INCREASING}:
|
|
502
|
+
return finish(
|
|
503
|
+
Monotonicity.STRICTLY_INCREASING, "monotonic-partition", ev
|
|
504
|
+
)
|
|
505
|
+
if dirs == {Monotonicity.STRICTLY_DECREASING}:
|
|
506
|
+
return finish(
|
|
507
|
+
Monotonicity.STRICTLY_DECREASING, "monotonic-partition", ev
|
|
508
|
+
)
|
|
509
|
+
if len(dirs) > 1:
|
|
510
|
+
return finish(Monotonicity.NOT_MONOTONE, "monotonic-partition", ev)
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
# Indexed zero geometry is cheap to recognize and gives stronger evidence
|
|
514
|
+
# than a generic derivative-sign attempt. Details/interval requests also
|
|
515
|
+
# explicitly ask for the partition certificate.
|
|
516
|
+
need_part = bool(intervals or details)
|
|
517
|
+
if not need_part:
|
|
518
|
+
try:
|
|
519
|
+
from ..transcendental.zero_geometry import zero_geometry
|
|
520
|
+
|
|
521
|
+
need_part = bool(
|
|
522
|
+
zero_geometry(expr, var, derivative_order=1, domain=geom_domain)
|
|
523
|
+
)
|
|
524
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
525
|
+
need_part = False
|
|
526
|
+
part = (
|
|
527
|
+
partition
|
|
528
|
+
if partition is not None and getattr(partition, "domain", None) == geom_domain
|
|
529
|
+
else None
|
|
530
|
+
)
|
|
531
|
+
if need_part and part is None:
|
|
532
|
+
part = monotonic_partition(expr, var, domain=geom_domain)
|
|
533
|
+
ans = from_partition(part)
|
|
534
|
+
if ans is not None:
|
|
535
|
+
return ans
|
|
536
|
+
|
|
537
|
+
# For generic expressions, derivative sign is often cheaper than solving
|
|
538
|
+
# all critical points. Strong signs finish immediately. Weak signs are
|
|
539
|
+
# retained as a fallback but a partition may upgrade them to strictness.
|
|
540
|
+
try:
|
|
541
|
+
from ..proof_cache import cached_derivative
|
|
542
|
+
|
|
543
|
+
derivative = (
|
|
544
|
+
part.derivative
|
|
545
|
+
if part is not None and part.derivative is not None
|
|
546
|
+
else cached_derivative(expr, var, order=1)
|
|
547
|
+
)
|
|
548
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
549
|
+
return finish(
|
|
550
|
+
Monotonicity.UNKNOWN,
|
|
551
|
+
"unknown",
|
|
552
|
+
(part,) if part is not None and (details or intervals) else (),
|
|
553
|
+
)
|
|
554
|
+
if derivative == 0:
|
|
555
|
+
return finish(
|
|
556
|
+
Monotonicity.CONSTANT,
|
|
557
|
+
"zero-derivative",
|
|
558
|
+
(part,) if part is not None else (),
|
|
559
|
+
)
|
|
560
|
+
s = function_sign(derivative, var, domain=assumptions, strict_inequalities=True)
|
|
561
|
+
if isinstance(s, PropertyResult):
|
|
562
|
+
out = normalize_cases(
|
|
563
|
+
(ConditionalValue(from_sign(c.value), c.condition) for c in s.cases),
|
|
564
|
+
source="derivative-sign-stratification",
|
|
565
|
+
)
|
|
566
|
+
return out if out.cases else Monotonicity.UNKNOWN
|
|
567
|
+
direct = from_sign(s)
|
|
568
|
+
if direct in (
|
|
569
|
+
Monotonicity.STRICTLY_INCREASING,
|
|
570
|
+
Monotonicity.STRICTLY_DECREASING,
|
|
571
|
+
Monotonicity.CONSTANT,
|
|
572
|
+
Monotonicity.NOT_MONOTONE,
|
|
573
|
+
):
|
|
574
|
+
return finish(
|
|
575
|
+
direct,
|
|
576
|
+
"derivative-sign",
|
|
577
|
+
(part,) if part is not None and (details or intervals) else (),
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
if part is None:
|
|
581
|
+
part = monotonic_partition(expr, var, domain=geom_domain)
|
|
582
|
+
ans = from_partition(part)
|
|
583
|
+
if ans is not None:
|
|
584
|
+
return ans
|
|
585
|
+
|
|
586
|
+
witness = quick_nonmonotonicity_witness(expr, var, geom_domain)
|
|
587
|
+
if witness is not None:
|
|
588
|
+
return finish(
|
|
589
|
+
Monotonicity.NOT_MONOTONE, "exact-ordering-witness", (witness, part)
|
|
590
|
+
)
|
|
591
|
+
return finish(direct, "derivative-sign", (part,) if (details or intervals) else ())
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _weaken_monotonicity(value):
|
|
595
|
+
"""Map strict monotonicity classes to their weak counterparts."""
|
|
596
|
+
if value is Monotonicity.STRICTLY_INCREASING:
|
|
597
|
+
return Monotonicity.NONDECREASING
|
|
598
|
+
if value is Monotonicity.STRICTLY_DECREASING:
|
|
599
|
+
return Monotonicity.NONINCREASING
|
|
600
|
+
return value
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _monotonicity_implications(value):
|
|
604
|
+
"""Return ordinary monotonicity properties implied by ``value``."""
|
|
605
|
+
if value is Monotonicity.STRICTLY_INCREASING:
|
|
606
|
+
return (Monotonicity.NONDECREASING,)
|
|
607
|
+
if value is Monotonicity.STRICTLY_DECREASING:
|
|
608
|
+
return (Monotonicity.NONINCREASING,)
|
|
609
|
+
if value is Monotonicity.CONSTANT:
|
|
610
|
+
return (Monotonicity.NONDECREASING, Monotonicity.NONINCREASING)
|
|
611
|
+
return ()
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _format_monotonicity(result, *, strict_inequalities, all_properties):
|
|
615
|
+
"""Apply public strictness and implication options to a classification."""
|
|
616
|
+
weaken = (lambda value: value) if strict_inequalities else _weaken_monotonicity
|
|
617
|
+
if isinstance(result, PropertyResult):
|
|
618
|
+
primary = weaken(result.primary) if result.primary is not None else None
|
|
619
|
+
cases = tuple(
|
|
620
|
+
ConditionalValue(weaken(case.value), case.condition)
|
|
621
|
+
for case in result.cases
|
|
622
|
+
)
|
|
623
|
+
implied = (
|
|
624
|
+
_monotonicity_implications(primary)
|
|
625
|
+
if all_properties
|
|
626
|
+
else result.all_properties
|
|
627
|
+
)
|
|
628
|
+
return PropertyResult(
|
|
629
|
+
primary,
|
|
630
|
+
cases=cases,
|
|
631
|
+
all_properties=implied,
|
|
632
|
+
source=result.source,
|
|
633
|
+
exceptions=result.exceptions,
|
|
634
|
+
evidence=result.evidence,
|
|
635
|
+
)
|
|
636
|
+
primary = weaken(result)
|
|
637
|
+
if all_properties:
|
|
638
|
+
return PropertyResult(
|
|
639
|
+
primary,
|
|
640
|
+
all_properties=_monotonicity_implications(primary),
|
|
641
|
+
source="monotonicity",
|
|
642
|
+
)
|
|
643
|
+
return primary
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def function_monotonicity(
|
|
647
|
+
expr,
|
|
648
|
+
var,
|
|
649
|
+
*,
|
|
650
|
+
domain=True,
|
|
651
|
+
strict_inequalities=True,
|
|
652
|
+
all_properties=False,
|
|
653
|
+
intervals=False,
|
|
654
|
+
details=False,
|
|
655
|
+
partition=None,
|
|
656
|
+
):
|
|
657
|
+
"""Classify global monotonicity on the natural/requested real domain.
|
|
658
|
+
|
|
659
|
+
``strict_inequalities=False`` weakens strict increasing/decreasing results.
|
|
660
|
+
``all_properties=True`` returns the primary class together with its ordinary
|
|
661
|
+
monotonicity implications. Detail and interval requests retain certificates.
|
|
662
|
+
"""
|
|
663
|
+
result = _classify_monotonicity(
|
|
664
|
+
expr,
|
|
665
|
+
var,
|
|
666
|
+
domain=domain,
|
|
667
|
+
intervals=intervals,
|
|
668
|
+
details=details,
|
|
669
|
+
partition=partition,
|
|
670
|
+
)
|
|
671
|
+
return _format_monotonicity(
|
|
672
|
+
result, strict_inequalities=strict_inequalities, all_properties=all_properties
|
|
673
|
+
)
|