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.
Files changed (121) hide show
  1. scikit_verify-0.1.0/.github/workflows/ci.yml +24 -0
  2. scikit_verify-0.1.0/.gitignore +10 -0
  3. scikit_verify-0.1.0/FAILURES.md +44 -0
  4. scikit_verify-0.1.0/LICENSE +28 -0
  5. scikit_verify-0.1.0/LOOP_DOMAINS.md +103 -0
  6. scikit_verify-0.1.0/PKG-INFO +145 -0
  7. scikit_verify-0.1.0/README.md +108 -0
  8. scikit_verify-0.1.0/coverage/cvxpy_full.out +16 -0
  9. scikit_verify-0.1.0/coverage/cvxpy_full.py +138 -0
  10. scikit_verify-0.1.0/coverage/make_coverage.py +63 -0
  11. scikit_verify-0.1.0/coverage/numpy_dialect.out +27 -0
  12. scikit_verify-0.1.0/coverage/numpy_dialect.py +218 -0
  13. scikit_verify-0.1.0/coverage/numpy_full.out +10 -0
  14. scikit_verify-0.1.0/coverage/numpy_full.py +154 -0
  15. scikit_verify-0.1.0/coverage/scale_min.py +18 -0
  16. scikit_verify-0.1.0/coverage/scipy_full.out +8 -0
  17. scikit_verify-0.1.0/coverage/scipy_full.py +82 -0
  18. scikit_verify-0.1.0/coverage/skl_full.out +15 -0
  19. scikit_verify-0.1.0/coverage/skl_full.py +155 -0
  20. scikit_verify-0.1.0/coverage/sm_full.out +11 -0
  21. scikit_verify-0.1.0/coverage/sm_full.py +121 -0
  22. scikit_verify-0.1.0/coverage/wild_100.out +61 -0
  23. scikit_verify-0.1.0/coverage/wild_100.py +293 -0
  24. scikit_verify-0.1.0/coverage/wild_sample.out +11 -0
  25. scikit_verify-0.1.0/coverage/wild_sample.py +179 -0
  26. scikit_verify-0.1.0/doc/coverage.md +53 -0
  27. scikit_verify-0.1.0/doc/logos/scikit-verify-lockup.svg +11 -0
  28. scikit_verify-0.1.0/doc/sharp-bits.md +80 -0
  29. scikit_verify-0.1.0/examples/README.md +59 -0
  30. scikit_verify-0.1.0/examples/corpus.py +165 -0
  31. scikit_verify-0.1.0/examples/demo.ipynb +1046 -0
  32. scikit_verify-0.1.0/examples/knot_debugging.ipynb +294 -0
  33. scikit_verify-0.1.0/examples/markov_birth_death.py +44 -0
  34. scikit_verify-0.1.0/examples/markov_walk_2d.py +50 -0
  35. scikit_verify-0.1.0/examples/ridgecv_decision.ipynb +266 -0
  36. scikit_verify-0.1.0/examples/sabotage_audit.ipynb +401 -0
  37. scikit_verify-0.1.0/pyproject.toml +80 -0
  38. scikit_verify-0.1.0/scikit_verify.egg-info/PKG-INFO +145 -0
  39. scikit_verify-0.1.0/scikit_verify.egg-info/SOURCES.txt +119 -0
  40. scikit_verify-0.1.0/scikit_verify.egg-info/dependency_links.txt +1 -0
  41. scikit_verify-0.1.0/scikit_verify.egg-info/entry_points.txt +2 -0
  42. scikit_verify-0.1.0/scikit_verify.egg-info/requires.txt +14 -0
  43. scikit_verify-0.1.0/scikit_verify.egg-info/scm_file_list.json +115 -0
  44. scikit_verify-0.1.0/scikit_verify.egg-info/scm_version.json +8 -0
  45. scikit_verify-0.1.0/scikit_verify.egg-info/top_level.txt +3 -0
  46. scikit_verify-0.1.0/setup.cfg +4 -0
  47. scikit_verify-0.1.0/skverify/__init__.py +73 -0
  48. scikit_verify-0.1.0/skverify/api.py +488 -0
  49. scikit_verify-0.1.0/skverify/atoms.py +448 -0
  50. scikit_verify-0.1.0/skverify/checks.py +189 -0
  51. scikit_verify-0.1.0/skverify/coercion.py +223 -0
  52. scikit_verify-0.1.0/skverify/contracts.py +346 -0
  53. scikit_verify-0.1.0/skverify/derivation.py +393 -0
  54. scikit_verify-0.1.0/skverify/dialect.py +118 -0
  55. scikit_verify-0.1.0/skverify/helpers.py +72 -0
  56. scikit_verify-0.1.0/skverify/instrument/__init__.py +44 -0
  57. scikit_verify-0.1.0/skverify/instrument/registries.py +109 -0
  58. scikit_verify-0.1.0/skverify/instrument/rewriter.py +325 -0
  59. scikit_verify-0.1.0/skverify/instrument/runtime.py +561 -0
  60. scikit_verify-0.1.0/skverify/instrument/triage.py +437 -0
  61. scikit_verify-0.1.0/skverify/instrument/twins.py +469 -0
  62. scikit_verify-0.1.0/skverify/maps/__init__.py +0 -0
  63. scikit_verify-0.1.0/skverify/maps/numpy.py +1587 -0
  64. scikit_verify-0.1.0/skverify/maps/special.py +81 -0
  65. scikit_verify-0.1.0/skverify/pair.py +2211 -0
  66. scikit_verify-0.1.0/skverify/recurrence.py +668 -0
  67. scikit_verify-0.1.0/skverify/registry.py +2 -0
  68. scikit_verify-0.1.0/skverify/session.py +91 -0
  69. scikit_verify-0.1.0/skverify/sets.py +151 -0
  70. scikit_verify-0.1.0/skverify-hypothesis/README.md +50 -0
  71. scikit_verify-0.1.0/skverify-hypothesis/__init__.py +3 -0
  72. scikit_verify-0.1.0/skverify-hypothesis/skverify_hypothesis.py +239 -0
  73. scikit_verify-0.1.0/skverify-mcp/README.md +24 -0
  74. scikit_verify-0.1.0/skverify-mcp/__init__.py +3 -0
  75. scikit_verify-0.1.0/skverify-mcp/server.py +124 -0
  76. scikit_verify-0.1.0/smoke.py +119 -0
  77. scikit_verify-0.1.0/tests/api/test_api.py +28 -0
  78. scikit_verify-0.1.0/tests/api/test_helpers.py +125 -0
  79. scikit_verify-0.1.0/tests/api/test_recompress.py +217 -0
  80. scikit_verify-0.1.0/tests/api/test_ux.py +41 -0
  81. scikit_verify-0.1.0/tests/atoms/test_contracts_core.py +60 -0
  82. scikit_verify-0.1.0/tests/atoms/test_opaque.py +117 -0
  83. scikit_verify-0.1.0/tests/atoms/test_rng.py +89 -0
  84. scikit_verify-0.1.0/tests/atoms/test_storage.py +63 -0
  85. scikit_verify-0.1.0/tests/checks/test_checks.py +141 -0
  86. scikit_verify-0.1.0/tests/checks/test_nan_family.py +75 -0
  87. scikit_verify-0.1.0/tests/checks/test_verification.py +62 -0
  88. scikit_verify-0.1.0/tests/checks/test_wild_mechanisms.py +89 -0
  89. scikit_verify-0.1.0/tests/coercion/test_no_silent_constants.py +101 -0
  90. scikit_verify-0.1.0/tests/coercion/test_tracing_minimal.py +231 -0
  91. scikit_verify-0.1.0/tests/conftest.py +20 -0
  92. scikit_verify-0.1.0/tests/derivation/test_steps.py +256 -0
  93. scikit_verify-0.1.0/tests/instrument/test_classes.py +33 -0
  94. scikit_verify-0.1.0/tests/instrument/test_guards.py +104 -0
  95. scikit_verify-0.1.0/tests/instrument/test_mask_fusion.py +165 -0
  96. scikit_verify-0.1.0/tests/instrument/test_recurrence.py +182 -0
  97. scikit_verify-0.1.0/tests/instrument/test_session.py +47 -0
  98. scikit_verify-0.1.0/tests/integration/test_battery.py +73 -0
  99. scikit_verify-0.1.0/tests/integration/test_distributions.py +190 -0
  100. scikit_verify-0.1.0/tests/integration/test_fuzz.py +193 -0
  101. scikit_verify-0.1.0/tests/integration/test_gate.py +42 -0
  102. scikit_verify-0.1.0/tests/integration/test_gate_interp.py +35 -0
  103. scikit_verify-0.1.0/tests/integration/test_gate_lsq.py +44 -0
  104. scikit_verify-0.1.0/tests/integration/test_gate_ols.py +29 -0
  105. scikit_verify-0.1.0/tests/integration/test_kernels.py +43 -0
  106. scikit_verify-0.1.0/tests/integration/test_sklearn_svm.py +37 -0
  107. scikit_verify-0.1.0/tests/integration/test_special_map.py +64 -0
  108. scikit_verify-0.1.0/tests/pair/test_broadcast.py +99 -0
  109. scikit_verify-0.1.0/tests/pair/test_capture.py +37 -0
  110. scikit_verify-0.1.0/tests/pair/test_functions.py +66 -0
  111. scikit_verify-0.1.0/tests/pair/test_getitem_nd.py +215 -0
  112. scikit_verify-0.1.0/tests/pair/test_inplace.py +61 -0
  113. scikit_verify-0.1.0/tests/pair/test_matmul.py +181 -0
  114. scikit_verify-0.1.0/tests/pair/test_relational.py +229 -0
  115. scikit_verify-0.1.0/tests/pair/test_scalar.py +117 -0
  116. scikit_verify-0.1.0/tests/pair/test_setitem.py +224 -0
  117. scikit_verify-0.1.0/tests/pair/test_sets.py +52 -0
  118. scikit_verify-0.1.0/tests/pair/test_slicing.py +109 -0
  119. scikit_verify-0.1.0/tests/pair/test_strides.py +119 -0
  120. scikit_verify-0.1.0/tests/pair/test_sum_axis.py +119 -0
  121. 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,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .venv/
8
+ __pycache__/
9
+ *.pyc
10
+ */__pycache__/*
@@ -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
+ ![CI](https://github.com/aadya940/scikit-verify/actions/workflows/ci.yml/badge.svg)
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
+ ![CI](https://github.com/aadya940/scikit-verify/actions/workflows/ci.yml/badge.svg)
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}")