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,494 @@
|
|
|
1
|
+
"""Shared parameter-stratification and three-valued proof helpers.
|
|
2
|
+
|
|
3
|
+
This module centralizes parameter-case normalization, semantic-predicate
|
|
4
|
+
lowering, and exact set comparisons so every property engine uses the same
|
|
5
|
+
conditional-result semantics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
11
|
+
from collections.abc import Callable, Iterable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import sympy as sp
|
|
15
|
+
from sympy import S
|
|
16
|
+
|
|
17
|
+
from .conditions import (
|
|
18
|
+
AAlgebraicQ,
|
|
19
|
+
AEqualQ,
|
|
20
|
+
AEvenQ,
|
|
21
|
+
AIntegerQ,
|
|
22
|
+
ANegative,
|
|
23
|
+
ANonIntegerQ,
|
|
24
|
+
ANonNegative,
|
|
25
|
+
ANonPositive,
|
|
26
|
+
AOddQ,
|
|
27
|
+
APositive,
|
|
28
|
+
ARationalQ,
|
|
29
|
+
ARealIfDefinedQ,
|
|
30
|
+
ARealQ,
|
|
31
|
+
AUnequalQ,
|
|
32
|
+
)
|
|
33
|
+
from .engine import simplify_condition
|
|
34
|
+
from .exceptions import SIMPLIFICATION_ERRORS, SYMBOLIC_QUERY_ERRORS
|
|
35
|
+
from .results import ConditionalValue, PropertyResult, TruthValue
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def semantic_condition(condition):
|
|
39
|
+
"""Lower catalogue semantic predicates to ordinary SymPy predicates.
|
|
40
|
+
|
|
41
|
+
This is deliberately one-way. Native inequalities/membership expressions
|
|
42
|
+
are much easier for ``simplify_logic``, ``refine`` and set conversion to
|
|
43
|
+
reason about, while the catalogue parser can continue to preserve source
|
|
44
|
+
semantics internally.
|
|
45
|
+
"""
|
|
46
|
+
c = sp.sympify(condition)
|
|
47
|
+
|
|
48
|
+
def repl(node):
|
|
49
|
+
if not isinstance(node, sp.Basic):
|
|
50
|
+
return node
|
|
51
|
+
f = node.func
|
|
52
|
+
a = node.args
|
|
53
|
+
if f is APositive and len(a) == 1:
|
|
54
|
+
return a[0] > 0
|
|
55
|
+
if f is ANegative and len(a) == 1:
|
|
56
|
+
return a[0] < 0
|
|
57
|
+
if f is ANonPositive and len(a) == 1:
|
|
58
|
+
return a[0] <= 0
|
|
59
|
+
if f is ANonNegative and len(a) == 1:
|
|
60
|
+
return a[0] >= 0
|
|
61
|
+
if f is AIntegerQ and len(a) == 1:
|
|
62
|
+
return sp.Contains(a[0], S.Integers)
|
|
63
|
+
if f is ANonIntegerQ and len(a) == 1:
|
|
64
|
+
return sp.Not(sp.Contains(a[0], S.Integers))
|
|
65
|
+
if f is ARationalQ and len(a) == 1:
|
|
66
|
+
return sp.Contains(a[0], S.Rationals)
|
|
67
|
+
if f in (ARealQ, ARealIfDefinedQ) and len(a) == 1:
|
|
68
|
+
return sp.Contains(a[0], S.Reals)
|
|
69
|
+
if f is AAlgebraicQ and len(a) == 1:
|
|
70
|
+
# SymPy has no global Algebraics singleton that behaves as cleanly
|
|
71
|
+
# as Reals/Integers in Boolean simplification. Preserve this one.
|
|
72
|
+
return node
|
|
73
|
+
if f is AEvenQ and len(a) == 1:
|
|
74
|
+
return sp.And(sp.Contains(a[0], S.Integers), sp.Eq(sp.Mod(a[0], 2), 0))
|
|
75
|
+
if f is AOddQ and len(a) == 1:
|
|
76
|
+
return sp.And(sp.Contains(a[0], S.Integers), sp.Eq(sp.Mod(a[0], 2), 1))
|
|
77
|
+
if f is AUnequalQ and len(a) == 2:
|
|
78
|
+
return sp.Ne(a[0], a[1])
|
|
79
|
+
if f is AEqualQ and len(a) == 2:
|
|
80
|
+
return sp.Eq(a[0], a[1])
|
|
81
|
+
return node
|
|
82
|
+
|
|
83
|
+
with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
|
|
84
|
+
c = c.replace(
|
|
85
|
+
lambda n: (
|
|
86
|
+
isinstance(n, sp.Basic)
|
|
87
|
+
and n.func
|
|
88
|
+
in {
|
|
89
|
+
APositive,
|
|
90
|
+
ANegative,
|
|
91
|
+
ANonPositive,
|
|
92
|
+
ANonNegative,
|
|
93
|
+
AIntegerQ,
|
|
94
|
+
ANonIntegerQ,
|
|
95
|
+
ARationalQ,
|
|
96
|
+
ARealQ,
|
|
97
|
+
ARealIfDefinedQ,
|
|
98
|
+
AAlgebraicQ,
|
|
99
|
+
AEvenQ,
|
|
100
|
+
AOddQ,
|
|
101
|
+
AUnequalQ,
|
|
102
|
+
AEqualQ,
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
repl,
|
|
106
|
+
)
|
|
107
|
+
with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
|
|
108
|
+
c = sp.refine(c)
|
|
109
|
+
with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
|
|
110
|
+
c = sp.simplify_logic(c)
|
|
111
|
+
return c
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def parameter_symbols(expr, var=None, *extra):
|
|
115
|
+
syms = set(sp.sympify(expr).free_symbols)
|
|
116
|
+
for obj in extra:
|
|
117
|
+
with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
|
|
118
|
+
syms.update(sp.sympify(obj).free_symbols)
|
|
119
|
+
if var is not None:
|
|
120
|
+
syms.discard(sp.sympify(var))
|
|
121
|
+
return tuple(sorted(syms, key=sp.default_sort_key))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def normalize_cases(
|
|
125
|
+
cases: Iterable[ConditionalValue],
|
|
126
|
+
*,
|
|
127
|
+
source=None,
|
|
128
|
+
primary=None,
|
|
129
|
+
all_properties=(),
|
|
130
|
+
evidence=(),
|
|
131
|
+
):
|
|
132
|
+
"""Canonicalize conditional cases and merge equal-valued strata."""
|
|
133
|
+
merged: list[tuple[Any, sp.Basic]] = []
|
|
134
|
+
for item in cases:
|
|
135
|
+
cond = semantic_condition(simplify_condition(item.condition))
|
|
136
|
+
if cond is S.false:
|
|
137
|
+
continue
|
|
138
|
+
found = False
|
|
139
|
+
for i, (value, old) in enumerate(merged):
|
|
140
|
+
if value == item.value:
|
|
141
|
+
try:
|
|
142
|
+
new = sp.simplify(sp.simplify_logic(sp.Or(old, cond)))
|
|
143
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
144
|
+
new = sp.Or(old, cond)
|
|
145
|
+
merged[i] = (value, new)
|
|
146
|
+
found = True
|
|
147
|
+
break
|
|
148
|
+
if not found:
|
|
149
|
+
merged.append((item.value, cond))
|
|
150
|
+
|
|
151
|
+
# Truth-valued strata are mutually exclusive claims. If independently
|
|
152
|
+
# derived TRUE and FALSE guards overlap, make that overlap explicitly
|
|
153
|
+
# UNKNOWN instead of letting insertion order decide the answer.
|
|
154
|
+
true_cond = S.false
|
|
155
|
+
false_cond = S.false
|
|
156
|
+
for value, cond in merged:
|
|
157
|
+
if value is TruthValue.TRUE:
|
|
158
|
+
true_cond = sp.Or(true_cond, cond)
|
|
159
|
+
elif value is TruthValue.FALSE:
|
|
160
|
+
false_cond = sp.Or(false_cond, cond)
|
|
161
|
+
conflict = S.false
|
|
162
|
+
if true_cond is not S.false and false_cond is not S.false:
|
|
163
|
+
try:
|
|
164
|
+
conflict = sp.simplify(sp.simplify_logic(sp.And(true_cond, false_cond)))
|
|
165
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
166
|
+
conflict = sp.And(true_cond, false_cond)
|
|
167
|
+
if conflict is not S.false and not _guard_satisfiable(conflict):
|
|
168
|
+
conflict = S.false
|
|
169
|
+
if conflict is not S.false:
|
|
170
|
+
adjusted = []
|
|
171
|
+
for value, cond in merged:
|
|
172
|
+
if value in (TruthValue.TRUE, TruthValue.FALSE):
|
|
173
|
+
try:
|
|
174
|
+
cond = sp.simplify(
|
|
175
|
+
sp.simplify_logic(sp.And(cond, sp.Not(conflict)))
|
|
176
|
+
)
|
|
177
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
178
|
+
cond = sp.And(cond, sp.Not(conflict))
|
|
179
|
+
if cond is not S.false:
|
|
180
|
+
adjusted.append((value, cond))
|
|
181
|
+
merged = [(TruthValue.UNKNOWN, conflict), *adjusted]
|
|
182
|
+
|
|
183
|
+
out: list[ConditionalValue] = []
|
|
184
|
+
covered = S.false
|
|
185
|
+
for value, cond in merged:
|
|
186
|
+
try:
|
|
187
|
+
effective = sp.simplify(sp.simplify_logic(sp.And(cond, sp.Not(covered))))
|
|
188
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
189
|
+
effective = sp.And(cond, sp.Not(covered))
|
|
190
|
+
if effective is S.false:
|
|
191
|
+
continue
|
|
192
|
+
out.append(ConditionalValue(value, effective))
|
|
193
|
+
try:
|
|
194
|
+
covered = sp.simplify(sp.simplify_logic(sp.Or(covered, cond)))
|
|
195
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
196
|
+
covered = sp.Or(covered, cond)
|
|
197
|
+
|
|
198
|
+
if primary is None:
|
|
199
|
+
for c in out:
|
|
200
|
+
if c.condition is S.true:
|
|
201
|
+
primary = c.value
|
|
202
|
+
break
|
|
203
|
+
return PropertyResult(
|
|
204
|
+
primary=primary,
|
|
205
|
+
cases=tuple(out),
|
|
206
|
+
all_properties=tuple(all_properties),
|
|
207
|
+
source=source,
|
|
208
|
+
evidence=tuple(evidence),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def map_conditional(value, mapper: Callable[[Any], Any], *, source=None):
|
|
213
|
+
if not isinstance(value, PropertyResult):
|
|
214
|
+
return mapper(value)
|
|
215
|
+
cases = [ConditionalValue(mapper(c.value), c.condition) for c in value.cases]
|
|
216
|
+
primary = mapper(value.primary) if value.primary is not None else None
|
|
217
|
+
return normalize_cases(
|
|
218
|
+
cases, primary=primary, source=source or value.source, evidence=value.evidence
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def implication_guard(domain, locus, var):
|
|
223
|
+
"""Return a parameter-only sufficient guard for ``domain => locus``.
|
|
224
|
+
|
|
225
|
+
``None`` means that the implication could not be reduced without the
|
|
226
|
+
function variable. The returned guard is exact for the structural forms
|
|
227
|
+
handled here; we never existentially sample parameters.
|
|
228
|
+
"""
|
|
229
|
+
var = sp.sympify(var)
|
|
230
|
+
d = semantic_condition(domain)
|
|
231
|
+
locus_cond = semantic_condition(locus)
|
|
232
|
+
try:
|
|
233
|
+
imp = sp.simplify_logic(sp.Implies(d, locus_cond))
|
|
234
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
235
|
+
imp = sp.Implies(d, locus_cond)
|
|
236
|
+
with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
|
|
237
|
+
imp = sp.refine(imp, d)
|
|
238
|
+
with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
|
|
239
|
+
imp = sp.simplify_logic(imp)
|
|
240
|
+
if not getattr(imp, "has", lambda *_: False)(var):
|
|
241
|
+
return imp
|
|
242
|
+
|
|
243
|
+
# Common catalogue form: conjunction of domain facts and parameter facts.
|
|
244
|
+
if isinstance(locus_cond, sp.And):
|
|
245
|
+
residual = []
|
|
246
|
+
for term in locus_cond.args:
|
|
247
|
+
try:
|
|
248
|
+
test = sp.simplify_logic(sp.Implies(d, term))
|
|
249
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
250
|
+
test = sp.Implies(d, term)
|
|
251
|
+
if test is S.true:
|
|
252
|
+
continue
|
|
253
|
+
if not term.has(var):
|
|
254
|
+
residual.append(term)
|
|
255
|
+
else:
|
|
256
|
+
return None
|
|
257
|
+
return sp.And(*residual) if residual else S.true
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def exact_set_equal(a, b):
|
|
262
|
+
"""Three-valued exact set equality test."""
|
|
263
|
+
if a == b:
|
|
264
|
+
return TruthValue.TRUE
|
|
265
|
+
if not isinstance(a, sp.Set) or not isinstance(b, sp.Set):
|
|
266
|
+
return TruthValue.UNKNOWN
|
|
267
|
+
try:
|
|
268
|
+
diff = a.symmetric_difference(b)
|
|
269
|
+
if diff is S.EmptySet or diff == S.EmptySet:
|
|
270
|
+
return TruthValue.TRUE
|
|
271
|
+
# Nonempty finite/interval/union differences are exact disproofs.
|
|
272
|
+
if isinstance(diff, (sp.FiniteSet, sp.Interval)):
|
|
273
|
+
return TruthValue.FALSE
|
|
274
|
+
emp = getattr(diff, "is_empty", None)
|
|
275
|
+
if emp is True:
|
|
276
|
+
return TruthValue.TRUE
|
|
277
|
+
if emp is False:
|
|
278
|
+
return TruthValue.FALSE
|
|
279
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
280
|
+
pass
|
|
281
|
+
try:
|
|
282
|
+
ab = a.is_subset(b)
|
|
283
|
+
ba = b.is_subset(a)
|
|
284
|
+
if ab is True and ba is True:
|
|
285
|
+
return TruthValue.TRUE
|
|
286
|
+
if ab is False or ba is False:
|
|
287
|
+
return TruthValue.FALSE
|
|
288
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
289
|
+
pass
|
|
290
|
+
return TruthValue.UNKNOWN
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def combine_truth_results(a, b, *, op="and", source="combined"):
|
|
294
|
+
"""Combine scalar/conditional TruthValue results without losing strata."""
|
|
295
|
+
|
|
296
|
+
def scalar(x, y):
|
|
297
|
+
if op == "and":
|
|
298
|
+
if x is TruthValue.FALSE or y is TruthValue.FALSE:
|
|
299
|
+
return TruthValue.FALSE
|
|
300
|
+
if x is TruthValue.TRUE and y is TruthValue.TRUE:
|
|
301
|
+
return TruthValue.TRUE
|
|
302
|
+
return TruthValue.UNKNOWN
|
|
303
|
+
if x is TruthValue.TRUE or y is TruthValue.TRUE:
|
|
304
|
+
return TruthValue.TRUE
|
|
305
|
+
if x is TruthValue.FALSE and y is TruthValue.FALSE:
|
|
306
|
+
return TruthValue.FALSE
|
|
307
|
+
return TruthValue.UNKNOWN
|
|
308
|
+
|
|
309
|
+
if not isinstance(a, PropertyResult) and not isinstance(b, PropertyResult):
|
|
310
|
+
return scalar(a, b)
|
|
311
|
+
acases = (
|
|
312
|
+
a.cases if isinstance(a, PropertyResult) else (ConditionalValue(a, S.true),)
|
|
313
|
+
)
|
|
314
|
+
bcases = (
|
|
315
|
+
b.cases if isinstance(b, PropertyResult) else (ConditionalValue(b, S.true),)
|
|
316
|
+
)
|
|
317
|
+
cases = []
|
|
318
|
+
for ca in acases:
|
|
319
|
+
for cb in bcases:
|
|
320
|
+
try:
|
|
321
|
+
cond = sp.simplify_logic(sp.And(ca.condition, cb.condition))
|
|
322
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
323
|
+
cond = sp.And(ca.condition, cb.condition)
|
|
324
|
+
if cond is not S.false:
|
|
325
|
+
cases.append(ConditionalValue(scalar(ca.value, cb.value), cond))
|
|
326
|
+
return normalize_cases(cases, source=source)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _guard_satisfiable(guard):
|
|
330
|
+
"""Return False only when a parameter guard is certified impossible.
|
|
331
|
+
|
|
332
|
+
SymPy's Boolean satisfier is propositional for inequalities, so polynomial
|
|
333
|
+
real guards are sent to semialg first. Unsupported guards fall back to
|
|
334
|
+
conservative propositional reasoning.
|
|
335
|
+
"""
|
|
336
|
+
guard = semantic_condition(guard)
|
|
337
|
+
if guard is S.false:
|
|
338
|
+
return False
|
|
339
|
+
if guard is S.true:
|
|
340
|
+
return True
|
|
341
|
+
variables = tuple(sorted(guard.free_symbols, key=sp.default_sort_key))
|
|
342
|
+
if variables:
|
|
343
|
+
try:
|
|
344
|
+
from . import _semialg
|
|
345
|
+
|
|
346
|
+
return _semialg.is_satisfiable(guard, variables)
|
|
347
|
+
except (
|
|
348
|
+
_semialg.SemialgUnavailable,
|
|
349
|
+
TypeError,
|
|
350
|
+
ValueError,
|
|
351
|
+
NotImplementedError,
|
|
352
|
+
ArithmeticError,
|
|
353
|
+
):
|
|
354
|
+
pass
|
|
355
|
+
try:
|
|
356
|
+
return sp.satisfiable(guard) is not False
|
|
357
|
+
except SIMPLIFICATION_ERRORS:
|
|
358
|
+
return True
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def stratify_boolean_by_parameters(
|
|
362
|
+
condition, var, *, max_atoms=8, source="boolean-parameter-stratification"
|
|
363
|
+
):
|
|
364
|
+
"""Split a Boolean condition into parameter guards and var-only values.
|
|
365
|
+
|
|
366
|
+
This is exact for the finite Boolean atom partition it constructs. It is
|
|
367
|
+
especially useful for catalogue domains stored as a single mixed formula,
|
|
368
|
+
e.g. ``IntegerQ(nu) | x>0 | (nu>=0 & x>=0)``.
|
|
369
|
+
"""
|
|
370
|
+
var = sp.sympify(var)
|
|
371
|
+
c = semantic_condition(condition)
|
|
372
|
+
# Parameter-only Boolean atoms. Do not split arbitrary function-valued
|
|
373
|
+
# endpoint expressions such as gamma(a); only predicates/relations matter.
|
|
374
|
+
try:
|
|
375
|
+
atoms = set(c.atoms(sp.core.relational.Relational, sp.Contains))
|
|
376
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
377
|
+
atoms = set()
|
|
378
|
+
atoms = {a for a in atoms if not a.has(var)}
|
|
379
|
+
atoms = tuple(sorted(atoms, key=sp.default_sort_key))
|
|
380
|
+
if not atoms or len(atoms) > max_atoms:
|
|
381
|
+
return PropertyResult(primary=c, source=source)
|
|
382
|
+
cases = []
|
|
383
|
+
from itertools import product
|
|
384
|
+
|
|
385
|
+
for bits in product((False, True), repeat=len(atoms)):
|
|
386
|
+
guard_terms = []
|
|
387
|
+
repl = {}
|
|
388
|
+
for a, b in zip(atoms, bits, strict=False):
|
|
389
|
+
repl[a] = S.true if b else S.false
|
|
390
|
+
guard_terms.append(a if b else sp.Not(a))
|
|
391
|
+
guard = semantic_condition(sp.And(*guard_terms))
|
|
392
|
+
if not _guard_satisfiable(guard):
|
|
393
|
+
continue
|
|
394
|
+
try:
|
|
395
|
+
value = c.xreplace(repl)
|
|
396
|
+
value = semantic_condition(value)
|
|
397
|
+
with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
|
|
398
|
+
value = sp.simplify(value)
|
|
399
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
400
|
+
value = c
|
|
401
|
+
# If parameters remain in predicates, this partition was insufficient.
|
|
402
|
+
params = set(value.free_symbols)
|
|
403
|
+
params.discard(var)
|
|
404
|
+
if params:
|
|
405
|
+
continue
|
|
406
|
+
cases.append(ConditionalValue(value, guard))
|
|
407
|
+
if not cases:
|
|
408
|
+
return PropertyResult(primary=c, source=source)
|
|
409
|
+
return normalize_cases(cases, source=source)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def result_cases(result):
|
|
413
|
+
"""Return a uniform conditional-case view of a scalar or PropertyResult."""
|
|
414
|
+
if isinstance(result, PropertyResult):
|
|
415
|
+
if result.cases:
|
|
416
|
+
return result.cases
|
|
417
|
+
if result.primary is not None:
|
|
418
|
+
return (ConditionalValue(result.primary, S.true),)
|
|
419
|
+
return ()
|
|
420
|
+
return (ConditionalValue(result, S.true),)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def guarded_result(result, guard, *, source=None):
|
|
424
|
+
"""Attach a parameter guard to every case in a property result."""
|
|
425
|
+
guard = semantic_condition(guard)
|
|
426
|
+
cases = []
|
|
427
|
+
for case in result_cases(result):
|
|
428
|
+
try:
|
|
429
|
+
cond = sp.simplify_logic(sp.And(guard, case.condition))
|
|
430
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
431
|
+
cond = sp.And(guard, case.condition)
|
|
432
|
+
if cond is not S.false:
|
|
433
|
+
cases.append(ConditionalValue(case.value, cond))
|
|
434
|
+
if not cases:
|
|
435
|
+
return PropertyResult(primary=None, source=source)
|
|
436
|
+
evidence = result.evidence if isinstance(result, PropertyResult) else ()
|
|
437
|
+
exceptions = result.exceptions if isinstance(result, PropertyResult) else ()
|
|
438
|
+
all_props = result.all_properties if isinstance(result, PropertyResult) else ()
|
|
439
|
+
out = normalize_cases(
|
|
440
|
+
cases,
|
|
441
|
+
source=source or getattr(result, "source", None),
|
|
442
|
+
all_properties=all_props,
|
|
443
|
+
evidence=evidence,
|
|
444
|
+
)
|
|
445
|
+
if exceptions:
|
|
446
|
+
return PropertyResult(
|
|
447
|
+
out.primary,
|
|
448
|
+
cases=out.cases,
|
|
449
|
+
all_properties=out.all_properties,
|
|
450
|
+
source=out.source,
|
|
451
|
+
exceptions=exceptions,
|
|
452
|
+
evidence=out.evidence,
|
|
453
|
+
)
|
|
454
|
+
return out
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def evaluate_parameter_strata(
|
|
458
|
+
condition,
|
|
459
|
+
var,
|
|
460
|
+
evaluator: Callable[[sp.Basic], Any],
|
|
461
|
+
*,
|
|
462
|
+
source="parameter-stratification",
|
|
463
|
+
max_atoms=8,
|
|
464
|
+
):
|
|
465
|
+
"""Evaluate one property separately on exact parameter-domain strata.
|
|
466
|
+
|
|
467
|
+
``condition`` may mix the function variable with parameter-only Boolean
|
|
468
|
+
atoms. When the finite parameter partition is exact, ``evaluator`` is
|
|
469
|
+
called on each resulting variable-only domain and nested conditional
|
|
470
|
+
results are flattened. ``None`` means there was no useful split.
|
|
471
|
+
"""
|
|
472
|
+
split = stratify_boolean_by_parameters(
|
|
473
|
+
condition, var, max_atoms=max_atoms, source=source
|
|
474
|
+
)
|
|
475
|
+
if not split.cases or (
|
|
476
|
+
len(split.cases) == 1 and split.cases[0].condition is S.true
|
|
477
|
+
):
|
|
478
|
+
return None
|
|
479
|
+
cases = []
|
|
480
|
+
evidence = []
|
|
481
|
+
for domain_case in split.cases:
|
|
482
|
+
value = evaluator(domain_case.value)
|
|
483
|
+
if isinstance(value, PropertyResult):
|
|
484
|
+
evidence.extend(value.evidence)
|
|
485
|
+
for item in result_cases(value):
|
|
486
|
+
try:
|
|
487
|
+
guard = sp.simplify_logic(sp.And(domain_case.condition, item.condition))
|
|
488
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
489
|
+
guard = sp.And(domain_case.condition, item.condition)
|
|
490
|
+
if guard is not S.false:
|
|
491
|
+
cases.append(ConditionalValue(item.value, guard))
|
|
492
|
+
if not cases:
|
|
493
|
+
return None
|
|
494
|
+
return normalize_cases(cases, source=source, evidence=tuple(evidence))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
from sympy import S
|
|
7
|
+
from sympy.calculus.util import continuous_domain
|
|
8
|
+
|
|
9
|
+
from .conditions import parse_catalogue_expression
|
|
10
|
+
from .exceptions import SYMBOLIC_QUERY_ERRORS
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ComparisonStatus(str, Enum):
|
|
14
|
+
AGREE = "agree"
|
|
15
|
+
DISAGREE = "disagree"
|
|
16
|
+
SYMPY_UNKNOWN = "sympy_unknown"
|
|
17
|
+
NOT_COMPARABLE = "not_comparable"
|
|
18
|
+
ERROR = "error"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Comparison:
|
|
23
|
+
status: ComparisonStatus
|
|
24
|
+
registry_value: object
|
|
25
|
+
sympy_value: object = None
|
|
26
|
+
detail: str = ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def compare_real_domain(expr, var, registry_condition):
|
|
30
|
+
"""Compare a catalogue real-domain rule with SymPy when both are expressible.
|
|
31
|
+
|
|
32
|
+
SymPy's continuous_domain returns a Set while catalogue rules are Boolean
|
|
33
|
+
conditions, so exact equality is only asserted for readily convertible cases.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
s = continuous_domain(expr, var, S.Reals)
|
|
37
|
+
except NotImplementedError as e:
|
|
38
|
+
return Comparison(
|
|
39
|
+
ComparisonStatus.SYMPY_UNKNOWN, registry_condition, None, str(e)
|
|
40
|
+
)
|
|
41
|
+
except SYMBOLIC_QUERY_ERRORS as e:
|
|
42
|
+
return Comparison(ComparisonStatus.ERROR, registry_condition, None, str(e))
|
|
43
|
+
c = (
|
|
44
|
+
parse_catalogue_expression(registry_condition)
|
|
45
|
+
if isinstance(registry_condition, str)
|
|
46
|
+
else registry_condition
|
|
47
|
+
)
|
|
48
|
+
if c is S.true:
|
|
49
|
+
return Comparison(
|
|
50
|
+
ComparisonStatus.AGREE if s == S.Reals else ComparisonStatus.DISAGREE, c, s
|
|
51
|
+
)
|
|
52
|
+
if c is S.false:
|
|
53
|
+
return Comparison(
|
|
54
|
+
ComparisonStatus.AGREE if s == S.EmptySet else ComparisonStatus.DISAGREE,
|
|
55
|
+
c,
|
|
56
|
+
s,
|
|
57
|
+
)
|
|
58
|
+
return Comparison(
|
|
59
|
+
ComparisonStatus.NOT_COMPARABLE,
|
|
60
|
+
c,
|
|
61
|
+
s,
|
|
62
|
+
"Boolean-condition to Set equivalence requires quantifier/set normalization",
|
|
63
|
+
)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from .algebraize import (
|
|
2
|
+
AlgebraizationResult,
|
|
3
|
+
algebraize,
|
|
4
|
+
algebraize_common_subexpressions,
|
|
5
|
+
algebraize_exponentials,
|
|
6
|
+
algebraize_trig_circle,
|
|
7
|
+
algebraize_trigonometric,
|
|
8
|
+
rewrite_hyperbolic_to_exp,
|
|
9
|
+
)
|
|
10
|
+
from .analytic import (
|
|
11
|
+
AnalyticNeighborhoodCertificate,
|
|
12
|
+
analytic_neighborhood,
|
|
13
|
+
holomorphic_on_rectangle,
|
|
14
|
+
)
|
|
15
|
+
from .complex_roots import (
|
|
16
|
+
ArgumentPrincipleCertificate,
|
|
17
|
+
CertifiedComplexRootRectangle,
|
|
18
|
+
ComplexRectangle,
|
|
19
|
+
ComplexRootIsolationResult,
|
|
20
|
+
argument_principle_count,
|
|
21
|
+
isolate_complex_roots,
|
|
22
|
+
)
|
|
23
|
+
from .exp_fourier import (
|
|
24
|
+
ExponentialFourierCount,
|
|
25
|
+
exponential_fourier_count,
|
|
26
|
+
exponential_polynomial_terms,
|
|
27
|
+
)
|
|
28
|
+
from .extrema import (
|
|
29
|
+
ExtremumEnvelope,
|
|
30
|
+
ExtremumFamilyCertificate,
|
|
31
|
+
ExtremumOrderingTheorem,
|
|
32
|
+
ExtremumRange,
|
|
33
|
+
IndexedExtremumValue,
|
|
34
|
+
extremum_geometry,
|
|
35
|
+
extremum_ordering_theorems,
|
|
36
|
+
simplify_extremum_range,
|
|
37
|
+
symbolic_extremum_range,
|
|
38
|
+
)
|
|
39
|
+
from .periodic import (
|
|
40
|
+
PeriodicCondition,
|
|
41
|
+
PeriodicSet,
|
|
42
|
+
common_period_values,
|
|
43
|
+
common_positive_period,
|
|
44
|
+
positive_period,
|
|
45
|
+
)
|
|
46
|
+
from .proof import (
|
|
47
|
+
PerformanceGoal,
|
|
48
|
+
ProofPolicy,
|
|
49
|
+
condition_to_intervals,
|
|
50
|
+
extended_real_sign,
|
|
51
|
+
find_parameters,
|
|
52
|
+
)
|
|
53
|
+
from .real_sign import robust_real_sign
|
|
54
|
+
from .reduce import UnivariateReductionResult, reduce_univariate_condition
|
|
55
|
+
from .roots import (
|
|
56
|
+
CertifiedRootInterval,
|
|
57
|
+
ParametricZeroFamily,
|
|
58
|
+
RootCertificate,
|
|
59
|
+
RootIsolationResult,
|
|
60
|
+
transcendental_roots,
|
|
61
|
+
)
|
|
62
|
+
from .zero_geometry import (
|
|
63
|
+
CertifiedIntegerBracket,
|
|
64
|
+
IndexedTailIndex,
|
|
65
|
+
IndexedTailInversionCertificate,
|
|
66
|
+
IndexedZero,
|
|
67
|
+
IntervalPropertyCell,
|
|
68
|
+
PropertyIntervalDecomposition,
|
|
69
|
+
ZeroGeometryCertificate,
|
|
70
|
+
ZeroInterlacing,
|
|
71
|
+
ZeroOrdering,
|
|
72
|
+
indexed_tail_integer_bracket,
|
|
73
|
+
indexed_tail_inversion,
|
|
74
|
+
property_interval_decomposition,
|
|
75
|
+
zero_geometry,
|
|
76
|
+
zero_ordering_bounds,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"AlgebraizationResult",
|
|
81
|
+
"AnalyticNeighborhoodCertificate",
|
|
82
|
+
"ArgumentPrincipleCertificate",
|
|
83
|
+
"CertifiedComplexRootRectangle",
|
|
84
|
+
"CertifiedIntegerBracket",
|
|
85
|
+
"CertifiedRootInterval",
|
|
86
|
+
"ComplexRectangle",
|
|
87
|
+
"ComplexRootIsolationResult",
|
|
88
|
+
"ExponentialFourierCount",
|
|
89
|
+
"ExtremumEnvelope",
|
|
90
|
+
"ExtremumFamilyCertificate",
|
|
91
|
+
"ExtremumOrderingTheorem",
|
|
92
|
+
"ExtremumRange",
|
|
93
|
+
"IndexedExtremumValue",
|
|
94
|
+
"IndexedTailIndex",
|
|
95
|
+
"IndexedTailInversionCertificate",
|
|
96
|
+
"IndexedZero",
|
|
97
|
+
"IntervalPropertyCell",
|
|
98
|
+
"ParametricZeroFamily",
|
|
99
|
+
"PerformanceGoal",
|
|
100
|
+
"PeriodicCondition",
|
|
101
|
+
"PeriodicSet",
|
|
102
|
+
"ProofPolicy",
|
|
103
|
+
"PropertyIntervalDecomposition",
|
|
104
|
+
"RootCertificate",
|
|
105
|
+
"RootIsolationResult",
|
|
106
|
+
"UnivariateReductionResult",
|
|
107
|
+
"ZeroGeometryCertificate",
|
|
108
|
+
"ZeroInterlacing",
|
|
109
|
+
"ZeroOrdering",
|
|
110
|
+
"algebraize",
|
|
111
|
+
"algebraize_common_subexpressions",
|
|
112
|
+
"algebraize_exponentials",
|
|
113
|
+
"algebraize_trig_circle",
|
|
114
|
+
"algebraize_trigonometric",
|
|
115
|
+
"analytic_neighborhood",
|
|
116
|
+
"argument_principle_count",
|
|
117
|
+
"common_period_values",
|
|
118
|
+
"common_positive_period",
|
|
119
|
+
"condition_to_intervals",
|
|
120
|
+
"exponential_fourier_count",
|
|
121
|
+
"exponential_polynomial_terms",
|
|
122
|
+
"extended_real_sign",
|
|
123
|
+
"extremum_geometry",
|
|
124
|
+
"extremum_ordering_theorems",
|
|
125
|
+
"find_parameters",
|
|
126
|
+
"holomorphic_on_rectangle",
|
|
127
|
+
"indexed_tail_integer_bracket",
|
|
128
|
+
"indexed_tail_inversion",
|
|
129
|
+
"isolate_complex_roots",
|
|
130
|
+
"positive_period",
|
|
131
|
+
"property_interval_decomposition",
|
|
132
|
+
"reduce_univariate_condition",
|
|
133
|
+
"rewrite_hyperbolic_to_exp",
|
|
134
|
+
"robust_real_sign",
|
|
135
|
+
"simplify_extremum_range",
|
|
136
|
+
"symbolic_extremum_range",
|
|
137
|
+
"transcendental_roots",
|
|
138
|
+
"zero_geometry",
|
|
139
|
+
"zero_ordering_bounds",
|
|
140
|
+
]
|