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,327 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sympy as sp
|
|
4
|
+
from sympy import S
|
|
5
|
+
|
|
6
|
+
from ..exceptions import SYMBOLIC_QUERY_ERRORS
|
|
7
|
+
from ..results import PropertyResult
|
|
8
|
+
from ..stratification import evaluate_parameter_strata
|
|
9
|
+
from ..transcendental.roots import (
|
|
10
|
+
RootCertificate,
|
|
11
|
+
RootIsolationResult,
|
|
12
|
+
transcendental_roots,
|
|
13
|
+
)
|
|
14
|
+
from .common import condition_to_set
|
|
15
|
+
from .domain import function_domain
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _merge_root_results(results, domain_set):
|
|
19
|
+
"""Merge per-cell zero results without discarding certification metadata."""
|
|
20
|
+
roots = []
|
|
21
|
+
zero_intervals = []
|
|
22
|
+
isolated = []
|
|
23
|
+
certificates = []
|
|
24
|
+
evidence = []
|
|
25
|
+
families = []
|
|
26
|
+
geometry = []
|
|
27
|
+
complete = True
|
|
28
|
+
methods = []
|
|
29
|
+
periodic = None
|
|
30
|
+
for result in results:
|
|
31
|
+
roots.extend(result.roots)
|
|
32
|
+
zero_intervals.extend(result.zero_intervals)
|
|
33
|
+
isolated.extend(result.isolated_intervals)
|
|
34
|
+
certificates.extend(result.certificates)
|
|
35
|
+
evidence.extend(result.evidence)
|
|
36
|
+
families.extend(result.parametric_families)
|
|
37
|
+
geometry.extend(result.geometry)
|
|
38
|
+
complete = complete and result.complete
|
|
39
|
+
methods.append(result.method)
|
|
40
|
+
if result.periodic is not None:
|
|
41
|
+
# A periodic representation cannot in general be combined losslessly
|
|
42
|
+
# with unrelated domain cells. Preserve it only for the single-cell case.
|
|
43
|
+
if periodic is None and len(results) == 1:
|
|
44
|
+
periodic = result.periodic
|
|
45
|
+
else:
|
|
46
|
+
complete = False
|
|
47
|
+
roots = tuple(sorted(set(roots), key=sp.default_sort_key))
|
|
48
|
+
zero_intervals = tuple(sorted(set(zero_intervals), key=sp.default_sort_key))
|
|
49
|
+
method = "domain_cell_isolation:" + "+".join(dict.fromkeys(methods))
|
|
50
|
+
return RootIsolationResult(
|
|
51
|
+
roots,
|
|
52
|
+
zero_intervals,
|
|
53
|
+
tuple(isolated),
|
|
54
|
+
periodic,
|
|
55
|
+
complete,
|
|
56
|
+
method,
|
|
57
|
+
(*tuple(evidence), ("domain_cells", domain_set)),
|
|
58
|
+
tuple(certificates),
|
|
59
|
+
tuple(families),
|
|
60
|
+
tuple(geometry),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _domain_cells(domain_set):
|
|
65
|
+
"""Return explicit real interval/point cells, or ``None`` if unresolved."""
|
|
66
|
+
domain_set = sp.simplify(domain_set)
|
|
67
|
+
if domain_set is S.EmptySet:
|
|
68
|
+
return ()
|
|
69
|
+
if isinstance(domain_set, (sp.Interval, sp.FiniteSet)):
|
|
70
|
+
return (domain_set,)
|
|
71
|
+
if isinstance(domain_set, sp.Union):
|
|
72
|
+
cells = []
|
|
73
|
+
for arg in domain_set.args:
|
|
74
|
+
sub = _domain_cells(arg)
|
|
75
|
+
if sub is None:
|
|
76
|
+
return None
|
|
77
|
+
cells.extend(sub)
|
|
78
|
+
return tuple(cells)
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _finite_real_solutions(equation, var):
|
|
83
|
+
try:
|
|
84
|
+
sol = sp.solveset(equation, var, domain=S.Reals)
|
|
85
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
86
|
+
return ()
|
|
87
|
+
if not isinstance(sol, sp.FiniteSet):
|
|
88
|
+
return ()
|
|
89
|
+
return tuple(q for q in sol if q.is_real is not False)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _structural_exceptional_points(expr, var, domain_set):
|
|
93
|
+
"""Exact points at which a real-expression regime or branch can change."""
|
|
94
|
+
equations = []
|
|
95
|
+
try:
|
|
96
|
+
den = sp.denom(sp.together(expr))
|
|
97
|
+
if den != 1:
|
|
98
|
+
equations.append(sp.Eq(den, 0))
|
|
99
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
100
|
+
pass
|
|
101
|
+
for node in sp.preorder_traversal(expr):
|
|
102
|
+
if node.func is sp.log and node.args:
|
|
103
|
+
equations.append(sp.Eq(node.args[0], 0))
|
|
104
|
+
elif isinstance(node, sp.Pow):
|
|
105
|
+
base, exponent = node.args
|
|
106
|
+
if exponent.is_Rational and not exponent.is_Integer:
|
|
107
|
+
equations.append(sp.Eq(base, 0))
|
|
108
|
+
elif isinstance(node, sp.Piecewise):
|
|
109
|
+
for _value, cond in node.args:
|
|
110
|
+
if cond is True or cond is S.true:
|
|
111
|
+
continue
|
|
112
|
+
from sympy.core.relational import Relational
|
|
113
|
+
|
|
114
|
+
for rel in cond.atoms(Relational):
|
|
115
|
+
equations.append(sp.Eq(rel.lhs - rel.rhs, 0))
|
|
116
|
+
points = []
|
|
117
|
+
for eq in equations:
|
|
118
|
+
for point in _finite_real_solutions(eq, var):
|
|
119
|
+
try:
|
|
120
|
+
contained = domain_set.contains(point)
|
|
121
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
122
|
+
continue
|
|
123
|
+
if contained is S.true and point not in points:
|
|
124
|
+
points.append(point)
|
|
125
|
+
return tuple(sorted(points, key=sp.default_sort_key))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _split_interval_at_points(interval, points):
|
|
129
|
+
if not isinstance(interval, sp.Interval):
|
|
130
|
+
return (interval,)
|
|
131
|
+
interior = []
|
|
132
|
+
for point in points:
|
|
133
|
+
try:
|
|
134
|
+
if interval.contains(point) is S.true and point not in (
|
|
135
|
+
interval.start,
|
|
136
|
+
interval.end,
|
|
137
|
+
):
|
|
138
|
+
interior.append(point)
|
|
139
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
140
|
+
pass
|
|
141
|
+
interior = tuple(sorted(set(interior), key=sp.default_sort_key))
|
|
142
|
+
if not interior:
|
|
143
|
+
return (interval,)
|
|
144
|
+
cells = []
|
|
145
|
+
left = interval.start
|
|
146
|
+
left_open = interval.left_open
|
|
147
|
+
for point in interior:
|
|
148
|
+
cells.append(sp.Interval(left, point, left_open=left_open, right_open=True))
|
|
149
|
+
cells.append(sp.FiniteSet(point))
|
|
150
|
+
left = point
|
|
151
|
+
left_open = True
|
|
152
|
+
cells.append(
|
|
153
|
+
sp.Interval(
|
|
154
|
+
left, interval.end, left_open=left_open, right_open=interval.right_open
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
return tuple(c for c in cells if c is not S.EmptySet)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _zeros_on_set(expr, var, domain_set, *, return_result, allow_numerical):
|
|
161
|
+
"""Evaluate zero geometry on one parameter-free real set.
|
|
162
|
+
|
|
163
|
+
Disconnected real domains are analyzed cell by cell so no certificate spans a
|
|
164
|
+
pole, branch boundary, logarithmic exclusion, or other point where the input is
|
|
165
|
+
undefined.
|
|
166
|
+
"""
|
|
167
|
+
cells = _domain_cells(domain_set)
|
|
168
|
+
if cells is not None:
|
|
169
|
+
exceptional = _structural_exceptional_points(expr, var, domain_set)
|
|
170
|
+
split_cells = []
|
|
171
|
+
for cell in cells:
|
|
172
|
+
split_cells.extend(_split_interval_at_points(cell, exceptional))
|
|
173
|
+
cells = tuple(split_cells)
|
|
174
|
+
if cells is not None and not (
|
|
175
|
+
len(cells) == 1 and isinstance(cells[0], sp.Interval) and cells[0] == domain_set
|
|
176
|
+
):
|
|
177
|
+
results = []
|
|
178
|
+
for cell in cells:
|
|
179
|
+
if isinstance(cell, sp.FiniteSet):
|
|
180
|
+
roots = []
|
|
181
|
+
certs = []
|
|
182
|
+
cell_complete = True
|
|
183
|
+
for point in cell:
|
|
184
|
+
try:
|
|
185
|
+
value = sp.simplify(expr.subs(var, point))
|
|
186
|
+
except SYMBOLIC_QUERY_ERRORS:
|
|
187
|
+
cell_complete = False
|
|
188
|
+
continue
|
|
189
|
+
if value == 0 or value.is_zero is True:
|
|
190
|
+
roots.append(point)
|
|
191
|
+
certs.append(
|
|
192
|
+
RootCertificate(
|
|
193
|
+
sp.FiniteSet(point),
|
|
194
|
+
1,
|
|
195
|
+
"exact_domain_point_root",
|
|
196
|
+
(("value", value),),
|
|
197
|
+
True,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
elif value.is_zero is not False:
|
|
201
|
+
cell_complete = False
|
|
202
|
+
results.append(
|
|
203
|
+
RootIsolationResult(
|
|
204
|
+
tuple(roots),
|
|
205
|
+
complete=cell_complete,
|
|
206
|
+
method="domain_point_evaluation",
|
|
207
|
+
certificates=tuple(certs),
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
results.append(
|
|
212
|
+
transcendental_roots(
|
|
213
|
+
expr, var, cell, allow_numerical=allow_numerical
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
rr = _merge_root_results(results, domain_set)
|
|
217
|
+
else:
|
|
218
|
+
rr = transcendental_roots(
|
|
219
|
+
expr, var, domain_set, allow_numerical=allow_numerical
|
|
220
|
+
)
|
|
221
|
+
if return_result:
|
|
222
|
+
return rr
|
|
223
|
+
if rr.complete:
|
|
224
|
+
if rr.periodic is not None:
|
|
225
|
+
return rr.periodic
|
|
226
|
+
if (
|
|
227
|
+
rr.method == "dlmf_indexed_zeros"
|
|
228
|
+
and rr.evidence
|
|
229
|
+
and isinstance(rr.evidence[0], sp.Set)
|
|
230
|
+
):
|
|
231
|
+
return rr.evidence[0]
|
|
232
|
+
if rr.isolated_intervals or rr.parametric_families:
|
|
233
|
+
return rr
|
|
234
|
+
parts = []
|
|
235
|
+
if rr.roots:
|
|
236
|
+
parts.append(sp.FiniteSet(*rr.roots))
|
|
237
|
+
parts.extend(rr.zero_intervals)
|
|
238
|
+
if not parts:
|
|
239
|
+
return S.EmptySet
|
|
240
|
+
return sp.Union(*parts) if len(parts) > 1 else parts[0]
|
|
241
|
+
return rr
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def function_zeros(
|
|
245
|
+
expr,
|
|
246
|
+
var,
|
|
247
|
+
*,
|
|
248
|
+
domain=True,
|
|
249
|
+
return_result=False,
|
|
250
|
+
allow_numerical=False,
|
|
251
|
+
_domain_ready=False,
|
|
252
|
+
):
|
|
253
|
+
"""Return the certified real zero set of ``expr``.
|
|
254
|
+
|
|
255
|
+
Exact finite and periodic families are returned directly. Parameter-dependent
|
|
256
|
+
domain conditions are evaluated stratum by stratum. If a root is only
|
|
257
|
+
certified by an isolating interval, ``RootIsolationResult`` is returned so
|
|
258
|
+
the certificate is not discarded.
|
|
259
|
+
"""
|
|
260
|
+
expr = sp.sympify(expr)
|
|
261
|
+
var = sp.sympify(var)
|
|
262
|
+
natural = function_domain(expr, var)
|
|
263
|
+
if isinstance(natural, PropertyResult):
|
|
264
|
+
# Unknown natural-domain information must not erase an explicit user
|
|
265
|
+
# restriction. Fall back to the requested set/condition only.
|
|
266
|
+
if domain is True:
|
|
267
|
+
domain_set = S.Reals
|
|
268
|
+
elif isinstance(domain, sp.Set):
|
|
269
|
+
domain_set = domain
|
|
270
|
+
else:
|
|
271
|
+
domain_set = condition_to_set(sp.sympify(domain), var)
|
|
272
|
+
if domain_set is None:
|
|
273
|
+
return RootIsolationResult(
|
|
274
|
+
(), complete=False, method="unsupported-domain"
|
|
275
|
+
)
|
|
276
|
+
return _zeros_on_set(
|
|
277
|
+
expr,
|
|
278
|
+
var,
|
|
279
|
+
domain_set,
|
|
280
|
+
return_result=return_result,
|
|
281
|
+
allow_numerical=allow_numerical,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if domain is True:
|
|
285
|
+
condition = natural
|
|
286
|
+
elif isinstance(domain, sp.Set):
|
|
287
|
+
natural_set = condition_to_set(natural, var)
|
|
288
|
+
domain_set = sp.Intersection(
|
|
289
|
+
natural_set if natural_set is not None else S.Reals, domain
|
|
290
|
+
)
|
|
291
|
+
return _zeros_on_set(
|
|
292
|
+
expr,
|
|
293
|
+
var,
|
|
294
|
+
domain_set,
|
|
295
|
+
return_result=return_result,
|
|
296
|
+
allow_numerical=allow_numerical,
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
condition = sp.And(natural, sp.sympify(domain))
|
|
300
|
+
|
|
301
|
+
if not _domain_ready:
|
|
302
|
+
stratified = evaluate_parameter_strata(
|
|
303
|
+
condition,
|
|
304
|
+
var,
|
|
305
|
+
lambda branch: function_zeros(
|
|
306
|
+
expr,
|
|
307
|
+
var,
|
|
308
|
+
domain=branch,
|
|
309
|
+
return_result=return_result,
|
|
310
|
+
allow_numerical=allow_numerical,
|
|
311
|
+
_domain_ready=True,
|
|
312
|
+
),
|
|
313
|
+
source="zero-parameter-stratification",
|
|
314
|
+
)
|
|
315
|
+
if stratified is not None:
|
|
316
|
+
return stratified
|
|
317
|
+
|
|
318
|
+
domain_set = condition_to_set(condition, var)
|
|
319
|
+
if domain_set is None:
|
|
320
|
+
return RootIsolationResult((), complete=False, method="unsupported-domain")
|
|
321
|
+
return _zeros_on_set(
|
|
322
|
+
expr,
|
|
323
|
+
var,
|
|
324
|
+
domain_set,
|
|
325
|
+
return_result=return_result,
|
|
326
|
+
allow_numerical=allow_numerical,
|
|
327
|
+
)
|
funcprops/registry.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from importlib.resources import files
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import sympy as sp
|
|
12
|
+
from sympy import Symbol
|
|
13
|
+
|
|
14
|
+
from .conditions import parse_catalogue_expression
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _balanced_content(text: str, start: int, open_char="[", close_char="]"):
|
|
18
|
+
depth = 0
|
|
19
|
+
for i in range(start, len(text)):
|
|
20
|
+
ch = text[i]
|
|
21
|
+
if ch == open_char:
|
|
22
|
+
depth += 1
|
|
23
|
+
elif ch == close_char:
|
|
24
|
+
depth -= 1
|
|
25
|
+
if depth == 0:
|
|
26
|
+
return text[start + 1 : i]
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _split_top_level(text: str):
|
|
31
|
+
out = []
|
|
32
|
+
start = 0
|
|
33
|
+
depth = 0
|
|
34
|
+
pairs = {"[": "]", "(": ")", "{": "}"}
|
|
35
|
+
closes = set(pairs.values())
|
|
36
|
+
stack = []
|
|
37
|
+
for i, ch in enumerate(text):
|
|
38
|
+
if ch in pairs:
|
|
39
|
+
stack.append(pairs[ch])
|
|
40
|
+
depth += 1
|
|
41
|
+
elif ch in closes and stack and ch == stack[-1]:
|
|
42
|
+
stack.pop()
|
|
43
|
+
depth -= 1
|
|
44
|
+
elif ch == "," and depth == 0:
|
|
45
|
+
out.append(text[start:i].strip())
|
|
46
|
+
start = i + 1
|
|
47
|
+
out.append(text[start:].strip())
|
|
48
|
+
return out
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _source_signature(lhs: str | None):
|
|
52
|
+
"""Extract the represented function pattern and property mode/variable."""
|
|
53
|
+
if not lhs:
|
|
54
|
+
return None, (), None, None
|
|
55
|
+
lhs = lhs.split("/;", 1)[0].strip()
|
|
56
|
+
first = lhs.find("[")
|
|
57
|
+
if first < 0:
|
|
58
|
+
return None, (), None, None
|
|
59
|
+
outer = _balanced_content(lhs, first)
|
|
60
|
+
if outer is None:
|
|
61
|
+
return None, (), None, None
|
|
62
|
+
outer_args = _split_top_level(outer)
|
|
63
|
+
if not outer_args:
|
|
64
|
+
return None, (), None, None
|
|
65
|
+
fpat = outer_args[0].strip()
|
|
66
|
+
if fpat.startswith("HoldPattern["):
|
|
67
|
+
i = fpat.find("[")
|
|
68
|
+
inner = _balanced_content(fpat, i)
|
|
69
|
+
if inner is not None:
|
|
70
|
+
fpat = inner.strip()
|
|
71
|
+
hidx = fpat.find("[")
|
|
72
|
+
if hidx < 0:
|
|
73
|
+
return fpat, (), None, outer_args[1] if len(outer_args) > 1 else None
|
|
74
|
+
head = fpat[:hidx].strip()
|
|
75
|
+
inner = _balanced_content(fpat, hidx)
|
|
76
|
+
args = tuple(_split_top_level(inner or "")) if inner is not None else ()
|
|
77
|
+
prop_head = lhs[:first].strip()
|
|
78
|
+
mode = None
|
|
79
|
+
var_token = None
|
|
80
|
+
if "SingularityProperties" in prop_head and len(outer_args) > 1:
|
|
81
|
+
token = outer_args[1].strip()
|
|
82
|
+
if token == "True":
|
|
83
|
+
mode = "real"
|
|
84
|
+
elif token == "False":
|
|
85
|
+
mode = "complex"
|
|
86
|
+
else:
|
|
87
|
+
mode = "any"
|
|
88
|
+
if "RealUnivariateProperties" in prop_head and len(outer_args) > 1:
|
|
89
|
+
var_token = outer_args[1].strip()
|
|
90
|
+
return head, args, mode, var_token
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _pattern_name(token: str):
|
|
94
|
+
token = token.strip()
|
|
95
|
+
m = re.match(r"([A-Za-z$][A-Za-z0-9$]*)_", token)
|
|
96
|
+
if m:
|
|
97
|
+
return m.group(1)
|
|
98
|
+
m = re.match(r"([A-Za-z$][A-Za-z0-9$]*):", token)
|
|
99
|
+
if m:
|
|
100
|
+
return m.group(1)
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _literal_pattern_matches(token: str, actual) -> bool:
|
|
105
|
+
token = token.strip()
|
|
106
|
+
# Ordinary named patterns are handled by guards/assumptions.
|
|
107
|
+
if "_" in token and re.match(r"^[A-Za-z$][A-Za-z0-9$]*_", token):
|
|
108
|
+
return True
|
|
109
|
+
# a:(2|3), a:3 style restricted alternatives/defaults.
|
|
110
|
+
if ":" in token and re.match(r"^[A-Za-z$][A-Za-z0-9$]*:", token):
|
|
111
|
+
rhs = token.split(":", 1)[1].strip()
|
|
112
|
+
if rhs.startswith("(") and rhs.endswith(")") and "|" in rhs:
|
|
113
|
+
vals = rhs[1:-1].split("|")
|
|
114
|
+
return any(_literal_pattern_matches(v, actual) for v in vals)
|
|
115
|
+
if rhs.startswith("_"):
|
|
116
|
+
return True
|
|
117
|
+
return _literal_pattern_matches(rhs, actual)
|
|
118
|
+
# List/vector patterns and Blank/Repeated constructs are too rich to match
|
|
119
|
+
# syntactically here; leave them to their explicit registry guard.
|
|
120
|
+
if any(x in token for x in ("_", "Repeated[", "Vectors[", "Matrices[")):
|
|
121
|
+
return True
|
|
122
|
+
try:
|
|
123
|
+
literal = parse_catalogue_expression(token)
|
|
124
|
+
if isinstance(actual, sp.Basic) and isinstance(literal, sp.Basic):
|
|
125
|
+
return sp.simplify(actual - literal) == 0
|
|
126
|
+
return actual == literal
|
|
127
|
+
except (TypeError, ValueError):
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True)
|
|
132
|
+
class RuleCase:
|
|
133
|
+
value_source: str
|
|
134
|
+
condition_source: str
|
|
135
|
+
source_property_type: str | None = None
|
|
136
|
+
source_lhs: str | None = None
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def condition(self):
|
|
140
|
+
return parse_catalogue_expression(self.condition_source or "True")
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def value(self):
|
|
144
|
+
return parse_catalogue_expression(self.value_source)
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def signature(self):
|
|
148
|
+
return _source_signature(self.source_lhs)
|
|
149
|
+
|
|
150
|
+
def substitutions(self, expr):
|
|
151
|
+
_, patterns, _, _ = self.signature
|
|
152
|
+
repl = {}
|
|
153
|
+
for token, actual in zip(patterns, getattr(expr, "args", ()), strict=False):
|
|
154
|
+
name = _pattern_name(token)
|
|
155
|
+
if name:
|
|
156
|
+
repl[Symbol(name)] = actual
|
|
157
|
+
return repl
|
|
158
|
+
|
|
159
|
+
def instantiate(self, expr):
|
|
160
|
+
repl = self.substitutions(expr)
|
|
161
|
+
value = self.value
|
|
162
|
+
condition = self.condition
|
|
163
|
+
if isinstance(value, sp.Basic):
|
|
164
|
+
value = value.xreplace(repl)
|
|
165
|
+
if isinstance(condition, sp.Basic):
|
|
166
|
+
condition = condition.xreplace(repl)
|
|
167
|
+
return value, condition
|
|
168
|
+
|
|
169
|
+
def matches(self, expr, *, var=None, mode=None) -> bool:
|
|
170
|
+
_, patterns, case_mode, var_token = self.signature
|
|
171
|
+
args = getattr(expr, "args", ())
|
|
172
|
+
if patterns and len(patterns) != len(args):
|
|
173
|
+
return False
|
|
174
|
+
for token, actual in zip(patterns, args, strict=False):
|
|
175
|
+
if not _literal_pattern_matches(token, actual):
|
|
176
|
+
return False
|
|
177
|
+
if mode in ("real", "complex") and case_mode not in (None, "any", mode):
|
|
178
|
+
return False
|
|
179
|
+
if var is not None and var_token is not None:
|
|
180
|
+
name = _pattern_name(var_token)
|
|
181
|
+
if name:
|
|
182
|
+
mapped = self.substitutions(expr).get(Symbol(name))
|
|
183
|
+
if mapped is not None and mapped != var:
|
|
184
|
+
return False
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
def specificity(self, *, mode=None) -> int:
|
|
188
|
+
_, patterns, case_mode, _ = self.signature
|
|
189
|
+
score = 0
|
|
190
|
+
for token in patterns:
|
|
191
|
+
t = token.strip()
|
|
192
|
+
if ":" in t:
|
|
193
|
+
score += 3
|
|
194
|
+
elif re.search(r"_(Integer|Real|Complex|Positive|NonNegative)", t):
|
|
195
|
+
score += 2
|
|
196
|
+
elif "_" not in t:
|
|
197
|
+
score += 4
|
|
198
|
+
if mode in ("real", "complex") and case_mode == mode:
|
|
199
|
+
score += 2
|
|
200
|
+
return score
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass(frozen=True)
|
|
204
|
+
class Family:
|
|
205
|
+
raw: dict[str, Any]
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def id(self):
|
|
209
|
+
return self.raw["family_id"]
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def sympy_names(self):
|
|
213
|
+
return tuple(self.raw.get("sympy_names", ()))
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def mpmath_names(self):
|
|
217
|
+
return tuple(self.raw.get("mpmath_names", ()))
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def catalogue_names(self):
|
|
221
|
+
return tuple(self.raw.get("catalogue_names", ()))
|
|
222
|
+
|
|
223
|
+
def cases(self, prop: str) -> tuple[RuleCase, ...]:
|
|
224
|
+
p = self.raw.get("properties", {}).get(prop, {})
|
|
225
|
+
return tuple(
|
|
226
|
+
RuleCase(
|
|
227
|
+
**{
|
|
228
|
+
k: c.get(k)
|
|
229
|
+
for k in (
|
|
230
|
+
"value_source",
|
|
231
|
+
"condition_source",
|
|
232
|
+
"source_property_type",
|
|
233
|
+
"source_lhs",
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
for c in p.get("cases", ())
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class FunctionRegistry:
|
|
242
|
+
def __init__(self, records: Iterable[dict[str, Any]]):
|
|
243
|
+
self._families = tuple(Family(r) for r in records)
|
|
244
|
+
self._by_name = {}
|
|
245
|
+
for fam in self._families:
|
|
246
|
+
for n in (
|
|
247
|
+
fam.id,
|
|
248
|
+
*fam.sympy_names,
|
|
249
|
+
*fam.mpmath_names,
|
|
250
|
+
*fam.catalogue_names,
|
|
251
|
+
):
|
|
252
|
+
self._by_name.setdefault(n, fam)
|
|
253
|
+
|
|
254
|
+
def __len__(self):
|
|
255
|
+
return len(self._families)
|
|
256
|
+
|
|
257
|
+
def __iter__(self):
|
|
258
|
+
return iter(self._families)
|
|
259
|
+
|
|
260
|
+
def get(self, name: str) -> Family | None:
|
|
261
|
+
return self._by_name.get(name)
|
|
262
|
+
|
|
263
|
+
def require(self, name: str) -> Family:
|
|
264
|
+
f = self.get(name)
|
|
265
|
+
if f is None:
|
|
266
|
+
raise KeyError(name)
|
|
267
|
+
return f
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@lru_cache(maxsize=1)
|
|
271
|
+
def registry() -> FunctionRegistry:
|
|
272
|
+
p = files("funcprops").joinpath("data/catalogue.json")
|
|
273
|
+
payload = json.loads(p.read_text())
|
|
274
|
+
return FunctionRegistry(payload["families"])
|