pycodemath 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pycodemath/__init__.py +91 -0
- pycodemath/__main__.py +10 -0
- pycodemath/bench/__init__.py +0 -0
- pycodemath/bench/benchmark.py +61 -0
- pycodemath/cli/__init__.py +0 -0
- pycodemath/cli/mcp_server.py +82 -0
- pycodemath/cli/repl.py +620 -0
- pycodemath/codegen/__init__.py +0 -0
- pycodemath/codegen/emit.py +554 -0
- pycodemath/codegen/pipeline.py +139 -0
- pycodemath/core/__init__.py +0 -0
- pycodemath/core/errors.py +15 -0
- pycodemath/core/ir.py +307 -0
- pycodemath/engine/__init__.py +0 -0
- pycodemath/engine/linalg.py +197 -0
- pycodemath/engine/numerics.py +973 -0
- pycodemath/engine/ode.py +1279 -0
- pycodemath/engine/symbolic.py +204 -0
- pycodemath/frontend/__init__.py +0 -0
- pycodemath/frontend/parser.py +299 -0
- pycodemath-0.2.0.dist-info/METADATA +327 -0
- pycodemath-0.2.0.dist-info/RECORD +26 -0
- pycodemath-0.2.0.dist-info/WHEEL +5 -0
- pycodemath-0.2.0.dist-info/entry_points.txt +3 -0
- pycodemath-0.2.0.dist-info/licenses/LICENSE +21 -0
- pycodemath-0.2.0.dist-info/top_level.txt +1 -0
pycodemath/cli/repl.py
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
"""Pycodemath REPL — interactive entry door to the language.
|
|
2
|
+
|
|
3
|
+
You type math, you get a result. Prefixed commands let you reach
|
|
4
|
+
specific engine operations and the code generator.
|
|
5
|
+
|
|
6
|
+
Commands:
|
|
7
|
+
<expression> — parse and show the simplified form
|
|
8
|
+
diff <expr> d<var> — derivative with respect to a variable (e.g. diff sin(x)*x dx)
|
|
9
|
+
integrate <expr> d<var> — symbolic integral (e.g. integrate 2*x dx)
|
|
10
|
+
solve <expr> for <var> [complex] — solve expr = 0 for var
|
|
11
|
+
(real only by default; 'complex' =
|
|
12
|
+
complex too; e.g. solve x^2-4 for x)
|
|
13
|
+
limit <expr> for <var> to <p> [dir +|-] — limit as var → p (p may be oo/-oo)
|
|
14
|
+
(e.g. limit sin(x)/x for x to 0)
|
|
15
|
+
series <expr> for <var> [at <p>] [n <k>] — Taylor/Laurent series around p
|
|
16
|
+
(without the O term; e.g. series exp(x) for x n 4)
|
|
17
|
+
sum <expr> for <var> from <lo> to <hi> — symbolic summation (hi may be n or oo)
|
|
18
|
+
(e.g. sum k for k from 1 to n)
|
|
19
|
+
code <expression> — generate optimized Python/NumPy code
|
|
20
|
+
|
|
21
|
+
matrix <A> — show the matrix (e.g. matrix [[1,2],[3,4]])
|
|
22
|
+
det <A> — determinant (e.g. det [[1,2],[3,4]])
|
|
23
|
+
inv <A> — inverse matrix
|
|
24
|
+
transpose <A> — transpose
|
|
25
|
+
eig <A> — eigenvalues
|
|
26
|
+
solve_system <A> = <b> — solve A x = b (e.g. solve_system [[2,1],[1,3]] = [3,5])
|
|
27
|
+
code_system <A> = <b> — generate NumPy code solving the system
|
|
28
|
+
|
|
29
|
+
root <expr> for <var> at <x0> — root f=0 (e.g. root x^2-2 for x at 1)
|
|
30
|
+
nintegrate <expr> d<var> from <a> to <b> — numerical integration (e.g. nintegrate 2*x dx from 0 to 1)
|
|
31
|
+
min <expr> for <var> at <x0> [method newton|bfgs]
|
|
32
|
+
— 1D minimum (gradient descent by
|
|
33
|
+
default; newton/bfgs = 2nd-order
|
|
34
|
+
methods with Armijo line search)
|
|
35
|
+
(e.g. min (x-3)^2 for x at 0 method newton)
|
|
36
|
+
|
|
37
|
+
grad <expr> for <x,y,...> — symbolic gradient (e.g. grad x^2*y for x,y)
|
|
38
|
+
solve_nd <f1>; <f2> for <x,y> at <x0,y0> — nonlinear system via Newton
|
|
39
|
+
(e.g. solve_nd x^2+y^2-4; x-y for x,y at 1,1)
|
|
40
|
+
min_nd <expr> for <x,y> at <x0,y0> [method newton|bfgs]
|
|
41
|
+
— minimum of several variables (bfgs
|
|
42
|
+
copes where gradient descent
|
|
43
|
+
stalls, e.g. the Rosenbrock valley)
|
|
44
|
+
(e.g. min_nd (x-1)^2+(y+2)^2 for x,y at 0,0)
|
|
45
|
+
|
|
46
|
+
dsolve <expr> for y(t) — ODE symbolically: y'(t) = f(t,y)
|
|
47
|
+
(e.g. dsolve y for y(t))
|
|
48
|
+
ode <expr> for y(t) from <t0> to <t1> at <y0> [steps <n>]
|
|
49
|
+
— ODE numerically (RK4, fixed step)
|
|
50
|
+
(e.g. ode y for y(t) from 0 to 1 at 1)
|
|
51
|
+
odestiff <expr> for y(t) from <t0> to <t1> at <y0> [steps <n>]
|
|
52
|
+
— STIFF ODE (BDF2, implicit method
|
|
53
|
+
+ Newton per step; large step where
|
|
54
|
+
explicit methods blow up)
|
|
55
|
+
odestiff_adaptive <expr> for y(t) from <t0> to <t1> at <y0> [rtol <r>]
|
|
56
|
+
— STIFF ODE with ADAPTIVE step
|
|
57
|
+
(variable-step BDF2 + error
|
|
58
|
+
estimate; an order fewer steps than
|
|
59
|
+
DOPRI5 on a stiff problem)
|
|
60
|
+
ode_adaptive <expr> for y(t) from <t0> to <t1> at <y0> [rtol <r>]
|
|
61
|
+
— ODE numerically (Dormand-Prince 5(4),
|
|
62
|
+
adaptive step)
|
|
63
|
+
odedense <expr> for y(t) from <t0> to <t1> at <y0> at t=<point> [rtol <r>]
|
|
64
|
+
— ODE with dense output: read y(t)
|
|
65
|
+
at any point (DOPRI5 interpolant)
|
|
66
|
+
odeevents <expr> for y(t) from <t0> to <t1> at <y0> zero <g> [dir <+1|-1>] [rtol <r>] [stop]
|
|
67
|
+
— ODE + event detection: moments
|
|
68
|
+
g(t,y)=0 (bisection on the interpolant);
|
|
69
|
+
the stop flag = TERMINAL event
|
|
70
|
+
(stop integration at the event)
|
|
71
|
+
|
|
72
|
+
help — help
|
|
73
|
+
quit / exit — exit
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
from __future__ import annotations
|
|
77
|
+
|
|
78
|
+
import re
|
|
79
|
+
import sys
|
|
80
|
+
from typing import Callable
|
|
81
|
+
|
|
82
|
+
from ..codegen.pipeline import generate, generate_system
|
|
83
|
+
from ..core.errors import PycodemathError
|
|
84
|
+
from ..core.ir import Matrix
|
|
85
|
+
from ..engine import linalg, numerics, ode, symbolic
|
|
86
|
+
from ..frontend.parser import parse
|
|
87
|
+
|
|
88
|
+
BANNER = "Pycodemath 0.2.0 — heavy math + Python. Type 'help' or 'quit'."
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _num(raw: str) -> float:
|
|
92
|
+
"""Turn a number token from a command into a ``float`` — bad input is a clear error.
|
|
93
|
+
|
|
94
|
+
The ``_NUM`` pattern is intentionally loose (it lets through e.g. ``1e``), so
|
|
95
|
+
correctness is enforced only here — with a readable message, instead of the raw
|
|
96
|
+
``ValueError`` from ``float()`` (contract: always PycodemathError).
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
return float(raw)
|
|
100
|
+
except ValueError as exc:
|
|
101
|
+
raise PycodemathError(f"invalid number: {raw!r}") from exc
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# --- command handlers (receive the regex match, return text) --------
|
|
105
|
+
def _cmd_diff(m: re.Match) -> str:
|
|
106
|
+
return str(symbolic.diff(parse(m["expr"]), m["var"]))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _cmd_integrate(m: re.Match) -> str:
|
|
110
|
+
return str(symbolic.integrate(parse(m["expr"]), m["var"]))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _cmd_solve(m: re.Match) -> str:
|
|
114
|
+
# The REPL cuts complex results by default: transcendental equations (e.g. cos(x)=2)
|
|
115
|
+
# were spewing junk complex roots. The trailing 'complex' flag
|
|
116
|
+
# restores the full set of roots.
|
|
117
|
+
real = not bool(m["complex"])
|
|
118
|
+
roots = symbolic.solve(parse(m["expr"]), m["var"], real=real)
|
|
119
|
+
return ", ".join(str(r) for r in roots) if roots else "(no solutions)"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _cmd_limit(m: re.Match) -> str:
|
|
123
|
+
return str(
|
|
124
|
+
symbolic.limit(
|
|
125
|
+
parse(m["expr"]), m["var"], parse(m["to"]), dir=m["dir"] or "+"
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _cmd_series(m: re.Match) -> str:
|
|
131
|
+
at = parse(m["at"]) if m["at"] else 0
|
|
132
|
+
n = int(m["n"]) if m["n"] else 6
|
|
133
|
+
return str(symbolic.series(parse(m["expr"]), m["var"], at=at, n=n))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _cmd_sum(m: re.Match) -> str:
|
|
137
|
+
return str(
|
|
138
|
+
symbolic.summation(
|
|
139
|
+
parse(m["expr"]), m["var"], parse(m["lo"]), parse(m["hi"])
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _cmd_code(m: re.Match) -> str:
|
|
145
|
+
return generate(parse(m["expr"])).source
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _cmd_matrix(m: re.Match) -> str:
|
|
149
|
+
return str(Matrix(m["a"]))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _cmd_det(m: re.Match) -> str:
|
|
153
|
+
return str(linalg.det(Matrix(m["a"])))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _cmd_inv(m: re.Match) -> str:
|
|
157
|
+
return str(linalg.inv(Matrix(m["a"])))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _cmd_transpose(m: re.Match) -> str:
|
|
161
|
+
return str(linalg.transpose(Matrix(m["a"])))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _cmd_eig(m: re.Match) -> str:
|
|
165
|
+
vals = linalg.eigenvalues(Matrix(m["a"]))
|
|
166
|
+
return ", ".join(str(v) for v in vals) if vals else "(none)"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _cmd_solve_system(m: re.Match) -> str:
|
|
170
|
+
return str(linalg.solve_system(Matrix(m["a"]), Matrix(m["b"])))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _cmd_code_system(m: re.Match) -> str:
|
|
174
|
+
return generate_system(Matrix(m["a"]), Matrix(m["b"])).source
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _cmd_root(m: re.Match) -> str:
|
|
178
|
+
return str(numerics.root_find(parse(m["expr"]), m["var"], _num(m["x0"])))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _cmd_min(m: re.Match) -> str:
|
|
182
|
+
method = (m["method"] or "gd").lower()
|
|
183
|
+
return str(
|
|
184
|
+
numerics.minimize(parse(m["expr"]), m["var"], _num(m["x0"]), method=method)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _cmd_nintegrate(m: re.Match) -> str:
|
|
189
|
+
val = numerics.integrate_num(
|
|
190
|
+
parse(m["expr"]), m["var"], _num(m["a"]), _num(m["b"])
|
|
191
|
+
)
|
|
192
|
+
return str(val)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _split_names(raw: str) -> list[str]:
|
|
196
|
+
return [v.strip() for v in raw.split(",")]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _split_floats(raw: str) -> list[float]:
|
|
200
|
+
return [_num(v.strip()) for v in raw.split(",")]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _cmd_grad(m: re.Match) -> str:
|
|
204
|
+
return str(numerics.gradient(parse(m["expr"]), _split_names(m["vars"])))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _cmd_solve_nd(m: re.Match) -> str:
|
|
208
|
+
exprs = [parse(e.strip()) for e in m["exprs"].split(";")]
|
|
209
|
+
names = _split_names(m["vars"])
|
|
210
|
+
sol = numerics.root_find_nd(exprs, names, _split_floats(m["x0"]))
|
|
211
|
+
return ", ".join(f"{v} = {val}" for v, val in zip(names, sol))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _cmd_min_nd(m: re.Match) -> str:
|
|
215
|
+
names = _split_names(m["vars"])
|
|
216
|
+
method = (m["method"] or "gd").lower()
|
|
217
|
+
sol = numerics.minimize_nd(
|
|
218
|
+
parse(m["expr"]), names, _split_floats(m["x0"]), method=method
|
|
219
|
+
)
|
|
220
|
+
return ", ".join(f"{v} = {val}" for v, val in zip(names, sol))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _cmd_dsolve(m: re.Match) -> str:
|
|
224
|
+
sols = ode.dsolve(parse(m["expr"]), m["func"], m["var"])
|
|
225
|
+
return ", ".join(f"{m['func']}({m['var']}) = {s}" for s in sols)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _cmd_ode(m: re.Match) -> str:
|
|
229
|
+
n = int(m["n"]) if m["n"] else 100
|
|
230
|
+
ts, ys = ode.solve_ode_num(
|
|
231
|
+
parse(m["expr"]),
|
|
232
|
+
m["var"],
|
|
233
|
+
_num(m["y0"]),
|
|
234
|
+
(_num(m["t0"]), _num(m["t1"])),
|
|
235
|
+
n,
|
|
236
|
+
func=m["func"],
|
|
237
|
+
)
|
|
238
|
+
return f"{m['func']}({ts[-1]:g}) = {ys[-1]}"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _cmd_ode_stiff(m: re.Match) -> str:
|
|
242
|
+
n = int(m["n"]) if m["n"] else 100
|
|
243
|
+
ts, ys = ode.solve_ode_stiff(
|
|
244
|
+
parse(m["expr"]),
|
|
245
|
+
m["var"],
|
|
246
|
+
_num(m["y0"]),
|
|
247
|
+
(_num(m["t0"]), _num(m["t1"])),
|
|
248
|
+
n,
|
|
249
|
+
func=m["func"],
|
|
250
|
+
)
|
|
251
|
+
return f"{m['func']}({ts[-1]:g}) = {ys[-1]} (BDF2, {n} implicit steps)"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _cmd_ode_stiff_adaptive(m: re.Match) -> str:
|
|
255
|
+
rtol = _num(m["rtol"]) if m["rtol"] else 1e-6
|
|
256
|
+
ts, ys = ode.solve_ode_stiff_adaptive(
|
|
257
|
+
parse(m["expr"]),
|
|
258
|
+
m["var"],
|
|
259
|
+
_num(m["y0"]),
|
|
260
|
+
(_num(m["t0"]), _num(m["t1"])),
|
|
261
|
+
rtol=rtol,
|
|
262
|
+
func=m["func"],
|
|
263
|
+
)
|
|
264
|
+
return (
|
|
265
|
+
f"{m['func']}({ts[-1]:g}) = {ys[-1]}"
|
|
266
|
+
f" (adaptive BDF, {len(ts) - 1} implicit steps)"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _cmd_ode_adaptive(m: re.Match) -> str:
|
|
271
|
+
rtol = _num(m["rtol"]) if m["rtol"] else 1e-6
|
|
272
|
+
ts, ys = ode.solve_ode_adaptive(
|
|
273
|
+
parse(m["expr"]),
|
|
274
|
+
m["var"],
|
|
275
|
+
_num(m["y0"]),
|
|
276
|
+
(_num(m["t0"]), _num(m["t1"])),
|
|
277
|
+
rtol=rtol,
|
|
278
|
+
func=m["func"],
|
|
279
|
+
)
|
|
280
|
+
return f"{m['func']}({ts[-1]:g}) = {ys[-1]} ({len(ts) - 1} adaptive steps)"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _cmd_ode_dense(m: re.Match) -> str:
|
|
284
|
+
rtol = _num(m["rtol"]) if m["rtol"] else 1e-6
|
|
285
|
+
sol = ode.solve_ode_dense(
|
|
286
|
+
parse(m["expr"]),
|
|
287
|
+
m["var"],
|
|
288
|
+
_num(m["y0"]),
|
|
289
|
+
(_num(m["t0"]), _num(m["t1"])),
|
|
290
|
+
rtol=rtol,
|
|
291
|
+
func=m["func"],
|
|
292
|
+
)
|
|
293
|
+
tq = _num(m["tq"])
|
|
294
|
+
return f"{m['func']}({tq:g}) = {sol(tq)} (dense output from {len(sol.ts) - 1} steps)"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _cmd_ode_events(m: re.Match) -> str:
|
|
298
|
+
direction = int(m["dir"]) if m["dir"] else 0
|
|
299
|
+
rtol = _num(m["rtol"]) if m["rtol"] else 1e-6
|
|
300
|
+
terminal = bool(m["stop"])
|
|
301
|
+
sol = ode.solve_ode_events(
|
|
302
|
+
parse(m["expr"]),
|
|
303
|
+
m["var"],
|
|
304
|
+
_num(m["y0"]),
|
|
305
|
+
(_num(m["t0"]), _num(m["t1"])),
|
|
306
|
+
parse(m["g"]),
|
|
307
|
+
direction=direction,
|
|
308
|
+
terminal=terminal,
|
|
309
|
+
rtol=rtol,
|
|
310
|
+
func=m["func"],
|
|
311
|
+
)
|
|
312
|
+
if not sol.events:
|
|
313
|
+
return "(no events)"
|
|
314
|
+
listed = "; ".join(f"t={t:g} ({m['func']}={y:g})" for t, y in sol.events)
|
|
315
|
+
if terminal:
|
|
316
|
+
return f"stop at {listed} (integration stopped at the event)"
|
|
317
|
+
return listed
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# --- command table --------------------------------------------------------
|
|
321
|
+
# Key = the first token of the line (lowercased). Value = (full pattern,
|
|
322
|
+
# handler, usage message). One pattern per command instead of manual
|
|
323
|
+
# slicing — each command's syntax is explicit and testable in one place.
|
|
324
|
+
_NUM = r"[-+]?[\d.eE+-]+" # number for float(); bad input is reported by float()
|
|
325
|
+
_NAMES = r"\w+(?:\s*,\s*\w+)*" # list of variables: x,y,z
|
|
326
|
+
_NUMS = rf"{_NUM}(?:\s*,\s*{_NUM})*" # list of numbers: 1,-2,0.5
|
|
327
|
+
|
|
328
|
+
_COMMANDS: dict[str, tuple[re.Pattern[str], Callable[[re.Match], str], str]] = {
|
|
329
|
+
"diff": (
|
|
330
|
+
re.compile(r"^diff\s+(?P<expr>.+)\s+d(?P<var>\w+)\s*$", re.IGNORECASE),
|
|
331
|
+
_cmd_diff,
|
|
332
|
+
"Usage: diff <expression> d<variable> (e.g. diff sin(x)*x dx)",
|
|
333
|
+
),
|
|
334
|
+
"integrate": (
|
|
335
|
+
re.compile(r"^integrate\s+(?P<expr>.+)\s+d(?P<var>\w+)\s*$", re.IGNORECASE),
|
|
336
|
+
_cmd_integrate,
|
|
337
|
+
"Usage: integrate <expression> d<variable> (e.g. integrate 2*x dx)",
|
|
338
|
+
),
|
|
339
|
+
"solve": (
|
|
340
|
+
re.compile(
|
|
341
|
+
r"^solve\s+(?P<expr>.+)\s+for\s+(?P<var>\w+)"
|
|
342
|
+
r"(?:\s+(?P<complex>complex))?\s*$",
|
|
343
|
+
re.IGNORECASE,
|
|
344
|
+
),
|
|
345
|
+
_cmd_solve,
|
|
346
|
+
"Usage: solve <expression> for <variable> [complex]"
|
|
347
|
+
" (e.g. solve x^2-4 for x; real only by default)",
|
|
348
|
+
),
|
|
349
|
+
"limit": (
|
|
350
|
+
re.compile(
|
|
351
|
+
r"^limit\s+(?P<expr>.+)\s+for\s+(?P<var>\w+)\s+to\s+(?P<to>\S+)"
|
|
352
|
+
r"(?:\s+dir\s+(?P<dir>[+-]))?\s*$",
|
|
353
|
+
re.IGNORECASE,
|
|
354
|
+
),
|
|
355
|
+
_cmd_limit,
|
|
356
|
+
"Usage: limit <expression> for <variable> to <point> [dir +|-]"
|
|
357
|
+
" (e.g. limit sin(x)/x for x to 0)",
|
|
358
|
+
),
|
|
359
|
+
"series": (
|
|
360
|
+
re.compile(
|
|
361
|
+
r"^series\s+(?P<expr>.+)\s+for\s+(?P<var>\w+)"
|
|
362
|
+
r"(?:\s+at\s+(?P<at>\S+))?(?:\s+n\s+(?P<n>\d+))?\s*$",
|
|
363
|
+
re.IGNORECASE,
|
|
364
|
+
),
|
|
365
|
+
_cmd_series,
|
|
366
|
+
"Usage: series <expression> for <variable> [at <point>] [n <order>]"
|
|
367
|
+
" (e.g. series exp(x) for x n 4)",
|
|
368
|
+
),
|
|
369
|
+
"sum": (
|
|
370
|
+
re.compile(
|
|
371
|
+
r"^sum\s+(?P<expr>.+)\s+for\s+(?P<var>\w+)"
|
|
372
|
+
r"\s+from\s+(?P<lo>\S+)\s+to\s+(?P<hi>\S+)\s*$",
|
|
373
|
+
re.IGNORECASE,
|
|
374
|
+
),
|
|
375
|
+
_cmd_sum,
|
|
376
|
+
"Usage: sum <expression> for <variable> from <lo> to <hi>"
|
|
377
|
+
" (e.g. sum k for k from 1 to n)",
|
|
378
|
+
),
|
|
379
|
+
"code": (
|
|
380
|
+
re.compile(r"^code\s+(?P<expr>.+?)\s*$", re.IGNORECASE),
|
|
381
|
+
_cmd_code,
|
|
382
|
+
"Usage: code <expression> (e.g. code (sin(x)+cos(x))^2)",
|
|
383
|
+
),
|
|
384
|
+
"matrix": (
|
|
385
|
+
re.compile(r"^matrix\s+(?P<a>.+?)\s*$", re.IGNORECASE),
|
|
386
|
+
_cmd_matrix,
|
|
387
|
+
"Usage: matrix <A> (e.g. matrix [[1,2],[3,4]])",
|
|
388
|
+
),
|
|
389
|
+
"det": (
|
|
390
|
+
re.compile(r"^det\s+(?P<a>.+?)\s*$", re.IGNORECASE),
|
|
391
|
+
_cmd_det,
|
|
392
|
+
"Usage: det <A> (e.g. det [[1,2],[3,4]])",
|
|
393
|
+
),
|
|
394
|
+
"inv": (
|
|
395
|
+
re.compile(r"^inv\s+(?P<a>.+?)\s*$", re.IGNORECASE),
|
|
396
|
+
_cmd_inv,
|
|
397
|
+
"Usage: inv <A> (e.g. inv [[4,7],[2,6]])",
|
|
398
|
+
),
|
|
399
|
+
"transpose": (
|
|
400
|
+
re.compile(r"^transpose\s+(?P<a>.+?)\s*$", re.IGNORECASE),
|
|
401
|
+
_cmd_transpose,
|
|
402
|
+
"Usage: transpose <A> (e.g. transpose [[1,2],[3,4]])",
|
|
403
|
+
),
|
|
404
|
+
"eig": (
|
|
405
|
+
re.compile(r"^eig\s+(?P<a>.+?)\s*$", re.IGNORECASE),
|
|
406
|
+
_cmd_eig,
|
|
407
|
+
"Usage: eig <A> (e.g. eig [[2,0],[0,3]])",
|
|
408
|
+
),
|
|
409
|
+
"solve_system": (
|
|
410
|
+
re.compile(
|
|
411
|
+
r"^solve_system\s+(?P<a>.+?)\s*=\s*(?P<b>.+?)\s*$", re.IGNORECASE
|
|
412
|
+
),
|
|
413
|
+
_cmd_solve_system,
|
|
414
|
+
"Usage: solve_system <A> = <b> (e.g. solve_system [[2,1],[1,3]] = [3,5])",
|
|
415
|
+
),
|
|
416
|
+
"code_system": (
|
|
417
|
+
re.compile(
|
|
418
|
+
r"^code_system\s+(?P<a>.+?)\s*=\s*(?P<b>.+?)\s*$", re.IGNORECASE
|
|
419
|
+
),
|
|
420
|
+
_cmd_code_system,
|
|
421
|
+
"Usage: code_system <A> = <b> (e.g. code_system [[2,1],[1,3]] = [3,5])",
|
|
422
|
+
),
|
|
423
|
+
"root": (
|
|
424
|
+
re.compile(
|
|
425
|
+
rf"^root\s+(?P<expr>.+)\s+for\s+(?P<var>\w+)\s+at\s+(?P<x0>{_NUM})\s*$",
|
|
426
|
+
re.IGNORECASE,
|
|
427
|
+
),
|
|
428
|
+
_cmd_root,
|
|
429
|
+
"Usage: root <expression> for <variable> at <x0> (e.g. root x^2-2 for x at 1)",
|
|
430
|
+
),
|
|
431
|
+
"min": (
|
|
432
|
+
re.compile(
|
|
433
|
+
rf"^min\s+(?P<expr>.+)\s+for\s+(?P<var>\w+)\s+at\s+(?P<x0>{_NUM})"
|
|
434
|
+
rf"(?:\s+method\s+(?P<method>\w+))?\s*$",
|
|
435
|
+
re.IGNORECASE,
|
|
436
|
+
),
|
|
437
|
+
_cmd_min,
|
|
438
|
+
"Usage: min <expression> for <variable> at <x0> [method newton|bfgs]"
|
|
439
|
+
" (e.g. min (x-3)^2 for x at 0 method newton)",
|
|
440
|
+
),
|
|
441
|
+
"nintegrate": (
|
|
442
|
+
re.compile(
|
|
443
|
+
rf"^nintegrate\s+(?P<expr>.+)\s+d(?P<var>\w+)"
|
|
444
|
+
rf"\s+from\s+(?P<a>{_NUM})\s+to\s+(?P<b>{_NUM})\s*$",
|
|
445
|
+
re.IGNORECASE,
|
|
446
|
+
),
|
|
447
|
+
_cmd_nintegrate,
|
|
448
|
+
"Usage: nintegrate <expression> d<variable> from <a> to <b>"
|
|
449
|
+
" (e.g. nintegrate 2*x dx from 0 to 1)",
|
|
450
|
+
),
|
|
451
|
+
"grad": (
|
|
452
|
+
re.compile(
|
|
453
|
+
rf"^grad\s+(?P<expr>.+)\s+for\s+(?P<vars>{_NAMES})\s*$", re.IGNORECASE
|
|
454
|
+
),
|
|
455
|
+
_cmd_grad,
|
|
456
|
+
"Usage: grad <expression> for <x,y,...> (e.g. grad x^2*y for x,y)",
|
|
457
|
+
),
|
|
458
|
+
"solve_nd": (
|
|
459
|
+
re.compile(
|
|
460
|
+
rf"^solve_nd\s+(?P<exprs>.+)\s+for\s+(?P<vars>{_NAMES})"
|
|
461
|
+
rf"\s+at\s+(?P<x0>{_NUMS})\s*$",
|
|
462
|
+
re.IGNORECASE,
|
|
463
|
+
),
|
|
464
|
+
_cmd_solve_nd,
|
|
465
|
+
"Usage: solve_nd <f1>; <f2> for <x,y> at <x0,y0>"
|
|
466
|
+
" (e.g. solve_nd x^2+y^2-4; x-y for x,y at 1,1)",
|
|
467
|
+
),
|
|
468
|
+
"min_nd": (
|
|
469
|
+
re.compile(
|
|
470
|
+
rf"^min_nd\s+(?P<expr>.+)\s+for\s+(?P<vars>{_NAMES})"
|
|
471
|
+
rf"\s+at\s+(?P<x0>{_NUMS})(?:\s+method\s+(?P<method>\w+))?\s*$",
|
|
472
|
+
re.IGNORECASE,
|
|
473
|
+
),
|
|
474
|
+
_cmd_min_nd,
|
|
475
|
+
"Usage: min_nd <expression> for <x,y> at <x0,y0> [method newton|bfgs]"
|
|
476
|
+
" (e.g. min_nd (x-1)^2+(y+2)^2 for x,y at 0,0 method bfgs)",
|
|
477
|
+
),
|
|
478
|
+
"dsolve": (
|
|
479
|
+
re.compile(
|
|
480
|
+
r"^dsolve\s+(?P<expr>.+)\s+for\s+(?P<func>\w+)\((?P<var>\w+)\)\s*$",
|
|
481
|
+
re.IGNORECASE,
|
|
482
|
+
),
|
|
483
|
+
_cmd_dsolve,
|
|
484
|
+
"Usage: dsolve <expression> for <y>(<t>) (e.g. dsolve y for y(t))",
|
|
485
|
+
),
|
|
486
|
+
"ode": (
|
|
487
|
+
re.compile(
|
|
488
|
+
rf"^ode\s+(?P<expr>.+)\s+for\s+(?P<func>\w+)\((?P<var>\w+)\)"
|
|
489
|
+
rf"\s+from\s+(?P<t0>{_NUM})\s+to\s+(?P<t1>{_NUM})\s+at\s+(?P<y0>{_NUM})"
|
|
490
|
+
rf"(?:\s+steps\s+(?P<n>\d+))?\s*$",
|
|
491
|
+
re.IGNORECASE,
|
|
492
|
+
),
|
|
493
|
+
_cmd_ode,
|
|
494
|
+
"Usage: ode <expression> for <y>(<t>) from <t0> to <t1> at <y0> [steps <n>]"
|
|
495
|
+
" (e.g. ode y for y(t) from 0 to 1 at 1)",
|
|
496
|
+
),
|
|
497
|
+
"odestiff": (
|
|
498
|
+
re.compile(
|
|
499
|
+
rf"^odestiff\s+(?P<expr>.+)\s+for\s+(?P<func>\w+)\((?P<var>\w+)\)"
|
|
500
|
+
rf"\s+from\s+(?P<t0>{_NUM})\s+to\s+(?P<t1>{_NUM})\s+at\s+(?P<y0>{_NUM})"
|
|
501
|
+
rf"(?:\s+steps\s+(?P<n>\d+))?\s*$",
|
|
502
|
+
re.IGNORECASE,
|
|
503
|
+
),
|
|
504
|
+
_cmd_ode_stiff,
|
|
505
|
+
"Usage: odestiff <expression> for <y>(<t>) from <t0> to <t1> at <y0> [steps <n>]"
|
|
506
|
+
" (e.g. odestiff -1000*(y-cos(t)) for y(t) from 0 to 1 at 0)",
|
|
507
|
+
),
|
|
508
|
+
"odestiff_adaptive": (
|
|
509
|
+
re.compile(
|
|
510
|
+
rf"^odestiff_adaptive\s+(?P<expr>.+)\s+for\s+(?P<func>\w+)\((?P<var>\w+)\)"
|
|
511
|
+
rf"\s+from\s+(?P<t0>{_NUM})\s+to\s+(?P<t1>{_NUM})\s+at\s+(?P<y0>{_NUM})"
|
|
512
|
+
rf"(?:\s+rtol\s+(?P<rtol>{_NUM}))?\s*$",
|
|
513
|
+
re.IGNORECASE,
|
|
514
|
+
),
|
|
515
|
+
_cmd_ode_stiff_adaptive,
|
|
516
|
+
"Usage: odestiff_adaptive <expression> for <y>(<t>) from <t0> to <t1> at <y0> [rtol <r>]"
|
|
517
|
+
" (e.g. odestiff_adaptive -1000*(y-cos(t)) for y(t) from 0 to 1 at 0)",
|
|
518
|
+
),
|
|
519
|
+
"ode_adaptive": (
|
|
520
|
+
re.compile(
|
|
521
|
+
rf"^ode_adaptive\s+(?P<expr>.+)\s+for\s+(?P<func>\w+)\((?P<var>\w+)\)"
|
|
522
|
+
rf"\s+from\s+(?P<t0>{_NUM})\s+to\s+(?P<t1>{_NUM})\s+at\s+(?P<y0>{_NUM})"
|
|
523
|
+
rf"(?:\s+rtol\s+(?P<rtol>{_NUM}))?\s*$",
|
|
524
|
+
re.IGNORECASE,
|
|
525
|
+
),
|
|
526
|
+
_cmd_ode_adaptive,
|
|
527
|
+
"Usage: ode_adaptive <expression> for <y>(<t>) from <t0> to <t1> at <y0> [rtol <r>]"
|
|
528
|
+
" (e.g. ode_adaptive y for y(t) from 0 to 1 at 1)",
|
|
529
|
+
),
|
|
530
|
+
"odedense": (
|
|
531
|
+
re.compile(
|
|
532
|
+
rf"^odedense\s+(?P<expr>.+)\s+for\s+(?P<func>\w+)\((?P<var>\w+)\)"
|
|
533
|
+
rf"\s+from\s+(?P<t0>{_NUM})\s+to\s+(?P<t1>{_NUM})\s+at\s+(?P<y0>{_NUM})"
|
|
534
|
+
rf"\s+at\s+t\s*=\s*(?P<tq>{_NUM})(?:\s+rtol\s+(?P<rtol>{_NUM}))?\s*$",
|
|
535
|
+
re.IGNORECASE,
|
|
536
|
+
),
|
|
537
|
+
_cmd_ode_dense,
|
|
538
|
+
"Usage: odedense <expression> for <y>(<t>) from <t0> to <t1> at <y0> at t=<point> [rtol <r>]"
|
|
539
|
+
" (e.g. odedense y for y(t) from 0 to 2 at 1 at t=0.37)",
|
|
540
|
+
),
|
|
541
|
+
"odeevents": (
|
|
542
|
+
re.compile(
|
|
543
|
+
rf"^odeevents\s+(?P<expr>.+)\s+for\s+(?P<func>\w+)\((?P<var>\w+)\)"
|
|
544
|
+
rf"\s+from\s+(?P<t0>{_NUM})\s+to\s+(?P<t1>{_NUM})\s+at\s+(?P<y0>{_NUM})"
|
|
545
|
+
rf"\s+zero\s+(?P<g>.+?)(?:\s+dir\s+(?P<dir>[-+]?1))?"
|
|
546
|
+
rf"(?:\s+rtol\s+(?P<rtol>{_NUM}))?(?:\s+(?P<stop>stop))?\s*$",
|
|
547
|
+
re.IGNORECASE,
|
|
548
|
+
),
|
|
549
|
+
_cmd_ode_events,
|
|
550
|
+
"Usage: odeevents <expression> for <y>(<t>) from <t0> to <t1> at <y0> zero <g> [dir <+1|-1>] [rtol <r>] [stop]"
|
|
551
|
+
" (e.g. odeevents cos(t) for y(t) from 0.5 to 10 at 0.479 zero y)",
|
|
552
|
+
),
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def handle(line: str) -> str:
|
|
557
|
+
"""Handle one line of REPL input and return the response text."""
|
|
558
|
+
line = line.strip()
|
|
559
|
+
if not line:
|
|
560
|
+
return ""
|
|
561
|
+
|
|
562
|
+
if line.lower() in ("help", "?"):
|
|
563
|
+
return __doc__ or ""
|
|
564
|
+
|
|
565
|
+
token = line.split(None, 1)[0].lower()
|
|
566
|
+
entry = _COMMANDS.get(token)
|
|
567
|
+
if entry is not None:
|
|
568
|
+
pattern, handler, usage = entry
|
|
569
|
+
match = pattern.match(line)
|
|
570
|
+
if match is None:
|
|
571
|
+
return usage
|
|
572
|
+
return handler(match)
|
|
573
|
+
|
|
574
|
+
# default: parse and simplify
|
|
575
|
+
return str(symbolic.simplify(parse(line)))
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def run() -> None:
|
|
579
|
+
"""Run the REPL — interactively or one-shot from command-line arguments.
|
|
580
|
+
|
|
581
|
+
``python -m pycodemath "diff sin(x)*x dx"`` (or the ``pycodemath`` script)
|
|
582
|
+
runs a single command and exits: result on stdout, error on stderr with exit
|
|
583
|
+
code 1 — this is what makes Pycodemath scriptable (e.g. as a skill).
|
|
584
|
+
"""
|
|
585
|
+
# The Windows console is sometimes cp1252 — force UTF-8 so non-ASCII characters work.
|
|
586
|
+
for stream in (sys.stdout, sys.stderr):
|
|
587
|
+
if hasattr(stream, "reconfigure"):
|
|
588
|
+
stream.reconfigure(encoding="utf-8")
|
|
589
|
+
|
|
590
|
+
try: # history and line editing on POSIX; the Windows console has this natively
|
|
591
|
+
import readline # noqa: F401
|
|
592
|
+
except ImportError:
|
|
593
|
+
pass
|
|
594
|
+
|
|
595
|
+
args = sys.argv[1:]
|
|
596
|
+
if args:
|
|
597
|
+
try:
|
|
598
|
+
out = handle(" ".join(args))
|
|
599
|
+
except Exception as exc:
|
|
600
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
601
|
+
raise SystemExit(1)
|
|
602
|
+
if out:
|
|
603
|
+
print(out)
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
print(BANNER)
|
|
607
|
+
while True:
|
|
608
|
+
try:
|
|
609
|
+
line = input("pcm> ")
|
|
610
|
+
except (EOFError, KeyboardInterrupt):
|
|
611
|
+
print()
|
|
612
|
+
break
|
|
613
|
+
if line.strip().lower() in ("quit", "exit"):
|
|
614
|
+
break
|
|
615
|
+
try:
|
|
616
|
+
out = handle(line)
|
|
617
|
+
except Exception as exc: # in the REPL an error must not kill the session
|
|
618
|
+
out = f"error: {exc}"
|
|
619
|
+
if out:
|
|
620
|
+
print(out)
|
|
File without changes
|