pycodemath 0.3.0__tar.gz → 0.4.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 (72) hide show
  1. pycodemath-0.4.0/PKG-INFO +618 -0
  2. pycodemath-0.4.0/README.md +590 -0
  3. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/__init__.py +10 -1
  4. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/__main__.py +10 -10
  5. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/bench/benchmark.py +61 -61
  6. pycodemath-0.4.0/pycodemath/bench/data/PRM800K_LICENSE +21 -0
  7. pycodemath-0.4.0/pycodemath/bench/data/derivations.jsonl +210 -0
  8. pycodemath-0.4.0/pycodemath/bench/data/prm800k_sample.jsonl +500 -0
  9. pycodemath-0.4.0/pycodemath/bench/real_bench.py +1126 -0
  10. pycodemath-0.4.0/pycodemath/bench/verify_bench.py +851 -0
  11. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/cli/mcp_server.py +627 -416
  12. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/cli/repl.py +284 -2
  13. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/codegen/pipeline.py +139 -139
  14. pycodemath-0.4.0/pycodemath/core/backstop.py +283 -0
  15. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/core/budget.py +168 -25
  16. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/core/errors.py +365 -337
  17. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/core/ir.py +193 -7
  18. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/core/result.py +317 -317
  19. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/frontend/parser.py +151 -10
  20. pycodemath-0.4.0/pycodemath/verify/__init__.py +50 -0
  21. pycodemath-0.4.0/pycodemath/verify/certify.py +1300 -0
  22. pycodemath-0.4.0/pycodemath/verify/equal.py +603 -0
  23. pycodemath-0.4.0/pycodemath/verify/steps.py +804 -0
  24. pycodemath-0.4.0/pycodemath/verify/verdict.py +73 -0
  25. pycodemath-0.4.0/pycodemath.egg-info/PKG-INFO +618 -0
  26. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath.egg-info/SOURCES.txt +21 -1
  27. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath.egg-info/requires.txt +1 -0
  28. {pycodemath-0.3.0 → pycodemath-0.4.0}/pyproject.toml +2 -2
  29. pycodemath-0.4.0/tests/test_backstop.py +142 -0
  30. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_budget.py +129 -1
  31. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_core.py +79 -0
  32. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_errors.py +2 -0
  33. pycodemath-0.4.0/tests/test_evalf_contract.py +164 -0
  34. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_mcp.py +621 -621
  35. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_numerics.py +858 -858
  36. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_ode.py +858 -858
  37. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_parser.py +129 -3
  38. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_quadrature.py +572 -572
  39. pycodemath-0.4.0/tests/test_real_bench.py +174 -0
  40. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_result.py +924 -919
  41. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_trailing_options.py +356 -356
  42. pycodemath-0.4.0/tests/test_verify_bench.py +164 -0
  43. pycodemath-0.4.0/tests/test_verify_certify.py +545 -0
  44. pycodemath-0.4.0/tests/test_verify_equal.py +465 -0
  45. pycodemath-0.4.0/tests/test_verify_fuzz.py +443 -0
  46. pycodemath-0.4.0/tests/test_verify_steps.py +494 -0
  47. pycodemath-0.4.0/tests/test_verify_surface.py +484 -0
  48. pycodemath-0.3.0/PKG-INFO +0 -389
  49. pycodemath-0.3.0/README.md +0 -362
  50. pycodemath-0.3.0/pycodemath.egg-info/PKG-INFO +0 -389
  51. {pycodemath-0.3.0 → pycodemath-0.4.0}/LICENSE +0 -0
  52. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/bench/__init__.py +0 -0
  53. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/cli/__init__.py +0 -0
  54. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/codegen/__init__.py +0 -0
  55. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/codegen/emit.py +0 -0
  56. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/core/__init__.py +0 -0
  57. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/engine/__init__.py +0 -0
  58. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/engine/linalg.py +0 -0
  59. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/engine/numerics.py +0 -0
  60. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/engine/ode.py +0 -0
  61. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/engine/symbolic.py +0 -0
  62. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/frontend/__init__.py +0 -0
  63. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath/py.typed +0 -0
  64. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath.egg-info/dependency_links.txt +0 -0
  65. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath.egg-info/entry_points.txt +0 -0
  66. {pycodemath-0.3.0 → pycodemath-0.4.0}/pycodemath.egg-info/top_level.txt +0 -0
  67. {pycodemath-0.3.0 → pycodemath-0.4.0}/setup.cfg +0 -0
  68. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_cli.py +0 -0
  69. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_codegen.py +0 -0
  70. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_coverage_r2.py +0 -0
  71. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_linalg.py +0 -0
  72. {pycodemath-0.3.0 → pycodemath-0.4.0}/tests/test_symbolic.py +0 -0
@@ -0,0 +1,618 @@
1
+ Metadata-Version: 2.4
2
+ Name: pycodemath
3
+ Version: 0.4.0
4
+ Summary: Token-efficient exact math for AI agents: symbolic engine, ODE suite, optimizer and an optimized-code generator (SymPy + NumPy).
5
+ Author: cybersora
6
+ License: MIT
7
+ Keywords: math,symbolic,ode,codegen,mcp,ai-agents,sympy,numpy
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: sympy>=1.12
19
+ Requires-Dist: numpy>=1.24
20
+ Requires-Dist: mpmath>=1.3
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7; extra == "dev"
23
+ Requires-Dist: mypy>=1.8; extra == "dev"
24
+ Requires-Dist: hypothesis>=6.100; extra == "dev"
25
+ Provides-Extra: mcp
26
+ Requires-Dist: mcp<2.0,>=1.0; extra == "mcp"
27
+ Dynamic: license-file
28
+
29
+ <p align="center">
30
+ <picture>
31
+ <source media="(prefers-color-scheme: dark)" srcset="assets/pycodemath-lockup.svg">
32
+ <img alt="pycodemath" src="assets/pycodemath-lockup-light.svg" width="420">
33
+ </picture>
34
+ </p>
35
+
36
+ <p align="center">
37
+ <strong>Token-efficient exact math for AI agents.</strong>
38
+ </p>
39
+
40
+ <p align="center">
41
+ <a href="https://pypi.org/project/pycodemath/"><img alt="PyPI" src="https://img.shields.io/pypi/v/pycodemath.svg?color=e11d33"></a>
42
+ <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-e11d33.svg"></a>
43
+ <img alt="Python 3.11+" src="https://img.shields.io/badge/python-3.11%2B-3776ab.svg">
44
+ <img alt="1152 tests passing" src="https://img.shields.io/badge/tests-1152%20passing-2ea043.svg">
45
+ <img alt="mypy: clean" src="https://img.shields.io/badge/mypy-clean-2ea043.svg">
46
+ <a href="https://github.com/cybersora9/pycodemath/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/cybersora9/pycodemath/actions/workflows/ci.yml/badge.svg"></a>
47
+ </p>
48
+
49
+ <p align="center">
50
+ <img alt="pycodemath demo — one command in, one exact result out" src="assets/demo.svg" width="760">
51
+ </p>
52
+
53
+ Write math in a few characters, get an exact answer or standalone, optimized
54
+ Python/NumPy code back — instead of asking a language model to "do arithmetic
55
+ in its head" or to hand-write numerical loops.
56
+
57
+ *New to terms like "MCP", "symbolic", or "ODE"? Jump to the [Glossary](#glossary).*
58
+
59
+ Built as a thin, disciplined layer over SymPy + NumPy:
60
+
61
+ 1. **Cut tokens** — one short command in, one exact result out. Perfect as an
62
+ agent tool (MCP server included).
63
+ 2. **Better code than naive Python** — the generator applies symbolic
64
+ simplification + common-subexpression elimination (CSE) before emitting
65
+ code (measured ~1.7x faster than naive expansion on repeated
66
+ subexpressions).
67
+ 3. **Check, not just compute** — `verify` answers VERIFIED, REFUTED (with a
68
+ counterexample you can re-check) or UNDECIDED for an identity, a
69
+ step-by-step derivation, or the engine's own answer. See
70
+ [Verify](#verify-check-instead-of-compute).
71
+
72
+ ```
73
+ text → [parser] → IR (expression tree) → [engine] evaluate / simplify / solve
74
+ → [generator] IR → CSE → Python/NumPy source
75
+ ```
76
+
77
+ ## Install
78
+
79
+ ```bash
80
+ pip install pycodemath # core: sympy + numpy
81
+ pip install pycodemath[mcp] # + MCP server for AI agents
82
+ ```
83
+
84
+ From a clone, for development (editable install):
85
+
86
+ ```bash
87
+ pip install -e .[dev] # editable + pytest + mypy
88
+ ```
89
+
90
+ Requires Python ≥ 3.11.
91
+
92
+ ## Quick start
93
+
94
+ ### One-shot CLI (scriptable)
95
+
96
+ Every command below is a real invocation with its real output.
97
+
98
+ ```console
99
+ $ python -m pycodemath "diff sin(x)*x dx"
100
+ x*cos(x) + sin(x)
101
+
102
+ $ python -m pycodemath "integrate 2*x dx"
103
+ x**2
104
+
105
+ $ python -m pycodemath "solve x^2 - 4 for x"
106
+ -2, 2
107
+
108
+ $ python -m pycodemath "sin(x)^2 + cos(x)^2"
109
+ 1
110
+
111
+ $ python -m pycodemath "eig [[2,1],[1,2]]"
112
+ 3, 1
113
+
114
+ $ python -m pycodemath "solve_nd x^2+y^2-4; x-y for x,y at 1,1"
115
+ x = 1.4142135623746899, y = 1.4142135623746899
116
+ ```
117
+
118
+ Errors go to stderr with exit code 1, results to stdout with exit code 0 —
119
+ safe to call from scripts and agent tools. On Windows consoles set
120
+ `PYTHONUTF8=1`.
121
+
122
+ ### REPL
123
+
124
+ ```bash
125
+ python -m pycodemath # or just: pycodemath
126
+ ```
127
+
128
+ Type `help` for the full command table (derivatives, integrals, equation
129
+ solving, matrices, root finding, optimization, the whole ODE suite, and code
130
+ generation).
131
+
132
+ ### MCP server (math for any AI agent)
133
+
134
+ ```bash
135
+ pip install -e .[mcp]
136
+ claude mcp add pycodemath -- python -m pycodemath.cli.mcp_server
137
+ ```
138
+
139
+ Exposes `math_eval`, that accepts the same commands as the REPL —
140
+ an agent sends `diff sin(x)*x dx` and receives `x*cos(x) + sin(x)` exactly,
141
+ with zero mental arithmetic. Its twin, `math_verify`, CHECKS instead of
142
+ computing — an identity, a derivation or an engine answer — see
143
+ [Verify](#verify-check-instead-of-compute).
144
+
145
+ ### Use it as a Claude Code skill
146
+
147
+ The MCP server above is the portable path — it plugs into **any** MCP client.
148
+ If you specifically use [Claude Code](https://claude.com/claude-code), you can
149
+ also wire Pycodemath in as a **skill**, so Claude reaches for the engine on its
150
+ own whenever a prompt needs real math (no MCP process to keep running).
151
+
152
+ **What it changes:** instead of computing "in its head" — where a large model
153
+ can quietly get arithmetic, an integral, or an eigenvalue wrong — Claude shells
154
+ out to the engine and pastes back an exact SymPy/NumPy result. One short command
155
+ in, one exact line out: fewer tokens, no silent mistakes.
156
+
157
+ **Deploy (once):**
158
+
159
+ ```bash
160
+ pip install pycodemath # or: pip install -e . (from a clone)
161
+ mkdir -p ~/.claude/skills/pycodemath
162
+ ```
163
+
164
+ Save the following as `~/.claude/skills/pycodemath/SKILL.md`:
165
+
166
+ ```markdown
167
+ ---
168
+ name: pycodemath
169
+ description: Compute math with the local Pycodemath engine (SymPy+NumPy) instead
170
+ of in your head — derivatives, integrals, solving equations and systems
171
+ (linear and nonlinear), determinants/inverses/eigenvalues, gradients/Jacobians/
172
+ Hessians, function minima, ODEs, and optimized Python/NumPy code generation
173
+ (CSE). Use whenever the user asks to compute or verify symbolic/numerical math,
174
+ or to generate code from a formula.
175
+ ---
176
+
177
+ # Pycodemath — local math engine
178
+
179
+ One command = one call (the package is pip-installed, so any working directory):
180
+
181
+ python -m pycodemath "<command>"
182
+
183
+ Result goes to stdout (exit 0); errors to stderr (exit 1). Power notation: `^` or `**`.
184
+ On Windows consoles, set `PYTHONUTF8=1`.
185
+
186
+ ## Commands
187
+
188
+ | Command | Example |
189
+ |---|---|
190
+ | `<expression>` — simplify | `python -m pycodemath "sin(x)^2 + cos(x)^2"` → `1` |
191
+ | `diff <expr> d<var>` — derivative | `"diff sin(x)*x dx"` → `x*cos(x) + sin(x)` |
192
+ | `integrate <expr> d<var>` — symbolic integral | `"integrate 2*x dx"` → `x**2` |
193
+ | `solve <expr> for <var>` — solve = 0 | `"solve x^2-4 for x"` → `-2, 2` |
194
+ | `code <expr>` — CSE-optimized NumPy code | `"code (sin(x)+cos(x))^2"` |
195
+ | `det / inv / transpose / eig <A>` | `"eig [[2,1],[1,2]]"` → `3, 1` |
196
+ | `solve_system <A> = <b>` — linear system | `"solve_system [[2,1],[1,3]] = [3,5]"` |
197
+ | `root <expr> for <var> at <x0>` — numeric root | `"root x^2-2 for x at 1"` |
198
+ | `min <expr> for <var> at <x0> [method newton|bfgs]` — 1D minimum | `"min (x-3)^2 for x at 0"` |
199
+ | `grad <expr> for <x,y,...>` — symbolic gradient | `"grad x^2*y for x,y"` |
200
+ | `solve_nd <f1>; <f2> for <x,y> at <x0,y0>` — nonlinear system | `"solve_nd x^2+y^2-4; x-y for x,y at 1,1"` |
201
+ | `min_nd <expr> for <x,y> at <x0,y0> [method newton|bfgs]` — N-D minimum | `"min_nd (1-x)^2+100*(y-x^2)^2 for x,y at -1.2,1 method bfgs"` |
202
+ | limits / series / sums / ODEs | `"limit sin(x)/x for x to 0"`, `"sum 1/k^2 for k from 1 to oo"` |
203
+ | `verify <a> == <b>` — is it an identity? | `"verify sqrt(x^2) == x"` → `REFUTED … at x = -1` |
204
+ | `verify steps <s1> = <s2> = …` — check a derivation | first wrong step + counterexample |
205
+ | `certify <command>` — compute, then check the answer | `"certify integrate x*cos(x) dx"` |
206
+
207
+ Type `help` in the REPL (`python -m pycodemath`) for the full command table.
208
+
209
+ ## Rules
210
+
211
+ - **When to use:** the user wants a concrete math result (derivative, integral,
212
+ equation, matrix, minimum, ODE) or optimized code from a formula. The engine is
213
+ exact — trust its output over mental arithmetic.
214
+ - **When not to use:** trivial arithmetic or conceptual questions with no compute.
215
+ - A `error: ...` line on stderr (exit 1) usually means a typo in the command, a
216
+ numerical method that did not converge, or no real solution — read the message,
217
+ it is specific.
218
+ ```
219
+
220
+ Restart Claude Code (or open a new session) and it will invoke the skill
221
+ automatically when a task needs exact math. To confirm it registered, run `/help`
222
+ and look for `pycodemath` in the skills list.
223
+
224
+ ## Verify: check instead of compute
225
+
226
+ A calculator answers "what is it?". A derivation from a language model needs a
227
+ different question answered: **is it right?** Pycodemath answers it with three
228
+ words, and never a fourth:
229
+
230
+ * **VERIFIED** — PROVED: a symbolic argument closed.
231
+ * **REFUTED** — false, with a **counterexample**: a concrete point, confirmed at
232
+ two precisions so it is not rounding noise.
233
+ * **UNDECIDED** — neither. Agreement at every sampled point is UNDECIDED, never
234
+ rounded up to VERIFIED. It is a first-class answer, not a failure: a verifier
235
+ that guesses is worse than no verifier, because it is trusted.
236
+
237
+ An identity — `verify <a> == <b>`:
238
+
239
+ ```console
240
+ $ python -m pycodemath "verify sqrt(x^2) == x"
241
+ REFUTED (numeric-sampling) — at x = -1: left = 1.0, right = -1.0
242
+
243
+ $ python -m pycodemath "verify (x^2-1)/(x-1) == x+1"
244
+ VERIFIED (symbolic) — left - right simplifies to 0; agree numerically at 31 of 32 sample points (1 not evaluable)
245
+
246
+ $ python -m pycodemath "verify cos(x)^6 + sin(x)^6 == 1 - 3*sin(x)^2*cos(x)^2"
247
+ UNDECIDED (numeric-sampling) — agree numerically at 32 of 32 sample points, without a symbolic proof
248
+ ```
249
+
250
+ The last one is a true identity; SymPy's `simplify` just does not close it, so
251
+ the honest answer is UNDECIDED. What "equal" means is fixed and worth knowing:
252
+ values on the principal branch (`log(x^2) == 2*log(x)` is refuted at `x = -1`),
253
+ equality where BOTH sides are defined (hence the second example), and decimals
254
+ meaning the decimal written (`0.1 + 0.2 == 0.3` holds).
255
+
256
+ A derivation — `verify steps`. Neighbouring steps are checked pairwise, so a slip
257
+ is reported once, at the step that made it, with a counterexample:
258
+
259
+ ```console
260
+ $ python -m pycodemath "verify steps (x+1)^2 - (x-1)^2 = x^2 + 2x + 1 - x^2 + 2x + 1 = 4x + 2"
261
+ step 2: REFUTED (numeric-sampling) — at x = 0: left = 0.0, right = 2.0
262
+ step 3: VERIFIED (symbolic) — the two sides are identical after automatic simplification
263
+ derivation: REFUTED — first wrong step: 2 (counterexample x = 0)
264
+ ```
265
+
266
+ Equation steps (one per line, or joined by `->`) must keep the same solution set
267
+ over the reals — squaring both sides is caught even though every old root
268
+ survives it:
269
+
270
+ ```console
271
+ $ python -m pycodemath "verify steps sqrt(x) = x - 2 -> x = (x-2)^2 -> x^2 - 5x + 4 = 0 -> x = 1 or x = 4"
272
+ step 2: REFUTED (solution-sets) — x = 1 solves step 2 but not step 1
273
+ warning: step 2 gains the root x = 1 — squaring both sides adds roots: check each one against step 1
274
+ step 3: VERIFIED (symbolic) — left - right of step 2 is -1 times that of step 3, with the same poles, so the solution sets coincide
275
+ step 4: VERIFIED (symbolic) — left - right of step 3 equals that of step 4, with the same poles, so the solution sets coincide
276
+ derivation: REFUTED — first wrong step: 2 (counterexample x = 1)
277
+ ```
278
+
279
+ In the interactive REPL, typing just `verify steps` reads one step per line
280
+ until an empty line.
281
+
282
+ An engine answer — `certify <command>` runs `integrate`, `diff`, `solve`,
283
+ `limit`, `dsolve` or `nintegrate`, then checks the answer by a route that did
284
+ not produce it (an antiderivative by finite differences before any symbolic
285
+ calculus — re-deriving with the engine that made the answer would agree with
286
+ its own mistakes):
287
+
288
+ ```console
289
+ $ python -m pycodemath "certify integrate x*cos(x) dx"
290
+ x*sin(x) + cos(x)
291
+ certificate: VERIFIED (symbolic) — d/dx of the result: the symbolic derivative matches (the two sides are identical after automatic simplification); finite differences agree at 16 of 16 points
292
+
293
+ $ python -m pycodemath "certify dsolve y for y(t)"
294
+ y(t) = C1*exp(t)
295
+ certificate: VERIFIED (symbolic) — each of the 1 solution(s) satisfies y' = y (symbolic proof; finite differences agree) — that they are ALL the solutions is not certified
296
+ ```
297
+
298
+ A certificate claims what it says and nothing more: `dsolve` — every returned
299
+ function solves the equation, not that they are all the solutions; `solve` —
300
+ completeness is proved only for rational functions (elsewhere a missing real
301
+ root can be found, never ruled out); `nintegrate` — the value is within the
302
+ run's own `error_estimate`.
303
+
304
+ Every check is bounded in time (120 s by default; a trailing `budget <s>`
305
+ replaces it). A CHECK that runs out is an UNDECIDED verdict with method
306
+ `time-budget`, not an error. On `certify`, the command's own `budget` bounds the
307
+ computation (which, running out, is the usual `TimeBudgetError` refusal) and,
308
+ separately, the certificate.
309
+
310
+ For an agent, the MCP tool `math_verify` takes the same lines (the leading word
311
+ `verify` is optional) and returns the verdict as `structuredContent`, with the
312
+ same rules as `math_eval`: five fields, always present, `null` where they do not
313
+ apply — `text` (what the REPL prints), `answer` (`certify`: the answer that was
314
+ certified), `verdict` (`status`, `method`, `counterexample`, `detail`), `steps`
315
+ (`status`, `first_error`, `counterexample`, and one entry per step with its own
316
+ verdict, `effect` and `warning`), and `error` (a refusal, exactly as
317
+ `math_eval` returns it — route included). Like `math_eval`, it is a plain
318
+ function returning a plain `dict`:
319
+
320
+ ```pycon
321
+ >>> from pycodemath.cli.mcp_server import math_verify
322
+ >>> math_verify("sqrt(x^2) == x")["verdict"]
323
+ {'status': 'refuted', 'method': 'numeric-sampling', 'counterexample': {'x': '-1'}, 'detail': 'at x = -1: left = 1.0, right = -1.0'}
324
+ >>> derivation = math_verify("steps sqrt(x) = x - 2 -> x = (x-2)^2")["steps"]
325
+ >>> (derivation["first_error"], derivation["checks"][0]["effect"])
326
+ (2, 'gains-roots')
327
+ >>> math_verify("certify integrate exp(sin(x)) dx")["error"]["route"]
328
+ 'nintegrate'
329
+ ```
330
+
331
+ The same checks from Python, without the command grammar:
332
+
333
+ ```pycon
334
+ >>> from pycodemath.verify import check_equal, check_steps, certify
335
+ >>> check_equal("log(x^2)", "2*log(x)").counterexample
336
+ {'x': '-1'}
337
+ >>> check_steps("2x + 3 = 7 -> 2x = 4 -> x = 2").status
338
+ <VerdictStatus.VERIFIED: 'verified'>
339
+ >>> certify("integrate", "2*x", "x", "x^2 + 5").status
340
+ <VerdictStatus.VERIFIED: 'verified'>
341
+ ```
342
+
343
+ ### How often it catches a real mistake — with the denominators
344
+
345
+ Two benchmarks ship with the package and run from an installed copy:
346
+
347
+ ```bash
348
+ python -m pycodemath.bench.verify_bench # synthetic derivations
349
+ python -m pycodemath.bench.real_bench # real model solutions (PRM800K sample)
350
+ ```
351
+
352
+ * **Synthetic:** 210 generated derivations, half with an injected typical
353
+ mistake (a lost sign, a wrong chain rule, squaring that adds a root, …):
354
+ 105 of 105 mistakes caught, 0 false positives. The data are generated, and
355
+ the verifier was fixed against misses found on this very set — read it as a
356
+ regression test, not as a claim about language models.
357
+ * **Real model errors** (a fixed, MIT-licensed sample of OpenAI's PRM800K,
358
+ human-labelled first wrong step): a checkable claim can be extracted from
359
+ 9.5% of steps — most steps are prose. The verifier pinpoints the labelled
360
+ first wrong step in **24 of 378** flawed solutions (6.3%) and raises
361
+ **0 false alarms on 122** correct ones.
362
+
363
+ Low recall, zero false alarms: when it says a step is wrong, it shows you the
364
+ point where it is wrong. It is a veto, not a grader.
365
+
366
+ ## Limits, series and symbolic sums
367
+
368
+ Beyond `diff`/`integrate`/`solve`, the engine handles limits (including
369
+ one-sided and at infinity), Taylor/Laurent expansions and symbolic summation
370
+ — finite or infinite. All real invocations with real outputs:
371
+
372
+ ```console
373
+ $ python -m pycodemath "limit (1+1/n)^n for n to oo"
374
+ E
375
+
376
+ $ python -m pycodemath "series exp(x) for x n 4"
377
+ x**3/6 + x**2/2 + x + 1
378
+
379
+ $ python -m pycodemath "sum k for k from 1 to n"
380
+ n**2/2 + n/2
381
+
382
+ $ python -m pycodemath "sum 1/k^2 for k from 1 to oo"
383
+ pi**2/6
384
+ ```
385
+
386
+ A divergent sum or a nonexistent limit refuses with a readable error instead
387
+ of returning a symbolic echo.
388
+
389
+ ## Optimization: Newton and BFGS where gradient descent stalls
390
+
391
+ `min` / `min_nd` default to plain gradient descent; `method newton|bfgs`
392
+ switches to second-order methods with Armijo backtracking line search.
393
+ On the Rosenbrock valley (start `(-1.2, 1)`) gradient descent refuses after
394
+ 10 000 iterations, while BFGS converges in 36:
395
+
396
+ ```console
397
+ $ python -m pycodemath "min_nd (1-x)^2 + 100*(y-x^2)^2 for x,y at -1.2,1 method bfgs"
398
+ x = 0.999999999999454, y = 0.9999999999989762
399
+ ```
400
+
401
+ ## ODE suite
402
+
403
+ Symbolic (`dsolve`) plus a full numerical toolbox — scalar and systems,
404
+ forward and backward in time (`t1 < t0`), all refusing to silently jump over
405
+ singularities (a readable error instead of garbage):
406
+
407
+ | solver | what it does |
408
+ | --- | --- |
409
+ | `solve_ode_num` | classic fixed-step RK4 |
410
+ | `solve_ode_adaptive` | Dormand–Prince 5(4), adaptive step (like `ode45`) |
411
+ | `solve_ode_dense` | adaptive + dense output: callable `sol(t)` anywhere |
412
+ | `solve_ode_events` | event detection `g(t,y)=0` (+ terminal events) |
413
+ | `solve_ode_stiff` | implicit BDF2 + Newton for stiff equations |
414
+ | `solve_ode_stiff_adaptive` | **variable-step BDF2** with local error control |
415
+ | `solve_ode_system_*` | vector variants of all of the above |
416
+
417
+ Adaptive integration of a smooth problem takes 41 steps at `rtol 1e-8`:
418
+
419
+ ```console
420
+ $ python -m pycodemath "ode_adaptive y*cos(t) for y(t) from 0 to 5 at 1 rtol 1e-8"
421
+ y(5) = 0.3833049965035854 (41 adaptive steps)
422
+ ```
423
+
424
+ On the stiff classic `y' = -1000(y - cos t)` the explicit pair is limited by
425
+ *stability* (378 steps at `rtol 1e-6`; with the same budget fixed-step RK4
426
+ returns astronomically wrong finite values). The adaptive BDF gets there in
427
+ 313 steps — and starting on the slow manifold, where stiffness is pure, in
428
+ **58 steps vs 357**:
429
+
430
+ ```console
431
+ $ python -m pycodemath "odestiff_adaptive -1000*(y-cos(t)) for y(t) from 0 to 1 at 0"
432
+ y(1) = 0.5411432587540607 (adaptive BDF, 313 implicit steps)
433
+ ```
434
+
435
+ The system variant handles the Van der Pol oscillator with μ=1000 over
436
+ `[0, 2000]` in 5 332 steps (~1 s) — an explicit method would need ≥ 2 000 000.
437
+
438
+ ## Code generation
439
+
440
+ `code <expr>` emits standalone, CSE-optimized NumPy source (real output):
441
+
442
+ ```console
443
+ $ python -m pycodemath "code (sin(x)+cos(x))^2 + (sin(x)+cos(x))^3"
444
+ import numpy as np
445
+
446
+
447
+ def f(x):
448
+ """Pycodemath: f(x) — NumPy code."""
449
+ _c0 = np.sin(x + (1/4)*np.pi)
450
+ return 2*_c0**2*(np.sqrt(2)*_c0 + 1)
451
+ ```
452
+
453
+ The Python API also generates standalone ODE integrators — including an
454
+ adaptive one and one with **dense output** whose emitted interpolant is
455
+ bit-for-bit identical to the engine (measured max difference: `0.0` on nodes
456
+ and off-node grids, forward and backward):
457
+
458
+ ```python
459
+ from pycodemath import parse, generate_ode_dense
460
+
461
+ art = generate_ode_dense(parse("y*cos(t)"), "t", "y")
462
+ sol = art(1.0, 0.0, 5.0, 1e-8) # standalone DOPRI5, returns an interpolant
463
+ sol(2.5) # -> 1.8193369962907706
464
+ len(sol.ts) # -> 42 accepted nodes
465
+ ```
466
+
467
+ Event detection from the API:
468
+
469
+ ```python
470
+ from pycodemath import parse, solve_ode_events
471
+
472
+ ev = solve_ode_events(parse("cos(t)"), "t", 0.0, (0.0, 10.0), parse("y"), rtol=1e-8)
473
+ ev.event_times # -> [3.141593, 6.283185, 9.424778] (π, 2π, 3π)
474
+ ```
475
+
476
+ ## Structured results: the evidence behind the answer
477
+
478
+ Every solver in this package now has a `full_result=True` form that returns
479
+ evidence instead of a bare number — a frozen `SolveResult` (value,
480
+ iterations, residual, converged, status) for `root_find` / `root_find_nd` /
481
+ `minimize` / `minimize_nd`, and a `QuadratureResult` (adds `error_estimate`)
482
+ for `integrate_num`. Default calls are unchanged, bit-identical to before.
483
+
484
+ ```pycon
485
+ >>> from pycodemath import root_find, minimize, integrate_num, parse
486
+ >>> root_find(parse("x^2 - 2"), "x", 1.0, full_result=True)
487
+ SolveResult(value=1.4142135623746899, iterations=5, residual=4.510614104447086e-12, converged=True, status='converged')
488
+
489
+ >>> minimize(parse("-x^2"), "x", 1.0, full_result=True)
490
+ SolveResult(value=1085298990978.309, iterations=152, residual=2170597981956.618, converged=False, status='diverged')
491
+
492
+ >>> integrate_num(parse("sqrt(x)"), "x", 0.0, 1.0, tol=1e-10, full_result=True)
493
+ QuadratureResult(value=0.6666666666666469, error_estimate=2.4271240969151142e-11, evaluations=1005, refinements=201, converged=True, status='converged')
494
+ ```
495
+
496
+ `converged` is the only field worth branching on, and it is corroborated, not
497
+ just "the tolerance test fired": a vanishing gradient at a saddle or a
498
+ maximum reports `status='not_a_minimum'` rather than a false convergence.
499
+
500
+ **`error_estimate` is an estimate, never a bound.** Where the smoothness
501
+ assumption fails, the estimate is not merely loose — it is optimistic. Here
502
+ every grid node and every node of its halving lands on a maximum of the
503
+ cosine, both grids agree exactly, the estimate is `0.0`, and the true integral
504
+ is `0.0` against a returned `1.0`:
505
+
506
+ ```pycon
507
+ >>> integrate_num(parse("cos(16*pi*x)"), "x", 0.0, 1.0, 4, full_result=True)
508
+ QuadratureResult(value=1.0, error_estimate=0.0, evaluations=9, refinements=0, converged=True, status='converged')
509
+ ```
510
+
511
+ The MCP server carries the same structure over the wire as `structuredContent`
512
+ (`text` / `solve` / `quadrature` / `error` fields), not just prose — a client
513
+ validates it against the declared schema instead of parsing text.
514
+
515
+ **Trailing options** put those same knobs on the REPL/one-shot grammar:
516
+
517
+ ```console
518
+ $ python -m pycodemath "min x^4 for x at 1 tol 1e-5"
519
+ 0.029229144526165384
520
+
521
+ $ python -m pycodemath "nintegrate sqrt(x) dx from 0 to 1 tol 1e-10"
522
+ 0.6666666666666469
523
+ ```
524
+
525
+ `tol <t>` (on `min` / `min_nd` / `nintegrate`), `max_iter <k>` (on `min` /
526
+ `min_nd`) and `budget <s>` (on the symbolic commands, bounding a call the
527
+ same way `pycodemath.time_budget(seconds)` does in Python) are `key value`
528
+ pairs at the end of a command, in any order.
529
+
530
+ A symbolic call that refuses says which of two things happened:
531
+ `NoClosedFormError` (the engine searched and found nothing — try numerically)
532
+ or `UnsupportedFormError` (no method exists for this shape at all).
533
+
534
+ ## Design contracts
535
+
536
+ - One IR (`Expr`/`Matrix`) shared by the engine and the generator.
537
+ - Numerical loops run on compiled functions (`lambdify` + LRU cache) —
538
+ zero SymPy calls per iteration.
539
+ - Divergence or a domain problem raises a readable `PycodemathError` —
540
+ never NaN/garbage in a result.
541
+ - The parser resolves only a whitelist of mathematical functions — unknown
542
+ names become symbols, not Python code (an unknown name *called* like a
543
+ function, `zeta(2)`, is a `ParseError`, not `2*zeta`); string literals and attribute
544
+ access are rejected outright, and evaluation cost is bounded so a single
545
+ expression (e.g. `9**9**9`) can't exhaust memory.
546
+ - Tests measure real numbers first, then assert them with a margin —
547
+ **1152 tests**, all green, on Ubuntu and Windows, Python 3.11–3.14 (CI + mypy included).
548
+ - Every failure is a specific `PycodemathError` subclass (`ParseError`,
549
+ `DomainError`, `DivergenceError`, `StagnationError`, `NonConvergenceError`,
550
+ `NoClosedFormError`, `UnsupportedFormError`, `TimeBudgetError`,
551
+ `IsolationError`) — a caller
552
+ can catch the mathematical outcome, not string-match a message.
553
+ - **`time_budget` is best-effort; `isolated` is the hard version.** The
554
+ in-process guard interrupts a hang by injecting an exception between two
555
+ bytecodes, so one long C-level call (a huge integer power, a compiled
556
+ extension) does not notice it until that call returns; delivery can also be
557
+ missed under heavy or virtualized scheduling (measured: Python 3.13, WSL2).
558
+ It never produces a wrong answer — the failure mode is a late refusal.
559
+ On **Python 3.13** only (CPython bug gh-139622: an asynchronous exception
560
+ skips the handlers of the frame it is raised in), the guard holds back while
561
+ the interrupted frame sits inside a `try` / `with` / `finally`: engine calls
562
+ are interrupted as before, but a loop written directly inside your own
563
+ `with time_budget(...)` block is refused when it ends rather than cut short.
564
+
565
+ ### The hard version: `isolated`
566
+
567
+ For a call that must end on time no matter what it hits, run it in the worker
568
+ process:
569
+
570
+ ```pycon
571
+ >>> from pycodemath import isolated, symbolic, E
572
+ >>> isolated(symbolic.integrate, E("x*sin(x)"), "x", budget=5.0)
573
+ Expr(-x*cos(x) + sin(x))
574
+ >>> isolated(pow, 7, 10**7, budget=1.0)
575
+ TimeBudgetError: pow: gave up on 7 after the 1s time budget — the call was stuck where the in-process guard cannot reach (one long C-level call), so its worker process was stopped and there is no partial result. This is NOT 'no closed form': an answer may well exist. Raise the budget or compute numerically.
576
+ ```
577
+
578
+ The call runs under the same guard inside one warm worker; if no answer comes
579
+ back by budget + 0.5 s, the worker is killed and `TimeBudgetError` is raised.
580
+ Measured on Windows / Python 3.12: `7 ** (10**7)` under a 2 s budget took
581
+ 7.9–8.7 s in-process and was refused after 2.52–2.53 s through `isolated`
582
+ (5 of 5 runs). It is opt-in because it costs about +0.4 ms per call (pickling and a
583
+ pipe) and 1.2 s to start the worker once — and again only after a kill. A worker
584
+ that dies without answering is `IsolationError`, not a budget.
585
+
586
+ ## Glossary
587
+
588
+ Plain-language definitions for the jargon used above — for anyone reading
589
+ this repo who isn't a programmer.
590
+
591
+ | Term | What it means |
592
+ | --- | --- |
593
+ | **Symbolic math** | Math done with exact letters and formulas (like `x^2 + 1`), the way it's done on paper — as opposed to plugging in decimal numbers. The answer is exact, not an approximation. |
594
+ | **Numerical math** | Math done with actual decimal numbers (like `1.41421356...`), computed by an algorithm that gets closer and closer to the right answer. Fast, but approximate. |
595
+ | **SymPy / NumPy** | The two open-source Python libraries this project is built on. SymPy does the exact/symbolic math; NumPy does the fast numerical math. |
596
+ | **Parser** | The part of the program that reads what you type (e.g. `diff sin(x)*x dx`) and figures out what math it describes. |
597
+ | **Engine** | The part that actually does the math once the parser has understood the question — computes the derivative, solves the equation, etc. |
598
+ | **Code generator ("codegen")** | The part that, instead of just giving you an answer, writes a ready-to-use block of Python code that computes your formula. |
599
+ | **CSE (common-subexpression elimination)** | An optimization: if a formula repeats the same calculation twice, the generated code computes it once and reuses the result, instead of redoing the work. |
600
+ | **REPL** | "Read-Eval-Print Loop" — an interactive prompt: you type one command, get one answer, type the next command, and so on (like a calculator you talk to in a terminal). |
601
+ | **MCP (Model Context Protocol)** | An open standard that lets an AI assistant (like Claude) call outside tools — in this case, so the AI can hand off a real math problem to this engine instead of guessing the answer itself. |
602
+ | **AI agent** | An AI assistant that can take actions and use tools on its own (not just chat) — e.g. Claude Code, or any MCP-compatible assistant. |
603
+ | **Token** | The small chunks of text an AI language model reads and writes in. Fewer tokens = a cheaper, faster exchange with the AI — one of the reasons this tool returns short, exact answers instead of a wall of text. |
604
+ | **Typed exception** | An error that comes labeled with a specific, named type (e.g. "the equation has no real solution" vs. "you typed something invalid") instead of just a generic error message — so a program can react correctly to *why* something failed. |
605
+ | **`SolveResult` / `QuadratureResult`** | Structured "evidence" objects this engine can return alongside an answer — not just the number, but how many steps it took, how far off it might be, and whether it's confident the answer is actually correct. |
606
+ | **Root / root finding** | Finding the value(s) where a formula equals zero (e.g. where a graph crosses the x-axis). |
607
+ | **Eigenvalue** | A special number tied to a matrix (a grid of numbers) that shows up constantly in physics, engineering and graphics — e.g. describing natural vibration frequencies or stable directions of a system. |
608
+ | **Gradient / Jacobian / Hessian** | Different flavors of "derivative" for formulas with more than one variable — a gradient points in the direction a function increases fastest; a Jacobian and Hessian are the multi-variable versions of the first and second derivative. |
609
+ | **ODE (Ordinary Differential Equation)** | An equation describing how something changes over time (e.g. how a falling object's speed changes due to gravity and drag). Central to physics, biology, and engineering simulations. |
610
+ | **RK4 / Dormand–Prince / BDF** | Named algorithms for solving ODEs numerically, step by step through time. They differ in speed, accuracy, and whether they automatically adjust their own step size. |
611
+ | **"Stiff" equation** | An ODE that forces ordinary solving methods to take absurdly tiny time-steps to stay accurate — it needs a specialized (BDF) method to solve in reasonable time. |
612
+ | **`py.typed` / mypy** | A marker telling other tools "this package declares what type of data (number, text, etc.) each function expects and returns"; `mypy` is the tool that checks those declarations are actually consistent, catching a class of bugs before the code ever runs. |
613
+ | **CI (Continuous Integration)** | An automated process that runs the full test suite every time the code changes, so a mistake gets caught immediately rather than after it ships. |
614
+ | **Wheel / sdist** | The two standard packaged formats a Python library is distributed in (what `pip install` actually downloads). |
615
+
616
+ ## License
617
+
618
+ MIT — see [LICENSE](LICENSE).