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
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")
|
funcprops/exceptions.py
ADDED
|
@@ -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
|
+
)
|