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,461 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from sympy import And, I, Not, Or, S, acosh, asec, asech, pi, simplify, sympify
|
|
9
|
+
from sympy.core.relational import (
|
|
10
|
+
Equality,
|
|
11
|
+
GreaterThan,
|
|
12
|
+
LessThan,
|
|
13
|
+
Relational,
|
|
14
|
+
StrictGreaterThan,
|
|
15
|
+
StrictLessThan,
|
|
16
|
+
)
|
|
17
|
+
from sympy.sets.contains import Contains
|
|
18
|
+
|
|
19
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
20
|
+
from .computation import clean_pairs
|
|
21
|
+
from .core import RestrictedExpr, restrict
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _validate_pair_sequence(pairs, func_name="pair-processing function"):
|
|
25
|
+
if isinstance(pairs, dict):
|
|
26
|
+
hint = ""
|
|
27
|
+
if "Jump" in pairs:
|
|
28
|
+
hint = " Did you mean to pass result['Jump'] from branch_cut_jumps(..., mode='pairs')?"
|
|
29
|
+
raise TypeError(
|
|
30
|
+
f"{func_name} expected a list/tuple of (expr, condition) pairs, got dict.{hint}"
|
|
31
|
+
)
|
|
32
|
+
if not isinstance(pairs, (list, tuple)):
|
|
33
|
+
raise TypeError(
|
|
34
|
+
f"{func_name} expected a list/tuple of (expr, condition) pairs, got {type(pairs).__name__}."
|
|
35
|
+
)
|
|
36
|
+
for i, item in enumerate(pairs):
|
|
37
|
+
if not (isinstance(item, (list, tuple)) and len(item) == 2):
|
|
38
|
+
raise TypeError(
|
|
39
|
+
f"{func_name} expected items of the form (expr, condition), but item {i} is {item!r}."
|
|
40
|
+
)
|
|
41
|
+
return pairs
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _validate_restricted_sequence(exprs, func_name="restricted-expression function"):
|
|
45
|
+
if isinstance(exprs, dict):
|
|
46
|
+
hint = ""
|
|
47
|
+
if "Jump" in exprs:
|
|
48
|
+
hint = " Did you mean to pass result['Jump'] or use mode='restricted'?"
|
|
49
|
+
raise TypeError(
|
|
50
|
+
f"{func_name} expected a list/tuple of RestrictedExpr-like expressions, got dict.{hint}"
|
|
51
|
+
)
|
|
52
|
+
if not isinstance(exprs, (list, tuple)):
|
|
53
|
+
raise TypeError(
|
|
54
|
+
f"{func_name} expected a list/tuple of RestrictedExpr-like expressions, got {type(exprs).__name__}."
|
|
55
|
+
)
|
|
56
|
+
return exprs
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
Pair = tuple[object, object]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class _Bound:
|
|
64
|
+
value: object
|
|
65
|
+
strict: bool
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_real_constant(expr) -> bool:
|
|
69
|
+
expr = sympify(expr)
|
|
70
|
+
return bool(expr.is_real and not expr.free_symbols)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _linear_affine(expr, z):
|
|
74
|
+
expr = sympify(expr).expand()
|
|
75
|
+
if expr.has(z):
|
|
76
|
+
a = expr.coeff(z)
|
|
77
|
+
b = simplify(expr - a * z)
|
|
78
|
+
if not b.has(z) and _is_real_constant(a) and _is_real_constant(b) and a != 0:
|
|
79
|
+
return simplify(a), simplify(b)
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _contains_affine_real(cond, z):
|
|
84
|
+
return (
|
|
85
|
+
isinstance(cond, Contains)
|
|
86
|
+
and cond.args[1] == S.Reals
|
|
87
|
+
and _linear_affine(cond.args[0], z) is not None
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _contains_real_impossible(cond, real_required=False):
|
|
92
|
+
if not isinstance(cond, Contains) or cond.args[1] != S.Reals:
|
|
93
|
+
return False
|
|
94
|
+
target = sympify(cond.args[0])
|
|
95
|
+
if real_required:
|
|
96
|
+
try:
|
|
97
|
+
_, im_part = target.as_real_imag()
|
|
98
|
+
im_part = simplify(im_part)
|
|
99
|
+
if im_part.is_real and not im_part.free_symbols and im_part != 0:
|
|
100
|
+
return True
|
|
101
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
102
|
+
pass
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _normalize_relational_to_var(rel, z):
|
|
107
|
+
rel = sympify(rel)
|
|
108
|
+
if not isinstance(rel, Relational) or isinstance(rel, Equality):
|
|
109
|
+
return None
|
|
110
|
+
lhs = simplify(rel.lhs - rel.rhs)
|
|
111
|
+
lin = _linear_affine(lhs, z)
|
|
112
|
+
if lin is None:
|
|
113
|
+
return None
|
|
114
|
+
a, b = lin
|
|
115
|
+
thresh = simplify(-b / a)
|
|
116
|
+
if a.is_negative:
|
|
117
|
+
if isinstance(rel, StrictLessThan):
|
|
118
|
+
kind = ">"
|
|
119
|
+
elif isinstance(rel, LessThan):
|
|
120
|
+
kind = ">="
|
|
121
|
+
elif isinstance(rel, StrictGreaterThan):
|
|
122
|
+
kind = "<"
|
|
123
|
+
else:
|
|
124
|
+
kind = "<="
|
|
125
|
+
else:
|
|
126
|
+
if isinstance(rel, StrictLessThan):
|
|
127
|
+
kind = "<"
|
|
128
|
+
elif isinstance(rel, LessThan):
|
|
129
|
+
kind = "<="
|
|
130
|
+
elif isinstance(rel, StrictGreaterThan):
|
|
131
|
+
kind = ">"
|
|
132
|
+
else:
|
|
133
|
+
kind = ">="
|
|
134
|
+
return kind, thresh
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _merge_lower(old: _Bound | None, new: _Bound) -> _Bound:
|
|
138
|
+
if old is None:
|
|
139
|
+
return new
|
|
140
|
+
if simplify(new.value - old.value).is_positive:
|
|
141
|
+
return new
|
|
142
|
+
if simplify(old.value - new.value).is_positive:
|
|
143
|
+
return old
|
|
144
|
+
return _Bound(old.value, old.strict or new.strict)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _merge_upper(old: _Bound | None, new: _Bound) -> _Bound:
|
|
148
|
+
if old is None:
|
|
149
|
+
return new
|
|
150
|
+
if simplify(new.value - old.value).is_negative:
|
|
151
|
+
return new
|
|
152
|
+
if simplify(old.value - new.value).is_negative:
|
|
153
|
+
return old
|
|
154
|
+
return _Bound(old.value, old.strict or new.strict)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _not_affine_bound(piece, z):
|
|
158
|
+
"""Normalize ``Not(real-affine cut predicate)`` to a bound on ``z``.
|
|
159
|
+
|
|
160
|
+
Normalize the negated relational through the same affine solver used for
|
|
161
|
+
ordinary inequalities. This keeps the threshold expressed in the actual
|
|
162
|
+
branch variable rather than in an intermediate affine expression.
|
|
163
|
+
"""
|
|
164
|
+
piece = sympify(piece)
|
|
165
|
+
if piece.func != Not or len(piece.args) != 1:
|
|
166
|
+
return None
|
|
167
|
+
inner = piece.args[0]
|
|
168
|
+
conjuncts = list(And.make_args(inner)) if inner.func == And else [inner]
|
|
169
|
+
contains_aff = None
|
|
170
|
+
rel = None
|
|
171
|
+
for c in conjuncts:
|
|
172
|
+
if (
|
|
173
|
+
isinstance(c, Contains)
|
|
174
|
+
and c.args[1] == S.Reals
|
|
175
|
+
and _linear_affine(c.args[0], z) is not None
|
|
176
|
+
):
|
|
177
|
+
contains_aff = c.args[0]
|
|
178
|
+
elif isinstance(c, Relational) and not isinstance(c, Equality):
|
|
179
|
+
rel = c
|
|
180
|
+
if contains_aff is None or rel is None:
|
|
181
|
+
return None
|
|
182
|
+
# Only use the shortcut when the relational actually constrains that affine expression.
|
|
183
|
+
aff = contains_aff
|
|
184
|
+
if not (simplify(rel.lhs - aff) == 0 or simplify(rel.rhs - aff) == 0):
|
|
185
|
+
return None
|
|
186
|
+
negated = None
|
|
187
|
+
if isinstance(rel, StrictLessThan):
|
|
188
|
+
negated = GreaterThan(rel.lhs, rel.rhs)
|
|
189
|
+
elif isinstance(rel, LessThan):
|
|
190
|
+
negated = StrictGreaterThan(rel.lhs, rel.rhs)
|
|
191
|
+
elif isinstance(rel, StrictGreaterThan):
|
|
192
|
+
negated = LessThan(rel.lhs, rel.rhs)
|
|
193
|
+
elif isinstance(rel, GreaterThan):
|
|
194
|
+
negated = StrictLessThan(rel.lhs, rel.rhs)
|
|
195
|
+
if negated is None:
|
|
196
|
+
return None
|
|
197
|
+
return _normalize_relational_to_var(negated, z)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _extract_log_real_test(piece):
|
|
201
|
+
"""Return (arg, sign) for Contains(log(arg) +/- I*pi, Reals), else None.
|
|
202
|
+
|
|
203
|
+
sign = 0 -> Contains(log(arg), Reals)
|
|
204
|
+
sign = +1 -> Contains(log(arg) + I*pi, Reals)
|
|
205
|
+
sign = -1 -> Contains(log(arg) - I*pi, Reals)
|
|
206
|
+
"""
|
|
207
|
+
if not isinstance(piece, Contains) or piece.args[1] != S.Reals:
|
|
208
|
+
return None
|
|
209
|
+
target = sympify(piece.args[0])
|
|
210
|
+
if target.func.__name__ == "log":
|
|
211
|
+
return (target.args[0], 0)
|
|
212
|
+
if target.is_Add:
|
|
213
|
+
lg = None
|
|
214
|
+
for a in target.args:
|
|
215
|
+
if getattr(a, "func", None) and a.func.__name__ == "log":
|
|
216
|
+
lg = a
|
|
217
|
+
break
|
|
218
|
+
if lg is not None:
|
|
219
|
+
rest = simplify(target - lg)
|
|
220
|
+
if rest == I * pi:
|
|
221
|
+
return (lg.args[0], +1)
|
|
222
|
+
if rest == -I * pi:
|
|
223
|
+
return (lg.args[0], -1)
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _affine_interval_sign(arg, z, lower, upper):
|
|
228
|
+
"""Determine the strict sign of an affine expression on an interval."""
|
|
229
|
+
lin = _linear_affine(arg, z)
|
|
230
|
+
if lin is None:
|
|
231
|
+
return None
|
|
232
|
+
a, b = lin
|
|
233
|
+
root = simplify(-b / a)
|
|
234
|
+
|
|
235
|
+
def _cmp(v):
|
|
236
|
+
d = simplify(v - root)
|
|
237
|
+
if d.is_positive:
|
|
238
|
+
return 1
|
|
239
|
+
if d.is_negative:
|
|
240
|
+
return -1
|
|
241
|
+
if d == 0:
|
|
242
|
+
return 0
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
# Entire interval lies strictly to the right of the zero.
|
|
246
|
+
if lower is not None:
|
|
247
|
+
c = _cmp(lower.value)
|
|
248
|
+
if c == 1 or (c == 0 and lower.strict):
|
|
249
|
+
return (
|
|
250
|
+
"positive" if a.is_positive else "negative" if a.is_negative else None
|
|
251
|
+
)
|
|
252
|
+
# Entire interval lies strictly to the left of the zero.
|
|
253
|
+
if upper is not None:
|
|
254
|
+
c = _cmp(upper.value)
|
|
255
|
+
if c == -1 or (c == 0 and upper.strict):
|
|
256
|
+
return (
|
|
257
|
+
"negative" if a.is_positive else "positive" if a.is_negative else None
|
|
258
|
+
)
|
|
259
|
+
# Closed endpoint at the root means the expression can vanish.
|
|
260
|
+
if (lower is not None and _cmp(lower.value) == 0 and not lower.strict) or (
|
|
261
|
+
upper is not None and _cmp(upper.value) == 0 and not upper.strict
|
|
262
|
+
):
|
|
263
|
+
return "touches_zero"
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _redundant_or_impossible_log_real_test(piece, z, lower, upper, real_required):
|
|
268
|
+
lt = _extract_log_real_test(piece)
|
|
269
|
+
if lt is None or not real_required:
|
|
270
|
+
return None
|
|
271
|
+
arg, sign = lt
|
|
272
|
+
implied = _affine_interval_sign(arg, z, lower, upper)
|
|
273
|
+
if implied == "positive":
|
|
274
|
+
if sign == 0:
|
|
275
|
+
return "redundant"
|
|
276
|
+
return "impossible"
|
|
277
|
+
if implied == "negative" and sign == 0:
|
|
278
|
+
return "impossible"
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _interval_atoms(lower, upper):
|
|
283
|
+
atoms = []
|
|
284
|
+
if lower is not None:
|
|
285
|
+
atoms.append((">", lower.value) if lower.strict else (">=", lower.value))
|
|
286
|
+
if upper is not None:
|
|
287
|
+
atoms.append(("<", upper.value) if upper.strict else ("<=", upper.value))
|
|
288
|
+
return tuple(atoms)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def simplify_branch_condition(cond, z):
|
|
292
|
+
"""Simplify a branch condition while preserving exact seam semantics.
|
|
293
|
+
|
|
294
|
+
Real/imaginary bounds and logarithmic cut tests are normalized separately so
|
|
295
|
+
logically redundant pieces can be removed without replacing complex branch
|
|
296
|
+
geometry by an unsafe real projection.
|
|
297
|
+
"""
|
|
298
|
+
cond = sympify(cond)
|
|
299
|
+
if cond in (S.true, True):
|
|
300
|
+
return S.true
|
|
301
|
+
if cond in (S.false, False):
|
|
302
|
+
return S.false
|
|
303
|
+
|
|
304
|
+
pieces = list(And.make_args(cond)) if cond.func == And else [cond]
|
|
305
|
+
lower = None
|
|
306
|
+
upper = None
|
|
307
|
+
real_required = False
|
|
308
|
+
extras = []
|
|
309
|
+
log_tests = []
|
|
310
|
+
|
|
311
|
+
for piece in pieces:
|
|
312
|
+
piece = sympify(piece)
|
|
313
|
+
if _contains_affine_real(piece, z):
|
|
314
|
+
real_required = True
|
|
315
|
+
continue
|
|
316
|
+
rb = _normalize_relational_to_var(piece, z)
|
|
317
|
+
if rb is None:
|
|
318
|
+
nb = _not_affine_bound(piece, z)
|
|
319
|
+
if nb is not None:
|
|
320
|
+
rb = nb
|
|
321
|
+
if rb is not None:
|
|
322
|
+
kind, thresh = rb
|
|
323
|
+
if kind == ">":
|
|
324
|
+
lower = _merge_lower(lower, _Bound(thresh, True))
|
|
325
|
+
real_required = True
|
|
326
|
+
continue
|
|
327
|
+
if kind == ">=":
|
|
328
|
+
lower = _merge_lower(lower, _Bound(thresh, False))
|
|
329
|
+
real_required = True
|
|
330
|
+
continue
|
|
331
|
+
if kind == "<":
|
|
332
|
+
upper = _merge_upper(upper, _Bound(thresh, True))
|
|
333
|
+
real_required = True
|
|
334
|
+
continue
|
|
335
|
+
if kind == "<=":
|
|
336
|
+
upper = _merge_upper(upper, _Bound(thresh, False))
|
|
337
|
+
real_required = True
|
|
338
|
+
continue
|
|
339
|
+
lt = _extract_log_real_test(piece)
|
|
340
|
+
if lt is not None:
|
|
341
|
+
log_tests.append(lt)
|
|
342
|
+
extras.append(piece)
|
|
343
|
+
|
|
344
|
+
if lower is not None and upper is not None:
|
|
345
|
+
diff = simplify(upper.value - lower.value)
|
|
346
|
+
if diff.is_negative or (diff == 0 and (lower.strict or upper.strict)):
|
|
347
|
+
return S.false
|
|
348
|
+
|
|
349
|
+
if real_required:
|
|
350
|
+
seen = {}
|
|
351
|
+
for arg, sign in log_tests:
|
|
352
|
+
seen.setdefault(str(arg), set()).add(sign)
|
|
353
|
+
for s in seen.values():
|
|
354
|
+
if +1 in s and -1 in s:
|
|
355
|
+
return S.false
|
|
356
|
+
|
|
357
|
+
filtered = []
|
|
358
|
+
for piece in extras:
|
|
359
|
+
if _contains_real_impossible(piece, real_required=real_required):
|
|
360
|
+
return S.false
|
|
361
|
+
verdict = _redundant_or_impossible_log_real_test(
|
|
362
|
+
piece, z, lower, upper, real_required
|
|
363
|
+
)
|
|
364
|
+
if verdict == "redundant":
|
|
365
|
+
continue
|
|
366
|
+
if verdict == "impossible":
|
|
367
|
+
return S.false
|
|
368
|
+
filtered.append(piece)
|
|
369
|
+
|
|
370
|
+
out = []
|
|
371
|
+
if real_required:
|
|
372
|
+
out.append(Contains(z, S.Reals))
|
|
373
|
+
if lower is not None:
|
|
374
|
+
out.append(z > lower.value if lower.strict else z >= lower.value)
|
|
375
|
+
if upper is not None:
|
|
376
|
+
out.append(z < upper.value if upper.strict else z <= upper.value)
|
|
377
|
+
out.extend(filtered)
|
|
378
|
+
if not out:
|
|
379
|
+
return S.true
|
|
380
|
+
try:
|
|
381
|
+
return simplify(And(*out))
|
|
382
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
383
|
+
return And(*out)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def rewrite_branch_expr(expr):
|
|
387
|
+
expr = sympify(expr)
|
|
388
|
+
|
|
389
|
+
def _rw(e):
|
|
390
|
+
if e.func == acosh and len(e.args) == 1:
|
|
391
|
+
arg = e.args[0]
|
|
392
|
+
num, den = arg.as_numer_denom()
|
|
393
|
+
if num == 1:
|
|
394
|
+
return asech(den)
|
|
395
|
+
if num == -1:
|
|
396
|
+
return acosh(-1 / den)
|
|
397
|
+
if e.is_Mul and I in e.args:
|
|
398
|
+
rest = simplify(e / I)
|
|
399
|
+
if rest.func == acosh and len(rest.args) == 1:
|
|
400
|
+
arg = rest.args[0]
|
|
401
|
+
num, den = arg.as_numer_denom()
|
|
402
|
+
if num == 1:
|
|
403
|
+
return asec(den)
|
|
404
|
+
return e
|
|
405
|
+
|
|
406
|
+
prev = None
|
|
407
|
+
cur = expr
|
|
408
|
+
while prev != cur:
|
|
409
|
+
prev = cur
|
|
410
|
+
cur = cur.replace(lambda e: True, _rw)
|
|
411
|
+
with contextlib.suppress(SYMBOLIC_QUERY_ERRORS):
|
|
412
|
+
cur = simplify(cur)
|
|
413
|
+
return cur
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def merge_duplicate_pairs(pairs: Iterable[Pair], z):
|
|
417
|
+
_validate_pair_sequence(pairs, func_name="merge_duplicate_pairs")
|
|
418
|
+
grouped = defaultdict(list)
|
|
419
|
+
for expr, cond in clean_pairs(pairs):
|
|
420
|
+
expr2 = rewrite_branch_expr(expr)
|
|
421
|
+
cond2 = simplify_branch_condition(cond, z)
|
|
422
|
+
if cond2 is S.false:
|
|
423
|
+
continue
|
|
424
|
+
grouped[expr2].append(cond2)
|
|
425
|
+
|
|
426
|
+
out = []
|
|
427
|
+
for expr, conds in grouped.items():
|
|
428
|
+
uniq = []
|
|
429
|
+
seen = set()
|
|
430
|
+
for c in conds:
|
|
431
|
+
key = str(c)
|
|
432
|
+
if key not in seen:
|
|
433
|
+
seen.add(key)
|
|
434
|
+
uniq.append(c)
|
|
435
|
+
cond = uniq[0] if len(uniq) == 1 else Or(*uniq)
|
|
436
|
+
out.append((expr, cond))
|
|
437
|
+
return out
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def post_simplify_pairs(pairs: Iterable[Pair], z):
|
|
441
|
+
_validate_pair_sequence(pairs, func_name="post_simplify_pairs")
|
|
442
|
+
out = []
|
|
443
|
+
for expr, cond in clean_pairs(pairs):
|
|
444
|
+
expr2 = rewrite_branch_expr(expr)
|
|
445
|
+
cond2 = simplify_branch_condition(cond, z)
|
|
446
|
+
if cond2 is S.false:
|
|
447
|
+
continue
|
|
448
|
+
out.append((expr2, cond2))
|
|
449
|
+
return merge_duplicate_pairs(out, z)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def post_simplify_restricted(exprs: Iterable[object], z):
|
|
453
|
+
_validate_restricted_sequence(exprs, func_name="post_simplify_restricted")
|
|
454
|
+
pairs = []
|
|
455
|
+
for item in exprs:
|
|
456
|
+
if isinstance(item, RestrictedExpr):
|
|
457
|
+
pairs.append((item.expr, item.cond))
|
|
458
|
+
else:
|
|
459
|
+
pairs.append((sympify(item), S.true))
|
|
460
|
+
simp = post_simplify_pairs(pairs, z)
|
|
461
|
+
return [restrict(expr, cond) for expr, cond in simp]
|
funcprops/conditions.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Semantic predicates and parsing for the bundled property catalogue.
|
|
2
|
+
|
|
3
|
+
Catalogue conditions are stored as SymPy constructor expressions. The custom
|
|
4
|
+
Boolean predicates below preserve mathematical assumptions that do not map
|
|
5
|
+
cleanly to ordinary ``Symbol`` assumptions until the proof engine has enough
|
|
6
|
+
context to reduce them.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
|
|
13
|
+
from sympy import S, sympify
|
|
14
|
+
from sympy.core.relational import Relational
|
|
15
|
+
from sympy.logic.boolalg import Boolean, BooleanFunction
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AIntegerQ(BooleanFunction):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ARationalQ(BooleanFunction):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ARealQ(BooleanFunction):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ARealIfDefinedQ(BooleanFunction):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AAlgebraicQ(BooleanFunction):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ANonIntegerQ(BooleanFunction):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class APositive(BooleanFunction):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ANegative(BooleanFunction):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ANonPositive(BooleanFunction):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ANonNegative(BooleanFunction):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AEvenQ(BooleanFunction):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AOddQ(BooleanFunction):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AUnequalQ(BooleanFunction):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AEqualQ(BooleanFunction):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AAssumedQ(BooleanFunction):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AAssumedIneqQ(BooleanFunction):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class NonTerminatingQ(BooleanFunction):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_PREDICATE_TYPES = (
|
|
87
|
+
AIntegerQ,
|
|
88
|
+
ARationalQ,
|
|
89
|
+
ARealQ,
|
|
90
|
+
ARealIfDefinedQ,
|
|
91
|
+
AAlgebraicQ,
|
|
92
|
+
ANonIntegerQ,
|
|
93
|
+
APositive,
|
|
94
|
+
ANegative,
|
|
95
|
+
ANonPositive,
|
|
96
|
+
ANonNegative,
|
|
97
|
+
AEvenQ,
|
|
98
|
+
AOddQ,
|
|
99
|
+
AUnequalQ,
|
|
100
|
+
AEqualQ,
|
|
101
|
+
AAssumedQ,
|
|
102
|
+
AAssumedIneqQ,
|
|
103
|
+
NonTerminatingQ,
|
|
104
|
+
)
|
|
105
|
+
_PREDICATES = {cls.__name__: cls for cls in _PREDICATE_TYPES}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@lru_cache(maxsize=8192)
|
|
109
|
+
def parse_catalogue_expression(text: str):
|
|
110
|
+
"""Parse one bundled catalogue value or condition into a SymPy object.
|
|
111
|
+
|
|
112
|
+
The catalogue uses ``srepr``-compatible constructor syntax, so parsing is
|
|
113
|
+
deterministic and does not require a second symbolic-language grammar.
|
|
114
|
+
Custom semantic predicates remain explicit ``BooleanFunction`` nodes.
|
|
115
|
+
"""
|
|
116
|
+
source = text.strip()
|
|
117
|
+
if not source:
|
|
118
|
+
return S.true
|
|
119
|
+
return sympify(source, locals=_PREDICATES, evaluate=False)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def is_boolean_expr(expr) -> bool:
|
|
123
|
+
"""Return whether ``expr`` is a Boolean or relational SymPy expression."""
|
|
124
|
+
return isinstance(expr, (Boolean, Relational)) or expr in (S.true, S.false)
|