scikit-verify 0.1.0__tar.gz
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.
- scikit_verify-0.1.0/.github/workflows/ci.yml +24 -0
- scikit_verify-0.1.0/.gitignore +10 -0
- scikit_verify-0.1.0/FAILURES.md +44 -0
- scikit_verify-0.1.0/LICENSE +28 -0
- scikit_verify-0.1.0/LOOP_DOMAINS.md +103 -0
- scikit_verify-0.1.0/PKG-INFO +145 -0
- scikit_verify-0.1.0/README.md +108 -0
- scikit_verify-0.1.0/coverage/cvxpy_full.out +16 -0
- scikit_verify-0.1.0/coverage/cvxpy_full.py +138 -0
- scikit_verify-0.1.0/coverage/make_coverage.py +63 -0
- scikit_verify-0.1.0/coverage/numpy_dialect.out +27 -0
- scikit_verify-0.1.0/coverage/numpy_dialect.py +218 -0
- scikit_verify-0.1.0/coverage/numpy_full.out +10 -0
- scikit_verify-0.1.0/coverage/numpy_full.py +154 -0
- scikit_verify-0.1.0/coverage/scale_min.py +18 -0
- scikit_verify-0.1.0/coverage/scipy_full.out +8 -0
- scikit_verify-0.1.0/coverage/scipy_full.py +82 -0
- scikit_verify-0.1.0/coverage/skl_full.out +15 -0
- scikit_verify-0.1.0/coverage/skl_full.py +155 -0
- scikit_verify-0.1.0/coverage/sm_full.out +11 -0
- scikit_verify-0.1.0/coverage/sm_full.py +121 -0
- scikit_verify-0.1.0/coverage/wild_100.out +61 -0
- scikit_verify-0.1.0/coverage/wild_100.py +293 -0
- scikit_verify-0.1.0/coverage/wild_sample.out +11 -0
- scikit_verify-0.1.0/coverage/wild_sample.py +179 -0
- scikit_verify-0.1.0/doc/coverage.md +53 -0
- scikit_verify-0.1.0/doc/logos/scikit-verify-lockup.svg +11 -0
- scikit_verify-0.1.0/doc/sharp-bits.md +80 -0
- scikit_verify-0.1.0/examples/README.md +59 -0
- scikit_verify-0.1.0/examples/corpus.py +165 -0
- scikit_verify-0.1.0/examples/demo.ipynb +1046 -0
- scikit_verify-0.1.0/examples/knot_debugging.ipynb +294 -0
- scikit_verify-0.1.0/examples/markov_birth_death.py +44 -0
- scikit_verify-0.1.0/examples/markov_walk_2d.py +50 -0
- scikit_verify-0.1.0/examples/ridgecv_decision.ipynb +266 -0
- scikit_verify-0.1.0/examples/sabotage_audit.ipynb +401 -0
- scikit_verify-0.1.0/pyproject.toml +80 -0
- scikit_verify-0.1.0/scikit_verify.egg-info/PKG-INFO +145 -0
- scikit_verify-0.1.0/scikit_verify.egg-info/SOURCES.txt +119 -0
- scikit_verify-0.1.0/scikit_verify.egg-info/dependency_links.txt +1 -0
- scikit_verify-0.1.0/scikit_verify.egg-info/entry_points.txt +2 -0
- scikit_verify-0.1.0/scikit_verify.egg-info/requires.txt +14 -0
- scikit_verify-0.1.0/scikit_verify.egg-info/scm_file_list.json +115 -0
- scikit_verify-0.1.0/scikit_verify.egg-info/scm_version.json +8 -0
- scikit_verify-0.1.0/scikit_verify.egg-info/top_level.txt +3 -0
- scikit_verify-0.1.0/setup.cfg +4 -0
- scikit_verify-0.1.0/skverify/__init__.py +73 -0
- scikit_verify-0.1.0/skverify/api.py +488 -0
- scikit_verify-0.1.0/skverify/atoms.py +448 -0
- scikit_verify-0.1.0/skverify/checks.py +189 -0
- scikit_verify-0.1.0/skverify/coercion.py +223 -0
- scikit_verify-0.1.0/skverify/contracts.py +346 -0
- scikit_verify-0.1.0/skverify/derivation.py +393 -0
- scikit_verify-0.1.0/skverify/dialect.py +118 -0
- scikit_verify-0.1.0/skverify/helpers.py +72 -0
- scikit_verify-0.1.0/skverify/instrument/__init__.py +44 -0
- scikit_verify-0.1.0/skverify/instrument/registries.py +109 -0
- scikit_verify-0.1.0/skverify/instrument/rewriter.py +325 -0
- scikit_verify-0.1.0/skverify/instrument/runtime.py +561 -0
- scikit_verify-0.1.0/skverify/instrument/triage.py +437 -0
- scikit_verify-0.1.0/skverify/instrument/twins.py +469 -0
- scikit_verify-0.1.0/skverify/maps/__init__.py +0 -0
- scikit_verify-0.1.0/skverify/maps/numpy.py +1587 -0
- scikit_verify-0.1.0/skverify/maps/special.py +81 -0
- scikit_verify-0.1.0/skverify/pair.py +2211 -0
- scikit_verify-0.1.0/skverify/recurrence.py +668 -0
- scikit_verify-0.1.0/skverify/registry.py +2 -0
- scikit_verify-0.1.0/skverify/session.py +91 -0
- scikit_verify-0.1.0/skverify/sets.py +151 -0
- scikit_verify-0.1.0/skverify-hypothesis/README.md +50 -0
- scikit_verify-0.1.0/skverify-hypothesis/__init__.py +3 -0
- scikit_verify-0.1.0/skverify-hypothesis/skverify_hypothesis.py +239 -0
- scikit_verify-0.1.0/skverify-mcp/README.md +24 -0
- scikit_verify-0.1.0/skverify-mcp/__init__.py +3 -0
- scikit_verify-0.1.0/skverify-mcp/server.py +124 -0
- scikit_verify-0.1.0/smoke.py +119 -0
- scikit_verify-0.1.0/tests/api/test_api.py +28 -0
- scikit_verify-0.1.0/tests/api/test_helpers.py +125 -0
- scikit_verify-0.1.0/tests/api/test_recompress.py +217 -0
- scikit_verify-0.1.0/tests/api/test_ux.py +41 -0
- scikit_verify-0.1.0/tests/atoms/test_contracts_core.py +60 -0
- scikit_verify-0.1.0/tests/atoms/test_opaque.py +117 -0
- scikit_verify-0.1.0/tests/atoms/test_rng.py +89 -0
- scikit_verify-0.1.0/tests/atoms/test_storage.py +63 -0
- scikit_verify-0.1.0/tests/checks/test_checks.py +141 -0
- scikit_verify-0.1.0/tests/checks/test_nan_family.py +75 -0
- scikit_verify-0.1.0/tests/checks/test_verification.py +62 -0
- scikit_verify-0.1.0/tests/checks/test_wild_mechanisms.py +89 -0
- scikit_verify-0.1.0/tests/coercion/test_no_silent_constants.py +101 -0
- scikit_verify-0.1.0/tests/coercion/test_tracing_minimal.py +231 -0
- scikit_verify-0.1.0/tests/conftest.py +20 -0
- scikit_verify-0.1.0/tests/derivation/test_steps.py +256 -0
- scikit_verify-0.1.0/tests/instrument/test_classes.py +33 -0
- scikit_verify-0.1.0/tests/instrument/test_guards.py +104 -0
- scikit_verify-0.1.0/tests/instrument/test_mask_fusion.py +165 -0
- scikit_verify-0.1.0/tests/instrument/test_recurrence.py +182 -0
- scikit_verify-0.1.0/tests/instrument/test_session.py +47 -0
- scikit_verify-0.1.0/tests/integration/test_battery.py +73 -0
- scikit_verify-0.1.0/tests/integration/test_distributions.py +190 -0
- scikit_verify-0.1.0/tests/integration/test_fuzz.py +193 -0
- scikit_verify-0.1.0/tests/integration/test_gate.py +42 -0
- scikit_verify-0.1.0/tests/integration/test_gate_interp.py +35 -0
- scikit_verify-0.1.0/tests/integration/test_gate_lsq.py +44 -0
- scikit_verify-0.1.0/tests/integration/test_gate_ols.py +29 -0
- scikit_verify-0.1.0/tests/integration/test_kernels.py +43 -0
- scikit_verify-0.1.0/tests/integration/test_sklearn_svm.py +37 -0
- scikit_verify-0.1.0/tests/integration/test_special_map.py +64 -0
- scikit_verify-0.1.0/tests/pair/test_broadcast.py +99 -0
- scikit_verify-0.1.0/tests/pair/test_capture.py +37 -0
- scikit_verify-0.1.0/tests/pair/test_functions.py +66 -0
- scikit_verify-0.1.0/tests/pair/test_getitem_nd.py +215 -0
- scikit_verify-0.1.0/tests/pair/test_inplace.py +61 -0
- scikit_verify-0.1.0/tests/pair/test_matmul.py +181 -0
- scikit_verify-0.1.0/tests/pair/test_relational.py +229 -0
- scikit_verify-0.1.0/tests/pair/test_scalar.py +117 -0
- scikit_verify-0.1.0/tests/pair/test_setitem.py +224 -0
- scikit_verify-0.1.0/tests/pair/test_sets.py +52 -0
- scikit_verify-0.1.0/tests/pair/test_slicing.py +109 -0
- scikit_verify-0.1.0/tests/pair/test_strides.py +119 -0
- scikit_verify-0.1.0/tests/pair/test_sum_axis.py +119 -0
- scikit_verify-0.1.0/tests/pair/test_ufuncs.py +82 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.14"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- name: Install
|
|
21
|
+
run: pip install -e .[dev]
|
|
22
|
+
- name: Test
|
|
23
|
+
run: pytest -q
|
|
24
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Coercion break-pass — failure list (lifting only, no fixes)
|
|
2
|
+
~200 adversarial cases, 6 rounds. Verified formulas track perturbed inputs in 24/25 spot checks.
|
|
3
|
+
Scripts: /tmp/break/round{1..6}.py (repro: run any with the scipy-dev python)
|
|
4
|
+
|
|
5
|
+
## A. SILENT-WRONG (value or formula lies — worst class)
|
|
6
|
+
1. np.add.at(y, [0,0], 1.0) -> silently NO-OPS on a traced array (got 1.5, expect 3.5)
|
|
7
|
+
2. np.place(y, y<0, [0.0]) -> silently NO-OPS (got 3.25, expect 5.25)
|
|
8
|
+
3. np.nan_to_num(x/0.0) -> inf NOT clamped: value lane -inf vs numpy's ±1.798e308
|
|
9
|
+
4. pandas Series(x).sum() -> returns the full array instead of the scalar sum
|
|
10
|
+
5. int scalar input: to_sympy(lambda a: a+1, 3) -> formula "4", input symbol dropped
|
|
11
|
+
(float scalar input correctly gives a+1; only python ints collapse)
|
|
12
|
+
6. nditer over object array -> runs but formula constant
|
|
13
|
+
|
|
14
|
+
## B. DIED (should refuse loudly or work)
|
|
15
|
+
7. round(pair, n) / round(pair) TypeError: no __round__
|
|
16
|
+
8. divmod(pair, 2.0) TypeError
|
|
17
|
+
9. f"{pair:.2f}" / format(pair) TypeError: unsupported format string
|
|
18
|
+
10. float(str(pair)) ValueError (repr leaks "Pair(x[0])")
|
|
19
|
+
11. np.interp(t, xs, ys) traced xs TypeError: cannot cast dtype('O')
|
|
20
|
+
12. np.corrcoef(x, x[::-1]) TypeError: scalar Pair not subscriptable
|
|
21
|
+
13. np.cov(x) ValueError: unpack
|
|
22
|
+
14. pair // 2.0 (floor div) TypeError: no __floordiv__
|
|
23
|
+
15. np.select([conds],[choices]) TypeError: condlist must be bool ndarray
|
|
24
|
+
16. np.linalg.inv / eigvals on Pair AttributeError: no __array_wrap__
|
|
25
|
+
17. np.putmask(pair, ...) TypeError: first arg must be array
|
|
26
|
+
18. np.copyto(y, pair) TypeError
|
|
27
|
+
19. pair.fill(2.0) AttributeError: no fill
|
|
28
|
+
20. pair.tolist() AttributeError: no tolist
|
|
29
|
+
21. scipy.stats.iqr(x) ValueError: broadcast remap
|
|
30
|
+
22. varargs signature def f(*arrs) TypeError: takes 1 arguments, got 2 (api wrapper)
|
|
31
|
+
|
|
32
|
+
## C. Loud refusals — correct behavior, listed for coverage review
|
|
33
|
+
- np.round (math-changing: right), np.piecewise (bool coercion), np.histogram(raw operand msg misleads: input WAS wrapped)
|
|
34
|
+
- ufunc.accumulate / .outer / .reduceat / out= / frexp / modf (2-output)
|
|
35
|
+
- astype(str), structured arrays (fancy indexing msg misleading)
|
|
36
|
+
- pandas DataFrame ops (data-dependent branch)
|
|
37
|
+
- chained float() in user code (right; message good)
|
|
38
|
+
|
|
39
|
+
## D. Notes / oddities (not failures, worth eyes)
|
|
40
|
+
- weighted_mean formula contains disclosed const_0 table -> substitution needs table values (by design)
|
|
41
|
+
- np.sort(x)[1] etc: all ordering-guard paths verified correct incl. preconditions gating
|
|
42
|
+
- empty array: Sum(a[j], (j,0,-1)) — correct empty-sum convention, prints oddly
|
|
43
|
+
- f(x, x) same array twice -> named a and b independently (defensible; loses aliasing info)
|
|
44
|
+
- kwarg scalars become symbols (scale*a[i]) — nice, but means kwargs are never constant-folded
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Aadya Chinubhai
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Loops as domains
|
|
2
|
+
|
|
3
|
+
A loop is an axis in time. Array axes are already domains: `x[i]` with
|
|
4
|
+
bounds, bound by `Sum(..., (i, 0, n-1))`. An iteration index `k` is the
|
|
5
|
+
same object, bound by a recurrence instead of a reduction. Today the
|
|
6
|
+
tracer unrolls loops into the live formula, so iterative solvers
|
|
7
|
+
(BayesianRidge: 300 iterations, each formula containing the last)
|
|
8
|
+
snowball and never finish. Folding exists (`_fold_runs`,
|
|
9
|
+
`derivation()`) but runs post-hoc on `.steps` -- downstream of the
|
|
10
|
+
blowup. This note moves the fold into the formula lane itself.
|
|
11
|
+
|
|
12
|
+
Everything below is explicitly sympy: `subs`, `doit`, `lambdify`,
|
|
13
|
+
`free_symbols` work on the certificate with no side-channel records.
|
|
14
|
+
All claims execution-verified on sympy 1.14.
|
|
15
|
+
|
|
16
|
+
## The three tiers
|
|
17
|
+
|
|
18
|
+
| loop shape | sympy object | verified behavior |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| accumulator `t += x[k]` | `Sum` / `Product` | already emitted for np.sum; hand loops should reach it |
|
|
21
|
+
| linear scalar recurrence | `rsolve` -> closed expression | `rsolve(C(k+1)-2C(k)-1, C(k), {C(0):1})` -> `2**(k+1)-1` |
|
|
22
|
+
| linear VECTOR recurrence | `MatPow`: `C[k] = A**k * C[0]` | `(A**k).doit()` diagonalizes symbolically; `subs(k,3)` exact |
|
|
23
|
+
| general scalar recurrence | `RecursiveSeq` | Basic; `subs` traverses; `.coeff(k)` unrolls lazily |
|
|
24
|
+
| general vector / coupled | custom `Iterate` Function (below) | held form, lazy unroll, Tuple state |
|
|
25
|
+
|
|
26
|
+
## Verified capabilities and limits
|
|
27
|
+
|
|
28
|
+
RecursiveSeq (sympy.series.sequences):
|
|
29
|
+
- IS `Basic`; `free_symbols` correct; symbolic parameters in the body
|
|
30
|
+
survive (`coeff(3)` of `y=2y+a` -> `7*a + 8`); `subs` composes.
|
|
31
|
+
- Scalar ONLY: Matrix and MatrixSymbol terms fail (AttributeError /
|
|
32
|
+
ShapeError). Tuple packing fails. A second Function in the body
|
|
33
|
+
stays unresolved (`z(0), z(1)` free) -- no coupled systems.
|
|
34
|
+
|
|
35
|
+
MatPow:
|
|
36
|
+
- `A**k` with symbolic integer `k` is native, `.doit()` produces the
|
|
37
|
+
closed form via diagonalization, exact under `subs(k, n)`.
|
|
38
|
+
- Covers every CONSTANT-coefficient linear vector recurrence.
|
|
39
|
+
|
|
40
|
+
Iterate (ours, ~10 lines):
|
|
41
|
+
```python
|
|
42
|
+
class Iterate(sympy.Function):
|
|
43
|
+
"""Iterate(step, init, k): k-fold application of step to init."""
|
|
44
|
+
@classmethod
|
|
45
|
+
def eval(cls, step, init, k):
|
|
46
|
+
if k.is_Integer and k >= 0:
|
|
47
|
+
out = init
|
|
48
|
+
for _ in range(int(k)):
|
|
49
|
+
out = step(*out) if isinstance(out, sympy.Tuple) else step(out)
|
|
50
|
+
return out
|
|
51
|
+
```
|
|
52
|
+
- Held while `k` is symbolic: `Iterate(Lambda(c, 2c+a), 1, K)`,
|
|
53
|
+
free symbols `{a, K}`. `subs(K, 3)` unrolls on demand -> `7*a + 8`;
|
|
54
|
+
after full substitution it is a number (lambdify-able).
|
|
55
|
+
- Vector and COUPLED state as one `Tuple`:
|
|
56
|
+
`Iterate(Lambda((c1,c2), Tuple(c1+c2, c1*a)), Tuple(1,2), K)`
|
|
57
|
+
-> `subs(K,2)` -> `(a+3, 3*a)`. This is the BayesianRidge shape
|
|
58
|
+
(coef and alpha updating each other).
|
|
59
|
+
- A subclass of `sympy.Function` IS sympy: printing, traversal,
|
|
60
|
+
substitution all come from Basic. No metadata channel.
|
|
61
|
+
|
|
62
|
+
## Trace-time mechanics
|
|
63
|
+
|
|
64
|
+
The hooks exist. The rewriter already tags every loop
|
|
65
|
+
(`__skv_loop_iter__` / `__skv_loop_end__`, `session.loop_events`), so
|
|
66
|
+
Pairs are attributable to (loop, iteration) DURING the trace.
|
|
67
|
+
|
|
68
|
+
1. Run iteration 0 and 1 concretely, formulas eager (today's path).
|
|
69
|
+
2. At iteration 1's end, attempt the fold: do iteration 1's formulas
|
|
70
|
+
equal iteration 0's under a template with the loop-carried state
|
|
71
|
+
replaced by a state symbol? This is `_generalize`'s check, applied
|
|
72
|
+
live. Multiple carried variables pack into one Tuple state.
|
|
73
|
+
3. Fold succeeds: replace the carried Pairs' formulas with the tier
|
|
74
|
+
object (Sum/rsolve/MatPow/RecursiveSeq/Iterate) and run remaining
|
|
75
|
+
iterations on the VALUE LANE ONLY, verifying each iteration's
|
|
76
|
+
concrete values against the template (the per-iteration residual --
|
|
77
|
+
same role contracts play for atoms). Formula size is now
|
|
78
|
+
O(template), independent of iteration count.
|
|
79
|
+
4. Fold fails (body not one template -- data-dependent branch inside):
|
|
80
|
+
keep today's unrolling, with a formula-size budget that refuses
|
|
81
|
+
loudly instead of hanging.
|
|
82
|
+
|
|
83
|
+
Data-dependent exits (`while not converged`) are already guards: the
|
|
84
|
+
stopping condition's `__bool__` records the path. `K = 17` enters the
|
|
85
|
+
formula as a concrete bound with the convergence guard in
|
|
86
|
+
preconditions, exactly like searchsorted's counting bound.
|
|
87
|
+
|
|
88
|
+
## Verification story
|
|
89
|
+
|
|
90
|
+
- Tier objects evaluate: `.coeff(k)` / `subs(K, k)` reproduce the
|
|
91
|
+
unrolled formula for any k, so the two-lane fuzzer applies directly.
|
|
92
|
+
- Per-iteration residual at trace time: template instantiated with
|
|
93
|
+
iteration k's concrete state must equal iteration k+1's concrete
|
|
94
|
+
state. 300 cheap numeric checks replace one 300-deep expression.
|
|
95
|
+
|
|
96
|
+
## Refusals (loud, by construction)
|
|
97
|
+
|
|
98
|
+
- Loop body changes shape between iterations (branch taken differently)
|
|
99
|
+
and sizes exceed the budget -> "iterative body is not one template".
|
|
100
|
+
- `rsolve` fails and the recurrence is nonlinear scalar -> RecursiveSeq
|
|
101
|
+
(still exact); vector -> Iterate (still exact). Refusal is only for
|
|
102
|
+
non-foldable bodies, never for "no closed form" -- a held recurrence
|
|
103
|
+
IS an exact formula.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scikit-verify
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lift NumPy/SciPy code to the mathematics it implements, and verify it.
|
|
5
|
+
Author: Aadya Chinubhai
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/aadya940/scikit-verify
|
|
8
|
+
Project-URL: Issues, https://github.com/aadya940/scikit-verify/issues
|
|
9
|
+
Keywords: verification,symbolic,sympy,numpy,scipy,numerical-analysis,scientific-computing
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: numpy>=1.26
|
|
26
|
+
Requires-Dist: sympy>=1.12
|
|
27
|
+
Provides-Extra: mcp
|
|
28
|
+
Requires-Dist: mcp<2,>=1.2; extra == "mcp"
|
|
29
|
+
Provides-Extra: hypothesis
|
|
30
|
+
Requires-Dist: hypothesis>=6; extra == "hypothesis"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
33
|
+
Requires-Dist: hypothesis>=6; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff; extra == "dev"
|
|
35
|
+
Requires-Dist: scipy>=1.12; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<img src="doc/logos/scikit-verify-lockup.svg" alt="scikit-verify" width="380">
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<p align="center">Translate Python and NumPy programs to symbolic mathematics</p>
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
* [Source code](https://github.com/aadya940/scikit-verify)
|
|
47
|
+
* [Coverage](doc/coverage.md)
|
|
48
|
+
* [License](https://github.com/aadya940/scikit-verify/blob/master/LICENSE)
|
|
49
|
+
* [skverify-mcp](skverify-mcp/) - MCP for mathematical feedback for coding agents
|
|
50
|
+
* [skverify-hypothesis](skverify-hypothesis/) - find every branch, boundary and edge case of your function with Hypothesis
|
|
51
|
+
|
|
52
|
+
scikit-verify is a tracer for numerical Python. It runs your NumPy
|
|
53
|
+
function once and returns the formula it computed, as an ordinary SymPy
|
|
54
|
+
expression you can read, simplify, compare against a paper, or evaluate
|
|
55
|
+
at any other input. Your code is not modified or annotated. For example:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import numpy as np
|
|
59
|
+
from skverify import to_sympy
|
|
60
|
+
|
|
61
|
+
def weighted_rms(x, w):
|
|
62
|
+
return np.sqrt(np.sum(w * x**2) / np.sum(w))
|
|
63
|
+
|
|
64
|
+
out = to_sympy(weighted_rms, np.array([1.0, 2.0, 3.0]), np.array([0.5, 0.3, 0.2]))
|
|
65
|
+
|
|
66
|
+
out.formula
|
|
67
|
+
# sqrt(Sum(w[j]*x[j]**2, (j, 0, 2))/Sum(w[j], (j, 0, 2)))
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Every formula comes as a certificate: the expression, plus the
|
|
71
|
+
assumptions it was derived under. When code branches on your data, the
|
|
72
|
+
branch taken becomes a stated hypothesis instead of a hidden one:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
out = to_sympy(np.median, np.array([3.0, 1.0, 4.0, 1.5]))
|
|
76
|
+
print(out.pretty())
|
|
77
|
+
|
|
78
|
+
# formula = a[0]/2 + a[3]/2
|
|
79
|
+
# assumes[0] = a[0] <= a[2]
|
|
80
|
+
# assumes[1] = a[1] <= a[3]
|
|
81
|
+
# assumes[2] = a[3] <= a[0]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The contract is exact-or-refuse. If an operation has no faithful
|
|
85
|
+
symbolic form, scikit-verify raises instead of guessing:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
to_sympy(lambda a: a.astype(int).mean(), np.array([1.4, 2.6]))
|
|
89
|
+
# NotImplementedError: astype to non-float would change the math
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This works on real library code, not just kernels: scikit-learn metrics
|
|
93
|
+
come back as their defining formulas (precision as its ratio of counting
|
|
94
|
+
sums), fitted estimators as their closed forms, iterative solvers as
|
|
95
|
+
held recurrences, and compiled routines (LAPACK, FFT, Cython) as named
|
|
96
|
+
terms that are checked against their defining equations on every call:
|
|
97
|
+
svd against U diag(S) Vh = A, fft against the DFT sum itself.
|
|
98
|
+
|
|
99
|
+
Randomness stays honest too. A draw like ``rng.normal(0, s)`` enters
|
|
100
|
+
the formula as a random variable with that distribution, so
|
|
101
|
+
``sympy.stats.E`` and ``variance`` of the result compute in closed
|
|
102
|
+
form, while the concrete run keeps the exact numbers drawn.
|
|
103
|
+
|
|
104
|
+
Tested against numpy, scipy, scikit-learn, statsmodels, cvxpy and
|
|
105
|
+
random research code from GitHub; the boards in [coverage](coverage/)
|
|
106
|
+
regenerate every number.
|
|
107
|
+
|
|
108
|
+
## Installation
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pip install scikit-verify
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Requires Python >= 3.11, `numpy`, and `sympy`. The import name is
|
|
115
|
+
`skverify`. The companion layers install as extras:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install "scikit-verify[mcp]" # MCP server for coding agents
|
|
119
|
+
pip install "scikit-verify[hypothesis]" # testing helpers
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Pre-alpha; the API may change. Iterative solvers at real sizes can be
|
|
123
|
+
slow to trace (minutes, not wrong); the boards in coverage/ carry
|
|
124
|
+
timings.
|
|
125
|
+
|
|
126
|
+
## Lineage
|
|
127
|
+
|
|
128
|
+
The ideas here are old and good. Pairing a concrete execution with a
|
|
129
|
+
symbolic one is King's symbolic execution (CACM 1976), run in the
|
|
130
|
+
concolic style of Cadar and Sen. Checking a compiled routine's answer
|
|
131
|
+
against its defining equation, instead of trusting its name, is
|
|
132
|
+
Blum and Kannan's result checking (1989). Folding a long trace back
|
|
133
|
+
into its loop structure follows Larus's whole-program paths (PLDI
|
|
134
|
+
1999), with templates recovered by Plotkin's anti-unification (1970).
|
|
135
|
+
The stance that code verification means checking code against the
|
|
136
|
+
mathematics it claims to implement is Oberkampf and Roy's (2010).
|
|
137
|
+
Verified lifting of stencils to summaries was developed by Kamil et
|
|
138
|
+
al. (PLDI 2016) for performance; scikit-verify lifts for correctness.
|
|
139
|
+
Converting NumPy to SymPy was wished for in
|
|
140
|
+
[sympy#2810](https://github.com/sympy/sympy/issues/2810) (2014).
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
BSD-3-Clause. scikit-verify is an independent project and is not affiliated
|
|
145
|
+
with the SciPy developers.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="doc/logos/scikit-verify-lockup.svg" alt="scikit-verify" width="380">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">Translate Python and NumPy programs to symbolic mathematics</p>
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
* [Source code](https://github.com/aadya940/scikit-verify)
|
|
10
|
+
* [Coverage](doc/coverage.md)
|
|
11
|
+
* [License](https://github.com/aadya940/scikit-verify/blob/master/LICENSE)
|
|
12
|
+
* [skverify-mcp](skverify-mcp/) - MCP for mathematical feedback for coding agents
|
|
13
|
+
* [skverify-hypothesis](skverify-hypothesis/) - find every branch, boundary and edge case of your function with Hypothesis
|
|
14
|
+
|
|
15
|
+
scikit-verify is a tracer for numerical Python. It runs your NumPy
|
|
16
|
+
function once and returns the formula it computed, as an ordinary SymPy
|
|
17
|
+
expression you can read, simplify, compare against a paper, or evaluate
|
|
18
|
+
at any other input. Your code is not modified or annotated. For example:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import numpy as np
|
|
22
|
+
from skverify import to_sympy
|
|
23
|
+
|
|
24
|
+
def weighted_rms(x, w):
|
|
25
|
+
return np.sqrt(np.sum(w * x**2) / np.sum(w))
|
|
26
|
+
|
|
27
|
+
out = to_sympy(weighted_rms, np.array([1.0, 2.0, 3.0]), np.array([0.5, 0.3, 0.2]))
|
|
28
|
+
|
|
29
|
+
out.formula
|
|
30
|
+
# sqrt(Sum(w[j]*x[j]**2, (j, 0, 2))/Sum(w[j], (j, 0, 2)))
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Every formula comes as a certificate: the expression, plus the
|
|
34
|
+
assumptions it was derived under. When code branches on your data, the
|
|
35
|
+
branch taken becomes a stated hypothesis instead of a hidden one:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
out = to_sympy(np.median, np.array([3.0, 1.0, 4.0, 1.5]))
|
|
39
|
+
print(out.pretty())
|
|
40
|
+
|
|
41
|
+
# formula = a[0]/2 + a[3]/2
|
|
42
|
+
# assumes[0] = a[0] <= a[2]
|
|
43
|
+
# assumes[1] = a[1] <= a[3]
|
|
44
|
+
# assumes[2] = a[3] <= a[0]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The contract is exact-or-refuse. If an operation has no faithful
|
|
48
|
+
symbolic form, scikit-verify raises instead of guessing:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
to_sympy(lambda a: a.astype(int).mean(), np.array([1.4, 2.6]))
|
|
52
|
+
# NotImplementedError: astype to non-float would change the math
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This works on real library code, not just kernels: scikit-learn metrics
|
|
56
|
+
come back as their defining formulas (precision as its ratio of counting
|
|
57
|
+
sums), fitted estimators as their closed forms, iterative solvers as
|
|
58
|
+
held recurrences, and compiled routines (LAPACK, FFT, Cython) as named
|
|
59
|
+
terms that are checked against their defining equations on every call:
|
|
60
|
+
svd against U diag(S) Vh = A, fft against the DFT sum itself.
|
|
61
|
+
|
|
62
|
+
Randomness stays honest too. A draw like ``rng.normal(0, s)`` enters
|
|
63
|
+
the formula as a random variable with that distribution, so
|
|
64
|
+
``sympy.stats.E`` and ``variance`` of the result compute in closed
|
|
65
|
+
form, while the concrete run keeps the exact numbers drawn.
|
|
66
|
+
|
|
67
|
+
Tested against numpy, scipy, scikit-learn, statsmodels, cvxpy and
|
|
68
|
+
random research code from GitHub; the boards in [coverage](coverage/)
|
|
69
|
+
regenerate every number.
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install scikit-verify
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Requires Python >= 3.11, `numpy`, and `sympy`. The import name is
|
|
78
|
+
`skverify`. The companion layers install as extras:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install "scikit-verify[mcp]" # MCP server for coding agents
|
|
82
|
+
pip install "scikit-verify[hypothesis]" # testing helpers
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Pre-alpha; the API may change. Iterative solvers at real sizes can be
|
|
86
|
+
slow to trace (minutes, not wrong); the boards in coverage/ carry
|
|
87
|
+
timings.
|
|
88
|
+
|
|
89
|
+
## Lineage
|
|
90
|
+
|
|
91
|
+
The ideas here are old and good. Pairing a concrete execution with a
|
|
92
|
+
symbolic one is King's symbolic execution (CACM 1976), run in the
|
|
93
|
+
concolic style of Cadar and Sen. Checking a compiled routine's answer
|
|
94
|
+
against its defining equation, instead of trusting its name, is
|
|
95
|
+
Blum and Kannan's result checking (1989). Folding a long trace back
|
|
96
|
+
into its loop structure follows Larus's whole-program paths (PLDI
|
|
97
|
+
1999), with templates recovered by Plotkin's anti-unification (1970).
|
|
98
|
+
The stance that code verification means checking code against the
|
|
99
|
+
mathematics it claims to implement is Oberkampf and Roy's (2010).
|
|
100
|
+
Verified lifting of stencils to summaries was developed by Kamil et
|
|
101
|
+
al. (PLDI 2016) for performance; scikit-verify lifts for correctness.
|
|
102
|
+
Converting NumPy to SymPy was wished for in
|
|
103
|
+
[sympy#2810](https://github.com/sympy/sympy/issues/2810) (2014).
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
BSD-3-Clause. scikit-verify is an independent project and is not affiliated
|
|
108
|
+
with the SciPy developers.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
TOTAL 38 | LIFT+match 29 | lift-unverified 0 | refused 9 | died 0
|
|
2
|
+
|
|
3
|
+
LIFT+match: sum_squares, norm1, norm2, norm_inf, pnorm_3, abs_sum, square_sum, sqrt_sum, power_sum, pos_sum, neg_sum, maximum_elem, max, min, logistic_sum, entr_sum, kl_div_sum, rel_entr_sum, log_sum_exp, geo_mean, harmonic_mean, huber_sum, trace, lambda_max, lambda_min, sigma_max, log_det, tv_1d, residual_norm
|
|
4
|
+
|
|
5
|
+
REFUSED:
|
|
6
|
+
quad_form [skverify] could not lift this call (ValueEr
|
|
7
|
+
quad_over_lin [skverify] could not lift this call (Attribu
|
|
8
|
+
matrix_frac [skverify] could not lift this call (ValueEr
|
|
9
|
+
normNuc np.sum kwargs ['initial'] not supported
|
|
10
|
+
solve_lstsq [skverify] could not lift this call (ValueEr
|
|
11
|
+
solve_ridge [skverify] could not lift this call (ValueEr
|
|
12
|
+
solve_lasso [skverify] could not lift this call (ValueEr
|
|
13
|
+
solve_nonneg_ls [skverify] could not lift this call (ValueEr
|
|
14
|
+
solve_chebyshev [skverify] could not lift this call (ValueEr
|
|
15
|
+
|
|
16
|
+
DIED:
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""The cvxpy sweep: expression evaluation and small solved problems."""
|
|
2
|
+
import signal
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
warnings.filterwarnings("ignore")
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
import cvxpy as cp
|
|
9
|
+
from skverify import to_sympy, Pair
|
|
10
|
+
|
|
11
|
+
rng = np.random.default_rng(7)
|
|
12
|
+
A = rng.standard_normal((6, 3))
|
|
13
|
+
b = rng.standard_normal(6)
|
|
14
|
+
v = rng.standard_normal(5)
|
|
15
|
+
vpos = np.abs(v) + 0.5
|
|
16
|
+
p = vpos / vpos.sum()
|
|
17
|
+
q = np.abs(rng.standard_normal(5)) + 0.5
|
|
18
|
+
q = q / q.sum()
|
|
19
|
+
S = A.T @ A + 0.5 * np.eye(3)
|
|
20
|
+
w = rng.standard_normal(3)
|
|
21
|
+
|
|
22
|
+
MENU = []
|
|
23
|
+
|
|
24
|
+
def add(name, fn, *args):
|
|
25
|
+
MENU.append((name, fn, args))
|
|
26
|
+
|
|
27
|
+
def val(expr):
|
|
28
|
+
return expr.value
|
|
29
|
+
|
|
30
|
+
# ---- atom evaluation: cvxpy expression on data, read the value
|
|
31
|
+
add("sum_squares", lambda a: val(cp.sum_squares(a)), v)
|
|
32
|
+
add("norm1", lambda a: val(cp.norm1(a)), v)
|
|
33
|
+
add("norm2", lambda a: val(cp.norm2(a)), v)
|
|
34
|
+
add("norm_inf", lambda a: val(cp.norm_inf(a)), v)
|
|
35
|
+
add("pnorm_3", lambda a: val(cp.pnorm(a, 3)), vpos)
|
|
36
|
+
add("abs_sum", lambda a: val(cp.sum(cp.abs(a))), v)
|
|
37
|
+
add("square_sum", lambda a: val(cp.sum(cp.square(a))), v)
|
|
38
|
+
add("sqrt_sum", lambda a: val(cp.sum(cp.sqrt(a))), vpos)
|
|
39
|
+
add("power_sum", lambda a: val(cp.sum(cp.power(a, 2))), v)
|
|
40
|
+
add("pos_sum", lambda a: val(cp.sum(cp.pos(a))), v)
|
|
41
|
+
add("neg_sum", lambda a: val(cp.sum(cp.neg(a))), v)
|
|
42
|
+
add("maximum_elem", lambda a, c: val(cp.sum(cp.maximum(a, c))), v, w[0])
|
|
43
|
+
add("max", lambda a: val(cp.max(a)), v)
|
|
44
|
+
add("min", lambda a: val(cp.min(a)), v)
|
|
45
|
+
add("logistic_sum", lambda a: val(cp.sum(cp.logistic(a))), v)
|
|
46
|
+
add("entr_sum", lambda a: val(cp.sum(cp.entr(a))), p)
|
|
47
|
+
add("kl_div_sum", lambda a, c: val(cp.sum(cp.kl_div(a, c))), p, q)
|
|
48
|
+
add("rel_entr_sum", lambda a, c: val(cp.sum(cp.rel_entr(a, c))), p, q)
|
|
49
|
+
add("log_sum_exp", lambda a: val(cp.log_sum_exp(a)), v)
|
|
50
|
+
add("geo_mean", lambda a: val(cp.geo_mean(a)), vpos)
|
|
51
|
+
add("harmonic_mean", lambda a: val(cp.harmonic_mean(a)), vpos)
|
|
52
|
+
add("huber_sum", lambda a: val(cp.sum(cp.huber(a, 1.0))), v)
|
|
53
|
+
add("quad_form", lambda a, s: val(cp.quad_form(a, s)), w, S)
|
|
54
|
+
add("quad_over_lin", lambda a, c: val(cp.quad_over_lin(a, c)), v, vpos[0])
|
|
55
|
+
add("matrix_frac", lambda a, s: val(cp.matrix_frac(a, s)), w, S)
|
|
56
|
+
add("trace", lambda s: val(cp.trace(s)), S)
|
|
57
|
+
add("lambda_max", lambda s: val(cp.lambda_max(s)), S)
|
|
58
|
+
add("lambda_min", lambda s: val(cp.lambda_min(s)), S)
|
|
59
|
+
add("sigma_max", lambda a: val(cp.sigma_max(a)), A)
|
|
60
|
+
add("normNuc", lambda a: val(cp.normNuc(a)), A)
|
|
61
|
+
add("log_det", lambda s: val(cp.log_det(s)), S)
|
|
62
|
+
add("tv_1d", lambda a: val(cp.tv(a)), v)
|
|
63
|
+
add("residual_norm", lambda a, c: val(cp.norm2(a @ np.ones(3) - c)), A, b)
|
|
64
|
+
|
|
65
|
+
# ---- solved problems: data -> optimizer
|
|
66
|
+
def lstsq(a, c):
|
|
67
|
+
x = cp.Variable(3)
|
|
68
|
+
cp.Problem(cp.Minimize(cp.sum_squares(a @ x - c))).solve()
|
|
69
|
+
return x.value
|
|
70
|
+
|
|
71
|
+
def ridge(a, c):
|
|
72
|
+
x = cp.Variable(3)
|
|
73
|
+
cp.Problem(cp.Minimize(cp.sum_squares(a @ x - c) + cp.sum_squares(x))).solve()
|
|
74
|
+
return x.value
|
|
75
|
+
|
|
76
|
+
def lasso(a, c):
|
|
77
|
+
x = cp.Variable(3)
|
|
78
|
+
cp.Problem(cp.Minimize(cp.sum_squares(a @ x - c) + 0.1 * cp.norm1(x))).solve()
|
|
79
|
+
return x.value
|
|
80
|
+
|
|
81
|
+
def nonneg_ls(a, c):
|
|
82
|
+
x = cp.Variable(3)
|
|
83
|
+
cp.Problem(cp.Minimize(cp.sum_squares(a @ x - c)), [x >= 0]).solve()
|
|
84
|
+
return x.value
|
|
85
|
+
|
|
86
|
+
def chebyshev(a, c):
|
|
87
|
+
x = cp.Variable(3)
|
|
88
|
+
cp.Problem(cp.Minimize(cp.norm_inf(a @ x - c))).solve()
|
|
89
|
+
return x.value
|
|
90
|
+
|
|
91
|
+
add("solve_lstsq", lstsq, A, b)
|
|
92
|
+
add("solve_ridge", ridge, A, b)
|
|
93
|
+
add("solve_lasso", lasso, A, b)
|
|
94
|
+
add("solve_nonneg_ls", nonneg_ls, A, b)
|
|
95
|
+
add("solve_chebyshev", chebyshev, A, b)
|
|
96
|
+
|
|
97
|
+
class TO(Exception):
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
signal.signal(signal.SIGALRM, lambda s, f: (_ for _ in ()).throw(TO()))
|
|
101
|
+
|
|
102
|
+
lift_ok, lift_unverified, refused, died = [], [], [], []
|
|
103
|
+
for name, fn, args in MENU:
|
|
104
|
+
signal.alarm(120)
|
|
105
|
+
try:
|
|
106
|
+
ref = fn(*args)
|
|
107
|
+
r = to_sympy(fn, *args)
|
|
108
|
+
got = r.value if isinstance(r, Pair) else r
|
|
109
|
+
if isinstance(got, np.ndarray) and got.dtype == object:
|
|
110
|
+
got = np.asarray(Pair._value_of(got), dtype=float)
|
|
111
|
+
try:
|
|
112
|
+
match = np.allclose(
|
|
113
|
+
np.asarray(got, dtype=float), np.asarray(ref, dtype=float),
|
|
114
|
+
rtol=1e-7, atol=1e-9, equal_nan=True,
|
|
115
|
+
)
|
|
116
|
+
except Exception:
|
|
117
|
+
match = None
|
|
118
|
+
(lift_ok if match else lift_unverified).append(name)
|
|
119
|
+
except TO:
|
|
120
|
+
died.append((name, "TIMEOUT"))
|
|
121
|
+
except NotImplementedError as e:
|
|
122
|
+
refused.append((name, str(e)[:44]))
|
|
123
|
+
except Exception as e:
|
|
124
|
+
died.append((name, f"{type(e).__name__} {str(e)[:44]}"))
|
|
125
|
+
finally:
|
|
126
|
+
signal.alarm(0)
|
|
127
|
+
|
|
128
|
+
total = len(MENU)
|
|
129
|
+
print(f"TOTAL {total} | LIFT+match {len(lift_ok)} | lift-unverified {len(lift_unverified)} | refused {len(refused)} | died {len(died)}")
|
|
130
|
+
print("\nLIFT+match:", ", ".join(lift_ok))
|
|
131
|
+
if lift_unverified:
|
|
132
|
+
print("\nlift-unverified:", ", ".join(lift_unverified))
|
|
133
|
+
print("\nREFUSED:")
|
|
134
|
+
for n_, m in refused:
|
|
135
|
+
print(f" {n_:28s} {m}")
|
|
136
|
+
print("\nDIED:")
|
|
137
|
+
for n_, m in died:
|
|
138
|
+
print(f" {n_:28s} {m}")
|