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,561 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
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, PropertyResult, Sign, TruthValue
|
|
12
|
+
from ..stratification import (
|
|
13
|
+
evaluate_parameter_strata,
|
|
14
|
+
implication_guard,
|
|
15
|
+
normalize_cases,
|
|
16
|
+
semantic_condition,
|
|
17
|
+
)
|
|
18
|
+
from ..transcendental.algebraize import algebraize
|
|
19
|
+
from ..transcendental.roots import transcendental_roots
|
|
20
|
+
from .common import (
|
|
21
|
+
combine_natural_domain_condition,
|
|
22
|
+
condition_to_set,
|
|
23
|
+
exact_order_real_points,
|
|
24
|
+
)
|
|
25
|
+
from .domain import function_domain
|
|
26
|
+
from .piecewise import combine_piecewise_sign, piecewise_branches
|
|
27
|
+
from .special_functions import positive_axis_sign_strata
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _constant_sign(expr):
|
|
31
|
+
if expr == 0:
|
|
32
|
+
return Sign.ZERO
|
|
33
|
+
if expr.is_positive is True:
|
|
34
|
+
return Sign.POSITIVE
|
|
35
|
+
if expr.is_negative is True:
|
|
36
|
+
return Sign.NEGATIVE
|
|
37
|
+
if expr.is_nonnegative is True:
|
|
38
|
+
return Sign.NONNEGATIVE
|
|
39
|
+
if expr.is_nonpositive is True:
|
|
40
|
+
return Sign.NONPOSITIVE
|
|
41
|
+
return Sign.UNKNOWN
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _registry_sign(expr, var, assumptions=S.true):
|
|
45
|
+
"""Use catalogue sign loci as global sign proofs/parameter strata.
|
|
46
|
+
|
|
47
|
+
Registry ``Positive`` etc. values are predicates in the function variable,
|
|
48
|
+
not booleans saying that the function is globally positive. We therefore
|
|
49
|
+
prove that the requested domain implies each locus and retain any residual
|
|
50
|
+
parameter-only guard.
|
|
51
|
+
"""
|
|
52
|
+
cases = []
|
|
53
|
+
for prop, val in (
|
|
54
|
+
("Positive", Sign.POSITIVE),
|
|
55
|
+
("Negative", Sign.NEGATIVE),
|
|
56
|
+
("NonNegative", Sign.NONNEGATIVE),
|
|
57
|
+
("NonPositive", Sign.NONPOSITIVE),
|
|
58
|
+
):
|
|
59
|
+
r = evaluate_property(expr, prop, var=var, mode="real", assumptions=assumptions)
|
|
60
|
+
rcases = r.cases or (
|
|
61
|
+
() if r.primary is None else (ConditionalValue(r.primary, S.true),)
|
|
62
|
+
)
|
|
63
|
+
for c in rcases:
|
|
64
|
+
guard = implication_guard(assumptions, c.value, var)
|
|
65
|
+
if guard is None:
|
|
66
|
+
continue
|
|
67
|
+
try:
|
|
68
|
+
guard = sp.simplify_logic(
|
|
69
|
+
sp.And(semantic_condition(c.condition), guard)
|
|
70
|
+
)
|
|
71
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
72
|
+
guard = sp.And(semantic_condition(c.condition), guard)
|
|
73
|
+
if guard is not S.false:
|
|
74
|
+
cases.append(ConditionalValue(val, guard))
|
|
75
|
+
if not cases:
|
|
76
|
+
return Sign.UNKNOWN
|
|
77
|
+
out = normalize_cases(cases, source="registry-sign-stratification")
|
|
78
|
+
return out.primary if out.primary is not None else out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _constant_sign_strata(expr):
|
|
82
|
+
"""Exact parameter strata for a real constant with unresolved sign."""
|
|
83
|
+
expr = sp.sympify(expr)
|
|
84
|
+
if not expr.free_symbols or expr.is_real is False:
|
|
85
|
+
return Sign.UNKNOWN
|
|
86
|
+
return normalize_cases(
|
|
87
|
+
(
|
|
88
|
+
ConditionalValue(Sign.POSITIVE, expr > 0),
|
|
89
|
+
ConditionalValue(Sign.ZERO, sp.Eq(expr, 0)),
|
|
90
|
+
ConditionalValue(Sign.NEGATIVE, expr < 0),
|
|
91
|
+
),
|
|
92
|
+
source="constant-parameter-sign",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _semialg_sign(expr, var, assumptions):
|
|
97
|
+
if not _semialg.eligible_problem(expr, var, domain=assumptions):
|
|
98
|
+
return Sign.UNKNOWN
|
|
99
|
+
try:
|
|
100
|
+
raw = _semialg.function_sign(
|
|
101
|
+
expr, var, assumptions=assumptions, return_result=True
|
|
102
|
+
)
|
|
103
|
+
value = _semialg.normalize_sign(raw)
|
|
104
|
+
if value is not Sign.UNKNOWN:
|
|
105
|
+
return value
|
|
106
|
+
pieces = _semialg.function_sign_partition(expr, var, domain=assumptions)
|
|
107
|
+
signs = {
|
|
108
|
+
str(
|
|
109
|
+
getattr(
|
|
110
|
+
piece,
|
|
111
|
+
"classification",
|
|
112
|
+
piece[0] if isinstance(piece, tuple) else piece,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
for piece in pieces
|
|
116
|
+
}
|
|
117
|
+
if "positive" in signs and "negative" in signs:
|
|
118
|
+
return Sign.SIGN_CHANGING
|
|
119
|
+
if signs == {"zero"}:
|
|
120
|
+
return Sign.ZERO
|
|
121
|
+
return Sign.UNKNOWN
|
|
122
|
+
except (_semialg.SemialgUnavailable, *SYMBOLIC_QUERY_ERRORS):
|
|
123
|
+
return Sign.UNKNOWN
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _multiply_sign_values(a, b):
|
|
127
|
+
if a is Sign.ZERO or b is Sign.ZERO:
|
|
128
|
+
return Sign.ZERO
|
|
129
|
+
strict = {Sign.POSITIVE: 1, Sign.NEGATIVE: -1}
|
|
130
|
+
weak = {Sign.NONNEGATIVE: 1, Sign.NONPOSITIVE: -1}
|
|
131
|
+
if a in strict and b in strict:
|
|
132
|
+
return Sign.POSITIVE if strict[a] * strict[b] > 0 else Sign.NEGATIVE
|
|
133
|
+
if a in strict or a in weak:
|
|
134
|
+
sa = strict.get(a, weak.get(a))
|
|
135
|
+
if b in strict or b in weak:
|
|
136
|
+
sb = strict.get(b, weak.get(b))
|
|
137
|
+
return Sign.NONNEGATIVE if sa * sb > 0 else Sign.NONPOSITIVE
|
|
138
|
+
return Sign.UNKNOWN
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _multiply_sign_results(values):
|
|
142
|
+
cases = [ConditionalValue(Sign.POSITIVE, S.true)]
|
|
143
|
+
conditional = False
|
|
144
|
+
for value in values:
|
|
145
|
+
vcases = (
|
|
146
|
+
value.cases
|
|
147
|
+
if isinstance(value, PropertyResult)
|
|
148
|
+
else (ConditionalValue(value, S.true),)
|
|
149
|
+
)
|
|
150
|
+
conditional = conditional or isinstance(value, PropertyResult)
|
|
151
|
+
new = []
|
|
152
|
+
for a in cases:
|
|
153
|
+
for b in vcases:
|
|
154
|
+
v = _multiply_sign_values(a.value, b.value)
|
|
155
|
+
if v is Sign.UNKNOWN:
|
|
156
|
+
continue
|
|
157
|
+
try:
|
|
158
|
+
cond = sp.simplify_logic(sp.And(a.condition, b.condition))
|
|
159
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
160
|
+
cond = sp.And(a.condition, b.condition)
|
|
161
|
+
if cond is not S.false:
|
|
162
|
+
new.append(ConditionalValue(v, cond))
|
|
163
|
+
if not new:
|
|
164
|
+
return Sign.UNKNOWN
|
|
165
|
+
cases = new
|
|
166
|
+
out = normalize_cases(cases, source="product-sign-stratification")
|
|
167
|
+
if not conditional and out.primary is not None:
|
|
168
|
+
return out.primary
|
|
169
|
+
return out.primary if out.primary is not None else out
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _is_positive_axis_domain(domain, var):
|
|
173
|
+
ds = condition_to_set(domain, var)
|
|
174
|
+
if (
|
|
175
|
+
isinstance(ds, sp.Interval)
|
|
176
|
+
and ds.start == 0
|
|
177
|
+
and ds.left_open
|
|
178
|
+
and ds.end is S.Infinity
|
|
179
|
+
):
|
|
180
|
+
return True
|
|
181
|
+
try:
|
|
182
|
+
return (
|
|
183
|
+
sp.simplify_logic(sp.Implies(semantic_condition(domain), var > 0)) is S.true
|
|
184
|
+
)
|
|
185
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _special_sign_strata(expr, var, domain):
|
|
190
|
+
if not _is_positive_axis_domain(domain, var):
|
|
191
|
+
return Sign.UNKNOWN
|
|
192
|
+
facts = positive_axis_sign_strata(expr, var)
|
|
193
|
+
if not facts:
|
|
194
|
+
return Sign.UNKNOWN
|
|
195
|
+
out = normalize_cases(
|
|
196
|
+
(ConditionalValue(v, g) for v, g, *_ in facts),
|
|
197
|
+
source="special-function-sign-stratification",
|
|
198
|
+
)
|
|
199
|
+
return out.primary if out.primary is not None else out
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _structural_sign(expr, var, domain):
|
|
203
|
+
if expr.func is sp.exp:
|
|
204
|
+
return Sign.POSITIVE
|
|
205
|
+
if expr.func is sp.gamma:
|
|
206
|
+
ds = condition_to_set(domain, var)
|
|
207
|
+
pos = (
|
|
208
|
+
isinstance(ds, sp.Interval)
|
|
209
|
+
and ds.start.is_nonnegative is True
|
|
210
|
+
and (ds.start != 0 or ds.left_open)
|
|
211
|
+
)
|
|
212
|
+
pos = (
|
|
213
|
+
pos
|
|
214
|
+
or domain == (var > 0)
|
|
215
|
+
or (isinstance(domain, sp.And) and (var > 0) in domain.args)
|
|
216
|
+
)
|
|
217
|
+
if pos:
|
|
218
|
+
return Sign.POSITIVE
|
|
219
|
+
if (
|
|
220
|
+
expr.func is sp.besselk
|
|
221
|
+
and len(expr.args) == 2
|
|
222
|
+
and expr.args[1] == var
|
|
223
|
+
and expr.args[0].is_real is True
|
|
224
|
+
):
|
|
225
|
+
ds = condition_to_set(domain, var)
|
|
226
|
+
pos = (
|
|
227
|
+
isinstance(ds, sp.Interval)
|
|
228
|
+
and ds.start.is_nonnegative is True
|
|
229
|
+
and (ds.start != 0 or ds.left_open)
|
|
230
|
+
)
|
|
231
|
+
pos = (
|
|
232
|
+
pos
|
|
233
|
+
or domain == (var > 0)
|
|
234
|
+
or (isinstance(domain, sp.And) and (var > 0) in domain.args)
|
|
235
|
+
)
|
|
236
|
+
if pos:
|
|
237
|
+
return Sign.POSITIVE
|
|
238
|
+
if expr.func is sp.Abs:
|
|
239
|
+
# Strictness follows only if zero can be excluded.
|
|
240
|
+
return Sign.NONNEGATIVE
|
|
241
|
+
if expr.func is sp.Pow:
|
|
242
|
+
base, e = expr.args
|
|
243
|
+
if e.is_Integer and e.is_even:
|
|
244
|
+
if e.is_negative:
|
|
245
|
+
return Sign.POSITIVE
|
|
246
|
+
return Sign.NONNEGATIVE
|
|
247
|
+
if e.is_Integer and e.is_odd:
|
|
248
|
+
return _function_sign(base, var, domain)
|
|
249
|
+
if expr.func is sp.Mul:
|
|
250
|
+
vals = [_function_sign(a, var, domain) for a in expr.args]
|
|
251
|
+
return _multiply_sign_results(vals)
|
|
252
|
+
return Sign.UNKNOWN
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _sample_sign(value):
|
|
256
|
+
value = sp.simplify(value)
|
|
257
|
+
if value.is_positive is True:
|
|
258
|
+
return 1
|
|
259
|
+
if value.is_negative is True:
|
|
260
|
+
return -1
|
|
261
|
+
if value.is_zero is True:
|
|
262
|
+
return 0
|
|
263
|
+
if value.is_number:
|
|
264
|
+
try:
|
|
265
|
+
n = sp.N(value, 30)
|
|
266
|
+
if n.is_real:
|
|
267
|
+
if n > 0:
|
|
268
|
+
return 1
|
|
269
|
+
if n < 0:
|
|
270
|
+
return -1
|
|
271
|
+
if n == 0:
|
|
272
|
+
return 0
|
|
273
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
274
|
+
pass
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
_exact_order_real_points = exact_order_real_points
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _interval_sign(expr, var, domain):
|
|
282
|
+
"""Certify sign on a connected interval from continuity and exact zeros."""
|
|
283
|
+
ds = condition_to_set(domain, var)
|
|
284
|
+
if not isinstance(ds, sp.Interval) or ds.measure == 0:
|
|
285
|
+
return Sign.UNKNOWN
|
|
286
|
+
try:
|
|
287
|
+
from sympy.calculus.util import continuous_domain
|
|
288
|
+
|
|
289
|
+
if continuous_domain(expr, var, ds) != ds:
|
|
290
|
+
return Sign.UNKNOWN
|
|
291
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
292
|
+
return Sign.UNKNOWN
|
|
293
|
+
try:
|
|
294
|
+
zeros = sp.solveset(sp.Eq(expr, 0), var, domain=ds)
|
|
295
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
296
|
+
return Sign.UNKNOWN
|
|
297
|
+
if zeros is S.EmptySet:
|
|
298
|
+
if ds.start is -sp.oo and ds.end is sp.oo:
|
|
299
|
+
sample = S.Zero
|
|
300
|
+
elif ds.start is -sp.oo:
|
|
301
|
+
sample = ds.end - 1
|
|
302
|
+
elif ds.end is sp.oo:
|
|
303
|
+
sample = ds.start + 1
|
|
304
|
+
else:
|
|
305
|
+
sample = (ds.start + ds.end) / 2
|
|
306
|
+
sg = _sample_sign(expr.subs(var, sample))
|
|
307
|
+
return Sign.POSITIVE if sg == 1 else Sign.NEGATIVE if sg == -1 else Sign.UNKNOWN
|
|
308
|
+
if not isinstance(zeros, sp.FiniteSet):
|
|
309
|
+
return Sign.UNKNOWN
|
|
310
|
+
interior = [z for z in zeros if z in ds and z != ds.start and z != ds.end]
|
|
311
|
+
interior = _exact_order_real_points(interior)
|
|
312
|
+
if interior is None:
|
|
313
|
+
return Sign.UNKNOWN
|
|
314
|
+
pts = [ds.start, *list(interior), ds.end]
|
|
315
|
+
signs = []
|
|
316
|
+
for a, b in itertools.pairwise(pts):
|
|
317
|
+
if a is -sp.oo and b is sp.oo:
|
|
318
|
+
sample = S.Zero
|
|
319
|
+
elif a is -sp.oo:
|
|
320
|
+
sample = b - 1
|
|
321
|
+
elif b is sp.oo:
|
|
322
|
+
sample = a + 1
|
|
323
|
+
else:
|
|
324
|
+
sample = (a + b) / 2
|
|
325
|
+
sg = _sample_sign(expr.subs(var, sample))
|
|
326
|
+
if sg is None:
|
|
327
|
+
return Sign.UNKNOWN
|
|
328
|
+
if sg:
|
|
329
|
+
signs.append(sg)
|
|
330
|
+
if signs and all(s == 1 for s in signs):
|
|
331
|
+
return Sign.NONNEGATIVE
|
|
332
|
+
if signs and all(s == -1 for s in signs):
|
|
333
|
+
return Sign.NONPOSITIVE
|
|
334
|
+
if 1 in signs and -1 in signs:
|
|
335
|
+
return Sign.SIGN_CHANGING
|
|
336
|
+
return Sign.UNKNOWN
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _function_sign(expr, var, domain):
|
|
340
|
+
"""Run the shared exact sign-proof ladder on a requested real domain."""
|
|
341
|
+
expr = sp.sympify(expr)
|
|
342
|
+
if isinstance(expr, sp.Piecewise):
|
|
343
|
+
branches = piecewise_branches(expr, var, domain)
|
|
344
|
+
if branches:
|
|
345
|
+
values = [_function_sign(b.expression, var, b.condition) for b in branches]
|
|
346
|
+
combined = combine_piecewise_sign(values)
|
|
347
|
+
if combined is not Sign.UNKNOWN:
|
|
348
|
+
return combined
|
|
349
|
+
if not expr.has(var):
|
|
350
|
+
s = _constant_sign(expr)
|
|
351
|
+
return _constant_sign_strata(expr) if s is Sign.UNKNOWN else s
|
|
352
|
+
# Structural facts are globally valid on the natural domain.
|
|
353
|
+
s = _structural_sign(expr, var, domain)
|
|
354
|
+
if s is not Sign.UNKNOWN:
|
|
355
|
+
return s
|
|
356
|
+
s = _special_sign_strata(expr, var, domain)
|
|
357
|
+
if s is not Sign.UNKNOWN:
|
|
358
|
+
return s
|
|
359
|
+
s = _registry_sign(expr, var, domain)
|
|
360
|
+
if s is not Sign.UNKNOWN:
|
|
361
|
+
return s
|
|
362
|
+
s = _semialg_sign(expr, var, domain)
|
|
363
|
+
if s is not Sign.UNKNOWN:
|
|
364
|
+
return s
|
|
365
|
+
|
|
366
|
+
# Exact transcendental algebraization: e.g. exp(2*x)-3*exp(x)+2 is
|
|
367
|
+
# a polynomial in u=exp(x), u>0. The transformed problem can then use
|
|
368
|
+
# the semialgebraic/interval proof machinery without treating exp as an
|
|
369
|
+
# opaque numeric function.
|
|
370
|
+
ar = algebraize(expr, var)
|
|
371
|
+
if ar.changed and ar.exact and len(ar.substitutions) == 1:
|
|
372
|
+
u, _ = ar.substitutions[0]
|
|
373
|
+
transformed_domain = ar.constraints
|
|
374
|
+
ts = _function_sign(ar.expression, u, transformed_domain)
|
|
375
|
+
if ts is not Sign.UNKNOWN:
|
|
376
|
+
return ts
|
|
377
|
+
|
|
378
|
+
# Certified root partitioning on a single interval. Unlike a numerical
|
|
379
|
+
# scan, this path is used only when the root engine proves completeness.
|
|
380
|
+
ds = condition_to_set(domain, var)
|
|
381
|
+
if isinstance(ds, sp.Interval):
|
|
382
|
+
rr = transcendental_roots(expr, var, ds, allow_numerical=False)
|
|
383
|
+
if rr.complete and rr.periodic is None:
|
|
384
|
+
return _interval_sign(expr, var, domain)
|
|
385
|
+
return _interval_sign(expr, var, domain)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _function_sign_public_impl(
|
|
389
|
+
expr, var=None, *, domain=True, strict_inequalities=True
|
|
390
|
+
):
|
|
391
|
+
"""Classify the sign of a real expression over its natural/requested domain."""
|
|
392
|
+
expr = sp.sympify(expr)
|
|
393
|
+
if var is None:
|
|
394
|
+
syms = tuple(expr.free_symbols)
|
|
395
|
+
if len(syms) != 1:
|
|
396
|
+
return _constant_sign(expr)
|
|
397
|
+
var = syms[0]
|
|
398
|
+
var = sp.sympify(var)
|
|
399
|
+
natural = function_domain(expr, var)
|
|
400
|
+
assumptions = combine_natural_domain_condition(natural, domain, var)
|
|
401
|
+
split = evaluate_parameter_strata(
|
|
402
|
+
assumptions,
|
|
403
|
+
var,
|
|
404
|
+
lambda branch_domain: _function_sign(expr, var, branch_domain),
|
|
405
|
+
source="sign-parameter-stratification",
|
|
406
|
+
)
|
|
407
|
+
s = split if split is not None else _function_sign(expr, var, assumptions)
|
|
408
|
+
if strict_inequalities:
|
|
409
|
+
return s
|
|
410
|
+
|
|
411
|
+
# Weak query can safely weaken strict classifications, including strata.
|
|
412
|
+
def weaken(v):
|
|
413
|
+
if v is Sign.POSITIVE:
|
|
414
|
+
return Sign.NONNEGATIVE
|
|
415
|
+
if v is Sign.NEGATIVE:
|
|
416
|
+
return Sign.NONPOSITIVE
|
|
417
|
+
return v
|
|
418
|
+
|
|
419
|
+
if isinstance(s, PropertyResult):
|
|
420
|
+
return normalize_cases(
|
|
421
|
+
(ConditionalValue(weaken(c.value), c.condition) for c in s.cases),
|
|
422
|
+
primary=weaken(s.primary) if s.primary is not None else None,
|
|
423
|
+
source=s.source,
|
|
424
|
+
evidence=s.evidence,
|
|
425
|
+
)
|
|
426
|
+
return weaken(s)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _sign_implications(value):
|
|
430
|
+
"""Return weak sign properties implied by an exact sign class."""
|
|
431
|
+
if value is Sign.POSITIVE:
|
|
432
|
+
return (Sign.NONNEGATIVE,)
|
|
433
|
+
if value is Sign.NEGATIVE:
|
|
434
|
+
return (Sign.NONPOSITIVE,)
|
|
435
|
+
if value is Sign.ZERO:
|
|
436
|
+
return (Sign.NONNEGATIVE, Sign.NONPOSITIVE)
|
|
437
|
+
return ()
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def function_sign(
|
|
441
|
+
expr, var=None, *, domain=True, strict_inequalities=True, all_properties=False
|
|
442
|
+
):
|
|
443
|
+
"""Classify sign, optionally returning weaker implied sign properties."""
|
|
444
|
+
expr = sp.sympify(expr)
|
|
445
|
+
if var is None:
|
|
446
|
+
syms = tuple(expr.free_symbols)
|
|
447
|
+
if len(syms) != 1:
|
|
448
|
+
result = _constant_sign(expr)
|
|
449
|
+
return (
|
|
450
|
+
PropertyResult(
|
|
451
|
+
result,
|
|
452
|
+
all_properties=_sign_implications(result),
|
|
453
|
+
source="constant-sign",
|
|
454
|
+
)
|
|
455
|
+
if all_properties
|
|
456
|
+
else result
|
|
457
|
+
)
|
|
458
|
+
var = syms[0]
|
|
459
|
+
from ..proof_cache import property_proof_context
|
|
460
|
+
|
|
461
|
+
result = property_proof_context(expr, var, domain=domain).sign(
|
|
462
|
+
strict_inequalities=strict_inequalities
|
|
463
|
+
)
|
|
464
|
+
if not all_properties:
|
|
465
|
+
return result
|
|
466
|
+
if isinstance(result, PropertyResult):
|
|
467
|
+
return PropertyResult(
|
|
468
|
+
result.primary,
|
|
469
|
+
cases=result.cases,
|
|
470
|
+
all_properties=_sign_implications(result.primary),
|
|
471
|
+
source=result.source,
|
|
472
|
+
exceptions=result.exceptions,
|
|
473
|
+
evidence=result.evidence,
|
|
474
|
+
)
|
|
475
|
+
return PropertyResult(
|
|
476
|
+
result, all_properties=_sign_implications(result), source="sign"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def prove_relation(relation, var=None, *, domain=True):
|
|
481
|
+
"""Prove a scalar real relation by reducing it to the shared sign engine.
|
|
482
|
+
|
|
483
|
+
Equality and order relations are normalized to the sign of ``lhs-rhs``.
|
|
484
|
+
Conditional parameter results are preserved rather than collapsed to an
|
|
485
|
+
unknown Boolean.
|
|
486
|
+
"""
|
|
487
|
+
relation = sp.sympify(relation)
|
|
488
|
+
if relation in (S.true, S.false):
|
|
489
|
+
return TruthValue.TRUE if relation is S.true else TruthValue.FALSE
|
|
490
|
+
if not isinstance(relation, sp.core.relational.Relational):
|
|
491
|
+
raise TypeError("relation must be a SymPy equality or inequality")
|
|
492
|
+
expr = sp.simplify(relation.lhs - relation.rhs)
|
|
493
|
+
sign = function_sign(expr, var, domain=domain)
|
|
494
|
+
|
|
495
|
+
def classify(value):
|
|
496
|
+
if isinstance(relation, sp.StrictGreaterThan):
|
|
497
|
+
return (
|
|
498
|
+
TruthValue.TRUE
|
|
499
|
+
if value is Sign.POSITIVE
|
|
500
|
+
else (
|
|
501
|
+
TruthValue.FALSE
|
|
502
|
+
if value in {Sign.ZERO, Sign.NONPOSITIVE, Sign.NEGATIVE}
|
|
503
|
+
else TruthValue.UNKNOWN
|
|
504
|
+
)
|
|
505
|
+
)
|
|
506
|
+
if isinstance(relation, sp.GreaterThan):
|
|
507
|
+
return (
|
|
508
|
+
TruthValue.TRUE
|
|
509
|
+
if value in {Sign.POSITIVE, Sign.NONNEGATIVE, Sign.ZERO}
|
|
510
|
+
else (
|
|
511
|
+
TruthValue.FALSE if value is Sign.NEGATIVE else TruthValue.UNKNOWN
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
if isinstance(relation, sp.StrictLessThan):
|
|
515
|
+
return (
|
|
516
|
+
TruthValue.TRUE
|
|
517
|
+
if value is Sign.NEGATIVE
|
|
518
|
+
else (
|
|
519
|
+
TruthValue.FALSE
|
|
520
|
+
if value in {Sign.ZERO, Sign.NONNEGATIVE, Sign.POSITIVE}
|
|
521
|
+
else TruthValue.UNKNOWN
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
if isinstance(relation, sp.LessThan):
|
|
525
|
+
return (
|
|
526
|
+
TruthValue.TRUE
|
|
527
|
+
if value in {Sign.NEGATIVE, Sign.NONPOSITIVE, Sign.ZERO}
|
|
528
|
+
else (
|
|
529
|
+
TruthValue.FALSE if value is Sign.POSITIVE else TruthValue.UNKNOWN
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
if isinstance(relation, sp.Equality):
|
|
533
|
+
return (
|
|
534
|
+
TruthValue.TRUE
|
|
535
|
+
if value is Sign.ZERO
|
|
536
|
+
else (
|
|
537
|
+
TruthValue.FALSE
|
|
538
|
+
if value in {Sign.POSITIVE, Sign.NEGATIVE, Sign.SIGN_CHANGING}
|
|
539
|
+
else TruthValue.UNKNOWN
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
if isinstance(relation, sp.Unequality):
|
|
543
|
+
return (
|
|
544
|
+
TruthValue.FALSE
|
|
545
|
+
if value is Sign.ZERO
|
|
546
|
+
else (
|
|
547
|
+
TruthValue.TRUE
|
|
548
|
+
if value in {Sign.POSITIVE, Sign.NEGATIVE}
|
|
549
|
+
else TruthValue.UNKNOWN
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
return TruthValue.UNKNOWN
|
|
553
|
+
|
|
554
|
+
if isinstance(sign, PropertyResult):
|
|
555
|
+
return normalize_cases(
|
|
556
|
+
(ConditionalValue(classify(c.value), c.condition) for c in sign.cases),
|
|
557
|
+
primary=classify(sign.primary) if sign.primary is not None else None,
|
|
558
|
+
source="relation-from-sign",
|
|
559
|
+
evidence=sign.evidence,
|
|
560
|
+
)
|
|
561
|
+
return classify(sign)
|