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
funcprops/dlmf.py ADDED
@@ -0,0 +1,29 @@
1
+ """Access to the generated DLMF property-table audit manifest."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from functools import lru_cache
7
+ from importlib.resources import files
8
+
9
+ from .real_analysis.special_functions import DLMFFact, dlmf_facts
10
+
11
+
12
+ @lru_cache(maxsize=1)
13
+ def _manifest():
14
+ path = files("funcprops").joinpath("data/dlmf_audit.json")
15
+ return json.loads(path.read_text(encoding="utf-8"))
16
+
17
+
18
+ def dlmf_audit_report(family: str | None = None):
19
+ """Return the generated DLMF audit summary or one family's audit row."""
20
+ data = _manifest()
21
+ if family is None:
22
+ return data
23
+ for row in data["families"]:
24
+ if row["family_id"] == family:
25
+ return row
26
+ return None
27
+
28
+
29
+ __all__ = ["DLMFFact", "dlmf_audit_report", "dlmf_facts"]
funcprops/engine.py ADDED
@@ -0,0 +1,324 @@
1
+ """Conditional property-rule evaluation for funcprops.
2
+
3
+ This module turns the declarative catalogue-derived registry into an actual
4
+ rule engine. It is intentionally conservative: unknown parameter conditions
5
+ remain symbolic rather than being guessed true or false.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import contextlib
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+ from typing import Any
14
+
15
+ import sympy as sp
16
+ from sympy import S
17
+ from sympy.assumptions import Q, ask
18
+ from sympy.logic.boolalg import And, BooleanFunction, Not, Or
19
+ from sympy.sets.contains import Contains
20
+
21
+ from .conditions import (
22
+ AAlgebraicQ,
23
+ AAssumedIneqQ,
24
+ AAssumedQ,
25
+ AEqualQ,
26
+ AEvenQ,
27
+ AIntegerQ,
28
+ ANegative,
29
+ ANonIntegerQ,
30
+ ANonNegative,
31
+ ANonPositive,
32
+ AOddQ,
33
+ APositive,
34
+ ARationalQ,
35
+ ARealIfDefinedQ,
36
+ ARealQ,
37
+ AUnequalQ,
38
+ NonTerminatingQ,
39
+ )
40
+ from .results import ConditionalValue, PropertyResult
41
+
42
+
43
+ class ConditionTruth(str, Enum):
44
+ TRUE = "true"
45
+ FALSE = "false"
46
+ UNKNOWN = "unknown"
47
+
48
+
49
+ def _bool_from(value: bool | None):
50
+ if value is True:
51
+ return S.true
52
+ if value is False:
53
+ return S.false
54
+ return None
55
+
56
+
57
+ def _ask_predicate(arg, attr: str, predicate=None):
58
+ direct = getattr(arg, attr, None)
59
+ if direct is not None:
60
+ return _bool_from(bool(direct))
61
+ if predicate is not None:
62
+ try:
63
+ q = ask(predicate(arg))
64
+ except (TypeError, ValueError):
65
+ q = None
66
+ return _bool_from(q)
67
+ return None
68
+
69
+
70
+ def _semantic_predicate(node):
71
+ """Best-effort evaluation of catalogue assumption predicates."""
72
+ if not isinstance(node, BooleanFunction):
73
+ return None
74
+ args = node.args
75
+ if not args:
76
+ return None
77
+ a = args[0]
78
+ cls = node.func
79
+ if cls is AIntegerQ:
80
+ return _ask_predicate(a, "is_integer", Q.integer)
81
+ if cls is ARationalQ:
82
+ return _ask_predicate(a, "is_rational", Q.rational)
83
+ if cls is ARealQ:
84
+ return _ask_predicate(a, "is_real", Q.real)
85
+ if cls is ARealIfDefinedQ:
86
+ # The source predicate allows undefined/infinite values. Real is a
87
+ # sufficient proof; otherwise retain the predicate.
88
+ return _ask_predicate(a, "is_real", Q.real)
89
+ if cls is AAlgebraicQ:
90
+ return _ask_predicate(a, "is_algebraic", Q.algebraic)
91
+ if cls is ANonIntegerQ:
92
+ v = _ask_predicate(a, "is_integer", Q.integer)
93
+ return Not(AIntegerQ(a)) if v is None else Not(v)
94
+ if cls is APositive:
95
+ return _ask_predicate(a, "is_positive", Q.positive)
96
+ if cls is ANegative:
97
+ return _ask_predicate(a, "is_negative", Q.negative)
98
+ if cls is ANonPositive:
99
+ return _ask_predicate(a, "is_nonpositive", Q.nonpositive)
100
+ if cls is ANonNegative:
101
+ return _ask_predicate(a, "is_nonnegative", Q.nonnegative)
102
+ if cls is AEvenQ:
103
+ return _ask_predicate(a, "is_even", Q.even)
104
+ if cls is AOddQ:
105
+ return _ask_predicate(a, "is_odd", Q.odd)
106
+ if cls is AUnequalQ and len(args) == 2:
107
+ d = sp.simplify(args[0] - args[1])
108
+ if d == 0:
109
+ return S.false
110
+ if d.is_zero is False:
111
+ return S.true
112
+ return None
113
+ if cls is AEqualQ and len(args) == 2:
114
+ d = sp.simplify(args[0] - args[1])
115
+ if d == 0:
116
+ return S.true
117
+ if d.is_zero is False:
118
+ return S.false
119
+ return None
120
+ if cls in (AAssumedQ, AAssumedIneqQ):
121
+ return simplify_condition(a)
122
+ if cls is NonTerminatingQ:
123
+ # The catalogue normally passes a Tuple of upper parameters. A
124
+ # hypergeometric series terminates when at least one upper parameter
125
+ # is a nonpositive integer.
126
+ seq = tuple(a) if isinstance(a, (sp.Tuple, tuple, list)) else tuple(args)
127
+ unknown = False
128
+ for p in seq:
129
+ is_int = getattr(-p, "is_integer", None)
130
+ is_nonneg = getattr(-p, "is_nonnegative", None)
131
+ if is_int is True and is_nonneg is True:
132
+ return S.false
133
+ if is_int is None or is_nonneg is None:
134
+ unknown = True
135
+ return None if unknown else S.true
136
+ return None
137
+
138
+
139
+ def simplify_condition(condition, *, assumptions=S.true):
140
+ """Simplify a registry guard while preserving genuinely unknown facts."""
141
+ c = sp.sympify(condition)
142
+ assumptions = sp.sympify(assumptions)
143
+
144
+ if c in (S.true, S.false):
145
+ return c
146
+
147
+ # Bottom-up semantic reduction of custom predicates and membership tests.
148
+ def walk(node):
149
+ if node in (S.true, S.false):
150
+ return node
151
+ if isinstance(node, Contains):
152
+ item, dom = node.args
153
+ if dom == S.Integers:
154
+ v = _ask_predicate(item, "is_integer", Q.integer)
155
+ if v is not None:
156
+ return v
157
+ elif dom == S.Reals:
158
+ v = _ask_predicate(item, "is_real", Q.real)
159
+ if v is not None:
160
+ return v
161
+ elif dom == S.Rationals:
162
+ v = _ask_predicate(item, "is_rational", Q.rational)
163
+ if v is not None:
164
+ return v
165
+ elif dom == S.Complexes:
166
+ # Every finite ordinary SymPy number/expression is intended as
167
+ # complex-valued unless explicitly non-complex.
168
+ if getattr(item, "is_complex", None) is True:
169
+ return S.true
170
+ pred = _semantic_predicate(node)
171
+ if pred is not None:
172
+ return pred
173
+ if not getattr(node, "args", None):
174
+ return node
175
+ new_args = tuple(walk(a) for a in node.args)
176
+ try:
177
+ return node.func(*new_args)
178
+ except (TypeError, ValueError):
179
+ return node
180
+
181
+ c = walk(c)
182
+ has_semantic = isinstance(c, sp.Basic) and any(
183
+ c.has(cls)
184
+ for cls in (
185
+ AIntegerQ,
186
+ ARationalQ,
187
+ ARealQ,
188
+ ARealIfDefinedQ,
189
+ AAlgebraicQ,
190
+ ANonIntegerQ,
191
+ APositive,
192
+ ANegative,
193
+ ANonPositive,
194
+ ANonNegative,
195
+ AEvenQ,
196
+ AOddQ,
197
+ AUnequalQ,
198
+ AEqualQ,
199
+ AAssumedQ,
200
+ AAssumedIneqQ,
201
+ NonTerminatingQ,
202
+ )
203
+ )
204
+ if not has_semantic:
205
+ with contextlib.suppress(TypeError, ValueError):
206
+ c = sp.refine(c, assumptions)
207
+ with contextlib.suppress(TypeError, ValueError):
208
+ c = sp.simplify(c)
209
+ if c is True:
210
+ return S.true
211
+ if c is False:
212
+ return S.false
213
+ return c
214
+
215
+
216
+ def truth_value(condition, *, assumptions=S.true) -> ConditionTruth:
217
+ c = simplify_condition(condition, assumptions=assumptions)
218
+ if c is S.true:
219
+ return ConditionTruth.TRUE
220
+ if c is S.false:
221
+ return ConditionTruth.FALSE
222
+ # If the user's assumptions directly imply or refute the guard, refine
223
+ # often handles it. A final logical simplification is safe for Boolean
224
+ # combinations but does not solve arbitrary inequalities.
225
+ has_semantic = isinstance(c, sp.Basic) and any(
226
+ c.has(cls)
227
+ for cls in (
228
+ AIntegerQ,
229
+ ARationalQ,
230
+ ARealQ,
231
+ ARealIfDefinedQ,
232
+ AAlgebraicQ,
233
+ ANonIntegerQ,
234
+ APositive,
235
+ ANegative,
236
+ ANonPositive,
237
+ ANonNegative,
238
+ AEvenQ,
239
+ AOddQ,
240
+ AUnequalQ,
241
+ AEqualQ,
242
+ AAssumedQ,
243
+ AAssumedIneqQ,
244
+ NonTerminatingQ,
245
+ )
246
+ )
247
+ if not has_semantic:
248
+ try:
249
+ both = sp.simplify_logic(And(assumptions, Not(c)))
250
+ if both is S.false:
251
+ return ConditionTruth.TRUE
252
+ both = sp.simplify_logic(And(assumptions, c))
253
+ if both is S.false:
254
+ return ConditionTruth.FALSE
255
+ except (TypeError, ValueError, NotImplementedError):
256
+ pass
257
+ return ConditionTruth.UNKNOWN
258
+
259
+
260
+ @dataclass(frozen=True)
261
+ class EvaluatedCase:
262
+ value: Any
263
+ condition: sp.Basic
264
+ source_lhs: str | None
265
+ specificity: int
266
+
267
+
268
+ def evaluate_property(
269
+ expr, prop: str, *, var=None, mode=None, assumptions=S.true
270
+ ) -> PropertyResult:
271
+ """Evaluate all applicable registry cases for one concrete expression.
272
+
273
+ More-specific catalogue definitions shadow generic definitions when their
274
+ guards are proved true. If a guard cannot be decided, the result remains
275
+ conditional and the generic rule is retained as the fallback case.
276
+ """
277
+ from .registry import registry
278
+
279
+ expr = sp.sympify(expr)
280
+ fam = registry().get(getattr(expr.func, "__name__", str(expr.func)))
281
+ if fam is None:
282
+ return PropertyResult(primary=None, source="registry:no-family")
283
+
284
+ candidates = []
285
+ for rule in fam.cases(prop):
286
+ if not rule.matches(expr, var=var, mode=mode):
287
+ continue
288
+ value, guard = rule.instantiate(expr)
289
+ if mode in ("real", "complex"):
290
+ repl_mode = {sp.Symbol("real"): S.true if mode == "real" else S.false}
291
+ if isinstance(value, sp.Basic):
292
+ value = value.xreplace(repl_mode)
293
+ if isinstance(guard, sp.Basic):
294
+ guard = guard.xreplace(repl_mode)
295
+ guard = simplify_condition(guard, assumptions=assumptions)
296
+ if guard is S.false:
297
+ continue
298
+ candidates.append(
299
+ EvaluatedCase(value, guard, rule.source_lhs, rule.specificity(mode=mode))
300
+ )
301
+
302
+ if not candidates:
303
+ return PropertyResult(primary=None, source="registry:no-case")
304
+
305
+ candidates.sort(key=lambda c: c.specificity, reverse=True)
306
+ out = []
307
+ covered = S.false
308
+ for c in candidates:
309
+ effective = simplify_condition(
310
+ And(c.condition, Not(covered)), assumptions=assumptions
311
+ )
312
+ if effective is S.false:
313
+ continue
314
+ out.append(ConditionalValue(c.value, effective))
315
+ covered = simplify_condition(Or(covered, c.condition), assumptions=assumptions)
316
+ if effective is S.true:
317
+ # An unconditional rule at this precedence level shadows every
318
+ # less-specific definition exactly as catalogue precedence does.
319
+ break
320
+
321
+ if not out:
322
+ return PropertyResult(primary=None, source="registry:no-case")
323
+ primary = out[0].value if out[0].condition is S.true else None
324
+ return PropertyResult(primary=primary, cases=tuple(out), source="registry")
@@ -0,0 +1,29 @@
1
+ """Expected failures from optional or undecidable symbolic computations.
2
+
3
+ Fallback code should catch the smallest operation-specific family that can
4
+ legitimately be raised by the SymPy operation being attempted. The aggregate
5
+ ``SYMBOLIC_QUERY_ERRORS`` remains for compatibility at broad orchestration
6
+ boundaries, while new proof code should prefer the narrower groups below.
7
+ """
8
+
9
+ from sympy.core.function import PoleError
10
+ from sympy.core.sympify import SympifyError
11
+ from sympy.polys.polyerrors import PolynomialError
12
+
13
+ SYMPIFY_ERRORS = (SympifyError, TypeError)
14
+ POLYNOMIAL_ERRORS = (PolynomialError, ValueError)
15
+ SIMPLIFICATION_ERRORS = (TypeError, ValueError, NotImplementedError, RecursionError)
16
+ SET_OPERATION_ERRORS = (TypeError, ValueError, NotImplementedError)
17
+ LIMIT_ERRORS = (PoleError, ValueError, NotImplementedError)
18
+ BACKEND_QUERY_ERRORS = (ValueError, NotImplementedError, ArithmeticError)
19
+
20
+ # Compatibility umbrella for high-level fallback boundaries. Deliberately
21
+ # excludes SyntaxError: malformed source is a programming/input bug, not an
22
+ # expected symbolic-decision failure.
23
+ SYMBOLIC_QUERY_ERRORS = (
24
+ *SIMPLIFICATION_ERRORS,
25
+ ArithmeticError,
26
+ SympifyError,
27
+ PolynomialError,
28
+ PoleError,
29
+ )