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.
Files changed (56) hide show
  1. funcprops/__init__.py +234 -0
  2. funcprops/_semialg.py +246 -0
  3. funcprops/api.py +70 -0
  4. funcprops/branchcuts/__init__.py +141 -0
  5. funcprops/branchcuts/computation.py +664 -0
  6. funcprops/branchcuts/core.py +1204 -0
  7. funcprops/branchcuts/post_simplify.py +461 -0
  8. funcprops/conditions.py +124 -0
  9. funcprops/data/catalogue.json +127449 -0
  10. funcprops/data/dlmf_audit.json +3382 -0
  11. funcprops/dlmf.py +29 -0
  12. funcprops/engine.py +324 -0
  13. funcprops/exceptions.py +29 -0
  14. funcprops/proof_cache.py +373 -0
  15. funcprops/real_analysis/__init__.py +52 -0
  16. funcprops/real_analysis/advanced_convexity.py +217 -0
  17. funcprops/real_analysis/boundedness.py +168 -0
  18. funcprops/real_analysis/common.py +244 -0
  19. funcprops/real_analysis/convexity.py +698 -0
  20. funcprops/real_analysis/differentiability.py +112 -0
  21. funcprops/real_analysis/domain.py +188 -0
  22. funcprops/real_analysis/mapping.py +374 -0
  23. funcprops/real_analysis/monotonic_certificates.py +867 -0
  24. funcprops/real_analysis/monotonicity.py +673 -0
  25. funcprops/real_analysis/oscillation.py +151 -0
  26. funcprops/real_analysis/piecewise.py +174 -0
  27. funcprops/real_analysis/range.py +552 -0
  28. funcprops/real_analysis/sign.py +561 -0
  29. funcprops/real_analysis/smoothness.py +268 -0
  30. funcprops/real_analysis/special_functions.py +515 -0
  31. funcprops/real_analysis/structural_properties.py +558 -0
  32. funcprops/real_analysis/zeros.py +327 -0
  33. funcprops/registry.py +274 -0
  34. funcprops/results.py +239 -0
  35. funcprops/singularities.py +1767 -0
  36. funcprops/stratification.py +494 -0
  37. funcprops/sympy_compare.py +63 -0
  38. funcprops/transcendental/__init__.py +140 -0
  39. funcprops/transcendental/algebraize.py +201 -0
  40. funcprops/transcendental/analytic.py +261 -0
  41. funcprops/transcendental/complex_roots.py +542 -0
  42. funcprops/transcendental/exp_fourier.py +221 -0
  43. funcprops/transcendental/extrema.py +821 -0
  44. funcprops/transcendental/periodic.py +222 -0
  45. funcprops/transcendental/proof.py +104 -0
  46. funcprops/transcendental/real_interval.py +128 -0
  47. funcprops/transcendental/real_sign.py +117 -0
  48. funcprops/transcendental/reduce.py +288 -0
  49. funcprops/transcendental/roots.py +1380 -0
  50. funcprops/transcendental/zero_geometry.py +1197 -0
  51. funcprops/verification.py +410 -0
  52. funcprops-0.1.0.dist-info/METADATA +208 -0
  53. funcprops-0.1.0.dist-info/RECORD +56 -0
  54. funcprops-0.1.0.dist-info/WHEEL +5 -0
  55. funcprops-0.1.0.dist-info/licenses/LICENSE +674 -0
  56. 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]
@@ -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)