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/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