slangmath 1.0.6 → 1.1.0

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.
@@ -0,0 +1,1616 @@
1
+ /**
2
+ * SLaNg Symbolic Engine
3
+ *
4
+ * Adds symbolic (non-numeric) computation support to SLaNg:
5
+ * - Symbolic constants (pi, e, inf)
6
+ * - Trigonometric functions (sin, cos, tan, sec, csc, cot)
7
+ * - Exponential and logarithmic functions (exp, ln, log)
8
+ * - Hyperbolic functions (sinh, cosh, tanh)
9
+ * - Inverse trig (asin, acos, atan)
10
+ * - Symbolic differentiation rules for all of the above
11
+ * - Symbolic integration rules for common forms
12
+ * - Expression simplification with trig identities
13
+ * - LaTeX rendering for function expressions
14
+ *
15
+ * SLaNg Function Expression Format:
16
+ * { type: 'fn', name: 'sin', arg: <expr> }
17
+ * { type: 'fn', name: 'ln', arg: <expr> }
18
+ * { type: 'add', left: <expr>, right: <expr> }
19
+ * { type: 'mul', left: <expr>, right: <expr> }
20
+ * { type: 'pow', base: <expr>, exp: <expr> }
21
+ * { type: 'const', value: number }
22
+ * { type: 'var', name: string }
23
+ * { type: 'neg', arg: <expr> }
24
+ */
25
+
26
+ // ============================================================================
27
+ // SYMBOLIC CONSTANTS
28
+ // ============================================================================
29
+
30
+ export const PI = { type: 'const', value: Math.PI, symbol: '\\pi' };
31
+ export const E = { type: 'const', value: Math.E, symbol: 'e' };
32
+ export const INF = { type: 'const', value: Infinity, symbol: '\\infty' };
33
+
34
+ // ============================================================================
35
+ // EXPRESSION BUILDERS
36
+ // ============================================================================
37
+
38
+ export function symConst(value) {
39
+ return { type: 'const', value };
40
+ }
41
+
42
+ export function symVar(name) {
43
+ return { type: 'var', name };
44
+ }
45
+
46
+ export function symFn(name, arg) {
47
+ return { type: 'fn', name, arg };
48
+ }
49
+
50
+ export function symAdd(left, right) {
51
+ return { type: 'add', left, right };
52
+ }
53
+
54
+ export function symSub(left, right) {
55
+ return { type: 'add', left, right: symNeg(right) };
56
+ }
57
+
58
+ export function symMul(left, right) {
59
+ return { type: 'mul', left, right };
60
+ }
61
+
62
+ export function symDiv(top, bot) {
63
+ return { type: 'div', top, bot };
64
+ }
65
+
66
+ export function symPow(base, exp) {
67
+ return { type: 'pow', base, exp };
68
+ }
69
+
70
+ export function symNeg(arg) {
71
+ return { type: 'neg', arg };
72
+ }
73
+
74
+ // Convenience constructors
75
+ export const sin = arg => symFn('sin', arg);
76
+ export const cos = arg => symFn('cos', arg);
77
+ export const tan = arg => symFn('tan', arg);
78
+ export const sec = arg => symFn('sec', arg);
79
+ export const csc = arg => symFn('csc', arg);
80
+ export const cot = arg => symFn('cot', arg);
81
+ export const asin = arg => symFn('asin', arg);
82
+ export const acos = arg => symFn('acos', arg);
83
+ export const atan = arg => symFn('atan', arg);
84
+ export const sinh = arg => symFn('sinh', arg);
85
+ export const cosh = arg => symFn('cosh', arg);
86
+ export const tanh = arg => symFn('tanh', arg);
87
+ export const ln = arg => symFn('ln', arg);
88
+ export const log10 = arg => symFn('log10',arg);
89
+ export const exp = arg => symFn('exp', arg);
90
+ export const sqrt = arg => symFn('sqrt', arg);
91
+ export const abs = arg => symFn('abs', arg);
92
+
93
+ // ============================================================================
94
+ // SYMBOLIC EVALUATION
95
+ // ============================================================================
96
+
97
+ const FN_EVAL = {
98
+ sin: Math.sin,
99
+ cos: Math.cos,
100
+ tan: Math.tan,
101
+ sec: x => 1 / Math.cos(x),
102
+ csc: x => 1 / Math.sin(x),
103
+ cot: x => Math.cos(x) / Math.sin(x),
104
+ asin: Math.asin,
105
+ acos: Math.acos,
106
+ atan: Math.atan,
107
+ sinh: Math.sinh,
108
+ cosh: Math.cosh,
109
+ tanh: Math.tanh,
110
+ ln: Math.log,
111
+ log10: Math.log10,
112
+ exp: Math.exp,
113
+ sqrt: Math.sqrt,
114
+ abs: Math.abs,
115
+ };
116
+
117
+ /**
118
+ * Evaluate a symbolic expression numerically at a given point.
119
+ * @param {Object} expr - Symbolic expression
120
+ * @param {Object} vars - Variable substitutions, e.g. { x: 2, y: 3 }
121
+ * @returns {number}
122
+ */
123
+ export function symEval(expr, vars = {}) {
124
+ switch (expr.type) {
125
+ case 'const': return expr.value;
126
+ case 'var': {
127
+ if (!(expr.name in vars)) throw new Error(`Variable ${expr.name} not provided`);
128
+ return vars[expr.name];
129
+ }
130
+ case 'neg': return -symEval(expr.arg, vars);
131
+ case 'add': return symEval(expr.left, vars) + symEval(expr.right, vars);
132
+ case 'mul': return symEval(expr.left, vars) * symEval(expr.right, vars);
133
+ case 'div': return symEval(expr.top, vars) / symEval(expr.bot, vars);
134
+ case 'pow': return Math.pow(symEval(expr.base, vars), symEval(expr.exp, vars));
135
+ case 'fn': {
136
+ const evalFn = FN_EVAL[expr.name];
137
+ if (!evalFn) throw new Error(`Unknown function: ${expr.name}`);
138
+ return evalFn(symEval(expr.arg, vars));
139
+ }
140
+ default: throw new Error(`Unknown expression type: ${expr.type}`);
141
+ }
142
+ }
143
+
144
+ // ============================================================================
145
+ // SYMBOLIC DIFFERENTIATION
146
+ // ============================================================================
147
+
148
+ /**
149
+ * Symbolically differentiate an expression with respect to a variable.
150
+ * Applies chain rule, product rule, quotient rule automatically.
151
+ * @param {Object} expr - Symbolic expression
152
+ * @param {string} variable - Differentiation variable
153
+ * @returns {Object} Derivative expression (not simplified)
154
+ */
155
+ export function symDiff(expr, variable) {
156
+ switch (expr.type) {
157
+ case 'const': return symConst(0);
158
+
159
+ case 'var':
160
+ return expr.name === variable ? symConst(1) : symConst(0);
161
+
162
+ case 'neg':
163
+ return symNeg(symDiff(expr.arg, variable));
164
+
165
+ case 'add':
166
+ return symAdd(symDiff(expr.left, variable), symDiff(expr.right, variable));
167
+
168
+ case 'mul': {
169
+ // Product rule: (uv)' = u'v + uv'
170
+ const du = symDiff(expr.left, variable);
171
+ const dv = symDiff(expr.right, variable);
172
+ return symAdd(
173
+ symMul(du, expr.right),
174
+ symMul(expr.left, dv)
175
+ );
176
+ }
177
+
178
+ case 'div': {
179
+ // Quotient rule: (u/v)' = (u'v - uv') / v²
180
+ const du = symDiff(expr.top, variable);
181
+ const dv = symDiff(expr.bot, variable);
182
+ return symDiv(
183
+ symSub(symMul(du, expr.bot), symMul(expr.top, dv)),
184
+ symPow(expr.bot, symConst(2))
185
+ );
186
+ }
187
+
188
+ case 'pow': {
189
+ const base = expr.base;
190
+ const expE = expr.exp;
191
+
192
+ // Check if exponent is constant (power rule)
193
+ if (expE.type === 'const') {
194
+ const n = expE.value;
195
+ // n * base^(n-1) * base'
196
+ const db = symDiff(base, variable);
197
+ return symMul(
198
+ symMul(symConst(n), symPow(base, symConst(n - 1))),
199
+ db
200
+ );
201
+ }
202
+
203
+ // General case: d/dx [f(x)^g(x)] = f^g * (g'*ln(f) + g*f'/f)
204
+ const df = symDiff(base, variable);
205
+ const dg = symDiff(expE, variable);
206
+ return symMul(
207
+ expr,
208
+ symAdd(
209
+ symMul(dg, ln(base)),
210
+ symDiv(symMul(expE, df), base)
211
+ )
212
+ );
213
+ }
214
+
215
+ case 'fn': {
216
+ const u = expr.arg;
217
+ const du = symDiff(u, variable); // chain rule inner derivative
218
+
219
+ // d/dx f(u) = f'(u) * u'
220
+ const outerDeriv = _fnDerivative(expr.name, u);
221
+ return symMul(outerDeriv, du);
222
+ }
223
+
224
+ default:
225
+ throw new Error(`Cannot differentiate expression type: ${expr.type}`);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Returns the derivative of f(u) with respect to u (outer derivative only).
231
+ */
232
+ function _fnDerivative(name, u) {
233
+ switch (name) {
234
+ case 'sin': return cos(u);
235
+ case 'cos': return symNeg(sin(u));
236
+ case 'tan': return symPow(sec(u), symConst(2));
237
+ case 'sec': return symMul(sec(u), tan(u));
238
+ case 'csc': return symNeg(symMul(csc(u), cot(u)));
239
+ case 'cot': return symNeg(symPow(csc(u), symConst(2)));
240
+ case 'asin': return symDiv(symConst(1), sqrt(symSub(symConst(1), symPow(u, symConst(2)))));
241
+ case 'acos': return symNeg(symDiv(symConst(1), sqrt(symSub(symConst(1), symPow(u, symConst(2))))));
242
+ case 'atan': return symDiv(symConst(1), symAdd(symConst(1), symPow(u, symConst(2))));
243
+ case 'sinh': return cosh(u);
244
+ case 'cosh': return sinh(u);
245
+ case 'tanh': return symSub(symConst(1), symPow(tanh(u), symConst(2)));
246
+ case 'ln': return symDiv(symConst(1), u);
247
+ case 'log10': return symDiv(symConst(1), symMul(ln(symConst(10)), u));
248
+ case 'exp': return exp(u);
249
+ case 'sqrt': return symDiv(symConst(1), symMul(symConst(2), sqrt(u)));
250
+ case 'abs': // sign function — not symbolic here
251
+ return symDiv(u, abs(u));
252
+ default:
253
+ throw new Error(`Unknown function derivative: ${name}`);
254
+ }
255
+ }
256
+
257
+ // ============================================================================
258
+ // SYMBOLIC INTEGRATION (common forms)
259
+ // ============================================================================
260
+
261
+ /**
262
+ * Attempt symbolic integration of expr with respect to variable.
263
+ * Handles common forms. Returns null if unable to integrate symbolically.
264
+ * @param {Object} expr
265
+ * @param {string} variable
266
+ * @returns {Object|null} Antiderivative (without +C) or null
267
+ */
268
+ export function symIntegrate(expr, variable) {
269
+ // ∫ c dx = c*x
270
+ if (expr.type === 'const') {
271
+ return symMul(expr, symVar(variable));
272
+ }
273
+
274
+ // ∫ x dx = x²/2 (simple variable)
275
+ if (expr.type === 'var' && expr.name === variable) {
276
+ return symDiv(symPow(symVar(variable), symConst(2)), symConst(2));
277
+ }
278
+
279
+ // ∫ x^n dx = x^(n+1)/(n+1)
280
+ if (expr.type === 'pow' && expr.base.type === 'var' && expr.base.name === variable
281
+ && expr.exp.type === 'const' && expr.exp.value !== -1) {
282
+ const n = expr.exp.value;
283
+ return symDiv(
284
+ symPow(symVar(variable), symConst(n + 1)),
285
+ symConst(n + 1)
286
+ );
287
+ }
288
+
289
+ // ∫ 1/x dx = ln|x|
290
+ if (expr.type === 'div' && expr.top.type === 'const' && expr.top.value === 1
291
+ && expr.bot.type === 'var' && expr.bot.name === variable) {
292
+ return ln(abs(symVar(variable)));
293
+ }
294
+
295
+ // ∫ sin(x) dx = -cos(x)
296
+ if (expr.type === 'fn' && expr.name === 'sin' && _isVar(expr.arg, variable)) {
297
+ return symNeg(cos(symVar(variable)));
298
+ }
299
+ // ∫ cos(x) dx = sin(x)
300
+ if (expr.type === 'fn' && expr.name === 'cos' && _isVar(expr.arg, variable)) {
301
+ return sin(symVar(variable));
302
+ }
303
+ // ∫ tan(x) dx = -ln|cos(x)|
304
+ if (expr.type === 'fn' && expr.name === 'tan' && _isVar(expr.arg, variable)) {
305
+ return symNeg(ln(abs(cos(symVar(variable)))));
306
+ }
307
+ // ∫ sec²(x) dx = tan(x)
308
+ if (expr.type === 'pow' && expr.base.type === 'fn' && expr.base.name === 'sec'
309
+ && _isVar(expr.base.arg, variable) && expr.exp.type === 'const' && expr.exp.value === 2) {
310
+ return tan(symVar(variable));
311
+ }
312
+ // ∫ exp(x) dx = exp(x)
313
+ if (expr.type === 'fn' && expr.name === 'exp' && _isVar(expr.arg, variable)) {
314
+ return exp(symVar(variable));
315
+ }
316
+ // ∫ sinh(x) dx = cosh(x)
317
+ if (expr.type === 'fn' && expr.name === 'sinh' && _isVar(expr.arg, variable)) {
318
+ return cosh(symVar(variable));
319
+ }
320
+ // ∫ cosh(x) dx = sinh(x)
321
+ if (expr.type === 'fn' && expr.name === 'cosh' && _isVar(expr.arg, variable)) {
322
+ return sinh(symVar(variable));
323
+ }
324
+ // ∫ 1/sqrt(1-x²) dx = asin(x)
325
+ // ∫ 1/(1+x²) dx = atan(x)
326
+ if (expr.type === 'div' && expr.top.type === 'const' && expr.top.value === 1) {
327
+ const d = expr.bot;
328
+ if (d.type === 'add') {
329
+ const isOnePlusX2 =
330
+ (d.left.type === 'const' && d.left.value === 1
331
+ && d.right.type === 'pow' && _isVar(d.right.base, variable) && d.right.exp.value === 2) ||
332
+ (d.right.type === 'const' && d.right.value === 1
333
+ && d.left.type === 'pow' && _isVar(d.left.base, variable) && d.left.exp.value === 2);
334
+ if (isOnePlusX2) return atan(symVar(variable));
335
+ }
336
+ }
337
+
338
+ // Addition: ∫(f + g) = ∫f + ∫g
339
+ if (expr.type === 'add') {
340
+ const intLeft = symIntegrate(expr.left, variable);
341
+ const intRight = symIntegrate(expr.right, variable);
342
+ if (intLeft && intRight) return symAdd(intLeft, intRight);
343
+ return null;
344
+ }
345
+
346
+ // Scalar multiple: ∫ c*f = c * ∫f
347
+ if (expr.type === 'mul' && expr.left.type === 'const') {
348
+ const inner = symIntegrate(expr.right, variable);
349
+ if (inner) return symMul(expr.left, inner);
350
+ return null;
351
+ }
352
+ if (expr.type === 'mul' && expr.right.type === 'const') {
353
+ const inner = symIntegrate(expr.left, variable);
354
+ if (inner) return symMul(expr.right, inner);
355
+ return null;
356
+ }
357
+
358
+ return null; // Unable to integrate symbolically
359
+ }
360
+
361
+ function _isVar(expr, variable) {
362
+ return expr.type === 'var' && expr.name === variable;
363
+ }
364
+
365
+ // ============================================================================
366
+ // EXPRESSION SIMPLIFICATION
367
+ // ============================================================================
368
+
369
+ /**
370
+ * Apply basic simplification rules to a symbolic expression.
371
+ * Constant folding, identity elimination, etc.
372
+ * @param {Object} expr
373
+ * @returns {Object} Simplified expression
374
+ */
375
+ export function symSimplify(expr) {
376
+ if (!expr) return expr;
377
+
378
+ switch (expr.type) {
379
+ case 'const':
380
+ case 'var':
381
+ return expr;
382
+
383
+ case 'neg': {
384
+ const a = symSimplify(expr.arg);
385
+ if (a.type === 'const') return symConst(-a.value);
386
+ if (a.type === 'neg') return a.arg; // --x = x
387
+ return symNeg(a);
388
+ }
389
+
390
+ case 'add': {
391
+ const l = symSimplify(expr.left);
392
+ const r = symSimplify(expr.right);
393
+ if (l.type === 'const' && r.type === 'const') return symConst(l.value + r.value);
394
+ if (l.type === 'const' && l.value === 0) return r;
395
+ if (r.type === 'const' && r.value === 0) return l;
396
+ return symAdd(l, r);
397
+ }
398
+
399
+ case 'mul': {
400
+ const l = symSimplify(expr.left);
401
+ const r = symSimplify(expr.right);
402
+ if (l.type === 'const' && r.type === 'const') return symConst(l.value * r.value);
403
+ if (l.type === 'const' && l.value === 0) return symConst(0);
404
+ if (r.type === 'const' && r.value === 0) return symConst(0);
405
+ if (l.type === 'const' && l.value === 1) return r;
406
+ if (r.type === 'const' && r.value === 1) return l;
407
+ if (l.type === 'const' && l.value === -1) return symNeg(r);
408
+ if (r.type === 'const' && r.value === -1) return symNeg(l);
409
+ return symMul(l, r);
410
+ }
411
+
412
+ case 'div': {
413
+ const t = symSimplify(expr.top);
414
+ const b = symSimplify(expr.bot);
415
+ if (t.type === 'const' && b.type === 'const') return symConst(t.value / b.value);
416
+ if (t.type === 'const' && t.value === 0) return symConst(0);
417
+ if (b.type === 'const' && b.value === 1) return t;
418
+ return symDiv(t, b);
419
+ }
420
+
421
+ case 'pow': {
422
+ const base = symSimplify(expr.base);
423
+ const e = symSimplify(expr.exp);
424
+ if (base.type === 'const' && e.type === 'const') return symConst(Math.pow(base.value, e.value));
425
+ if (e.type === 'const' && e.value === 0) return symConst(1);
426
+ if (e.type === 'const' && e.value === 1) return base;
427
+ if (base.type === 'const' && base.value === 1) return symConst(1);
428
+ return symPow(base, e);
429
+ }
430
+
431
+ case 'fn': {
432
+ const a = symSimplify(expr.arg);
433
+ // Constant folding for functions
434
+ if (a.type === 'const') {
435
+ const fn = FN_EVAL[expr.name];
436
+ if (fn) return symConst(fn(a.value));
437
+ }
438
+ return symFn(expr.name, a);
439
+ }
440
+
441
+ default:
442
+ return expr;
443
+ }
444
+ }
445
+
446
+ // ============================================================================
447
+ // LATEX RENDERING
448
+ // ============================================================================
449
+
450
+ /**
451
+ * Convert a symbolic expression to LaTeX string.
452
+ * @param {Object} expr
453
+ * @param {Object} opts
454
+ * @returns {string}
455
+ */
456
+ export function symToLatex(expr, opts = {}) {
457
+ switch (expr.type) {
458
+ case 'const': {
459
+ if (expr.symbol) return expr.symbol;
460
+ // Format nicely: avoid floating-point ugliness for common fractions
461
+ const v = expr.value;
462
+ if (Number.isInteger(v)) return v.toString();
463
+ // Try to represent as fraction
464
+ const frac = _toNiceFraction(v);
465
+ return frac || v.toPrecision(6).replace(/\.?0+$/, '');
466
+ }
467
+ case 'var': return expr.name;
468
+ case 'neg': {
469
+ const inner = symToLatex(expr.arg, opts);
470
+ return `-${_wrapIfNeeded(expr.arg, inner)}`;
471
+ }
472
+ case 'add': {
473
+ const l = symToLatex(expr.left, opts);
474
+ const r = symToLatex(expr.right, opts);
475
+ // If right is negation, show as subtraction
476
+ if (expr.right.type === 'neg') {
477
+ return `${l} - ${symToLatex(expr.right.arg, opts)}`;
478
+ }
479
+ return `${l} + ${r}`;
480
+ }
481
+ case 'mul': {
482
+ const l = symToLatex(expr.left, opts);
483
+ const r = symToLatex(expr.right, opts);
484
+ const lParen = _needsMulParen(expr.left) ? `\\left(${l}\\right)` : l;
485
+ const rParen = _needsMulParen(expr.right) ? `\\left(${r}\\right)` : r;
486
+ // Omit \cdot when right side is a variable or function
487
+ if (expr.right.type === 'var' || expr.right.type === 'fn') {
488
+ return `${lParen} ${rParen}`;
489
+ }
490
+ return `${lParen} \\cdot ${rParen}`;
491
+ }
492
+ case 'div':
493
+ return `\\frac{${symToLatex(expr.top, opts)}}{${symToLatex(expr.bot, opts)}}`;
494
+ case 'pow': {
495
+ const b = symToLatex(expr.base, opts);
496
+ const e = symToLatex(expr.exp, opts);
497
+ const bStr = _needsPowParen(expr.base) ? `\\left(${b}\\right)` : b;
498
+ return `${bStr}^{${e}}`;
499
+ }
500
+ case 'fn': {
501
+ const argLatex = symToLatex(expr.arg, opts);
502
+ return _fnToLatex(expr.name, argLatex);
503
+ }
504
+ default:
505
+ return '?';
506
+ }
507
+ }
508
+
509
+ function _fnToLatex(name, argLatex) {
510
+ const map = {
511
+ sin: `\\sin\\!\\left(${argLatex}\\right)`,
512
+ cos: `\\cos\\!\\left(${argLatex}\\right)`,
513
+ tan: `\\tan\\!\\left(${argLatex}\\right)`,
514
+ sec: `\\sec\\!\\left(${argLatex}\\right)`,
515
+ csc: `\\csc\\!\\left(${argLatex}\\right)`,
516
+ cot: `\\cot\\!\\left(${argLatex}\\right)`,
517
+ asin: `\\arcsin\\!\\left(${argLatex}\\right)`,
518
+ acos: `\\arccos\\!\\left(${argLatex}\\right)`,
519
+ atan: `\\arctan\\!\\left(${argLatex}\\right)`,
520
+ sinh: `\\sinh\\!\\left(${argLatex}\\right)`,
521
+ cosh: `\\cosh\\!\\left(${argLatex}\\right)`,
522
+ tanh: `\\tanh\\!\\left(${argLatex}\\right)`,
523
+ ln: `\\ln\\!\\left(${argLatex}\\right)`,
524
+ log10: `\\log_{10}\\!\\left(${argLatex}\\right)`,
525
+ exp: `e^{${argLatex}}`,
526
+ sqrt: `\\sqrt{${argLatex}}`,
527
+ abs: `\\left|${argLatex}\\right|`,
528
+ };
529
+ return map[name] || `\\operatorname{${name}}\\!\\left(${argLatex}\\right)`;
530
+ }
531
+
532
+ function _wrapIfNeeded(expr, latex) {
533
+ if (expr.type === 'add' || expr.type === 'mul') return `\\left(${latex}\\right)`;
534
+ return latex;
535
+ }
536
+
537
+ function _needsMulParen(expr) {
538
+ return expr.type === 'add' || expr.type === 'neg';
539
+ }
540
+
541
+ function _needsPowParen(expr) {
542
+ return expr.type !== 'const' && expr.type !== 'var' && expr.type !== 'fn';
543
+ }
544
+
545
+ function _toNiceFraction(v) {
546
+ for (let d = 2; d <= 12; d++) {
547
+ const n = Math.round(v * d);
548
+ if (Math.abs(n / d - v) < 1e-12) {
549
+ return `\\frac{${n}}{${d}}`;
550
+ }
551
+ }
552
+ return null;
553
+ }
554
+
555
+ // ============================================================================
556
+ // NUMERICAL INTEGRATION (for symbolic expressions)
557
+ // ============================================================================
558
+
559
+ /**
560
+ * Numerically integrate a symbolic expression over [a, b] using Simpson's rule.
561
+ * Useful when symIntegrate returns null.
562
+ * @param {Object} expr
563
+ * @param {string} variable
564
+ * @param {number} a - Lower bound
565
+ * @param {number} b - Upper bound
566
+ * @param {number} n - Number of subintervals (must be even), default 1000
567
+ * @returns {number}
568
+ */
569
+ export function symNumericalIntegrate(expr, variable, a, b, n = 1000) {
570
+ if (n % 2 !== 0) n++; // must be even for Simpson's
571
+ const h = (b - a) / n;
572
+ let sum = symEval(expr, { [variable]: a }) + symEval(expr, { [variable]: b });
573
+
574
+ for (let i = 1; i < n; i++) {
575
+ const x = a + i * h;
576
+ sum += (i % 2 === 0 ? 2 : 4) * symEval(expr, { [variable]: x });
577
+ }
578
+
579
+ return (h / 3) * sum;
580
+ }
581
+
582
+ // ============================================================================
583
+ // HIGHER-ORDER DIFFERENTIATION
584
+ // ============================================================================
585
+
586
+ /**
587
+ * Compute nth derivative of an expression.
588
+ * @param {Object} expr
589
+ * @param {string} variable
590
+ * @param {number} n - Order (default 1)
591
+ * @returns {Object} nth derivative (simplified)
592
+ */
593
+ export function symNthDiff(expr, variable, n = 1) {
594
+ let result = expr;
595
+ for (let i = 0; i < n; i++) {
596
+ result = symSimplify(symDiff(result, variable));
597
+ }
598
+ return result;
599
+ }
600
+
601
+ // ============================================================================
602
+ // TAYLOR / MACLAURIN SERIES
603
+ // ============================================================================
604
+
605
+ /**
606
+ * Compute Taylor series expansion of a symbolic expression.
607
+ * @param {Object} expr
608
+ * @param {string} variable
609
+ * @param {number} center - Point of expansion (a)
610
+ * @param {number} terms - Number of terms (default 5)
611
+ * @returns {{ polynomial: Object, coefficients: number[], latex: string }}
612
+ */
613
+ export function symTaylorSeries(expr, variable, center = 0, terms = 5) {
614
+ const coefficients = [];
615
+ let current = expr;
616
+ let factorial = 1;
617
+
618
+ for (let n = 0; n < terms; n++) {
619
+ if (n > 0) factorial *= n;
620
+ try {
621
+ const val = symEval(current, { [variable]: center });
622
+ coefficients.push(val / factorial);
623
+ } catch {
624
+ coefficients.push(0);
625
+ }
626
+ if (n < terms - 1) {
627
+ current = symSimplify(symDiff(current, variable));
628
+ }
629
+ }
630
+
631
+ // Build latex string
632
+ const parts = coefficients.map((c, n) => {
633
+ if (Math.abs(c) < 1e-14) return null;
634
+ const cStr = c === 1 ? '' : c === -1 ? '-' : _niceNum(c);
635
+ if (n === 0) return _niceNum(c);
636
+ const xPart = center === 0
637
+ ? (n === 1 ? variable : `${variable}^{${n}}`)
638
+ : (n === 1 ? `(${variable}-${center})` : `(${variable}-${center})^{${n}}`);
639
+ return (c < 0 ? `${cStr}${xPart}` : `${cStr}${xPart}`);
640
+ }).filter(Boolean);
641
+
642
+ let latex = parts.join(' + ').replace(/\+ -/g, '- ');
643
+
644
+ return {
645
+ coefficients,
646
+ latex: latex || '0',
647
+ evaluate: (x) => coefficients.reduce((sum, c, n) => {
648
+ return sum + c * Math.pow(x - center, n);
649
+ }, 0)
650
+ };
651
+ }
652
+
653
+ function _niceNum(v) {
654
+ if (Number.isInteger(v)) return v.toString();
655
+ const frac = _toNiceFraction(v);
656
+ if (frac) return frac;
657
+ return v.toPrecision(5).replace(/\.?0+$/, '');
658
+ }
659
+
660
+ // ============================================================================
661
+ // EXPRESSION EQUALITY CHECK
662
+ // ============================================================================
663
+
664
+ /**
665
+ * Test if two symbolic expressions are numerically equivalent at random points.
666
+ * @param {Object} expr1
667
+ * @param {Object} expr2
668
+ * @param {string[]} variables
669
+ * @param {number} samples
670
+ * @returns {boolean}
671
+ */
672
+ export function symAreEqual(expr1, expr2, variables, samples = 20) {
673
+ for (let i = 0; i < samples; i++) {
674
+ const point = {};
675
+ for (const v of variables) {
676
+ point[v] = Math.random() * 4 + 0.5; // avoid 0 to prevent ln(0), 1/0, etc.
677
+ }
678
+ try {
679
+ const v1 = symEval(expr1, point);
680
+ const v2 = symEval(expr2, point);
681
+ if (Math.abs(v1 - v2) > 1e-8 * (1 + Math.abs(v1))) return false;
682
+ } catch {
683
+ // If evaluation fails at a point, skip it
684
+ }
685
+ }
686
+ return true;
687
+ }
688
+
689
+ // ============================================================================
690
+ // COMBINED SLANG + SYMBOLIC BRIDGE
691
+ // ============================================================================
692
+
693
+ /**
694
+ * Convert a SLaNg fraction (polynomial form) to a symbolic expression.
695
+ * Enables using symDiff / symIntegrate on legacy SLaNg fractions.
696
+ * @param {Object} fraction - SLaNg fraction { numi: { terms }, deno }
697
+ * @returns {Object} Symbolic expression
698
+ */
699
+ export function slangToSym(fraction) {
700
+ const numTerms = fraction.numi.terms.map(_slangTermToSym);
701
+ const numerator = numTerms.reduce((acc, t) => acc ? symAdd(acc, t) : t, null) || symConst(0);
702
+
703
+ if (fraction.deno === 1 || fraction.deno === undefined) {
704
+ return numerator;
705
+ }
706
+
707
+ // Polynomial denominator
708
+ if (typeof fraction.deno === 'object' && fraction.deno.terms) {
709
+ const denTerms = fraction.deno.terms.map(_slangTermToSym);
710
+ const denominator = denTerms.reduce((acc, t) => acc ? symAdd(acc, t) : t, null) || symConst(1);
711
+ return symDiv(numerator, denominator);
712
+ }
713
+
714
+ return symDiv(numerator, symConst(fraction.deno));
715
+ }
716
+
717
+ function _slangTermToSym(term) {
718
+ let result = symConst(term.coeff);
719
+ if (term.var) {
720
+ for (const [varName, power] of Object.entries(term.var)) {
721
+ if (power === 1) {
722
+ result = symMul(result, symVar(varName));
723
+ } else {
724
+ result = symMul(result, symPow(symVar(varName), symConst(power)));
725
+ }
726
+ }
727
+ }
728
+ return result;
729
+ }
730
+
731
+ // ============================================================================
732
+ // EXPORTS (named)
733
+ // ============================================================================
734
+
735
+ export {
736
+ FN_EVAL,
737
+ };
738
+
739
+
740
+ // ============================================================================
741
+ // LIMITS (L'Hôpital's Rule + two-sided approach)
742
+ // ============================================================================
743
+
744
+ /**
745
+ * Compute lim_{variable → value} expr.
746
+ * Tries direct substitution first, then L'Hôpital if 0/0 or ∞/∞.
747
+ *
748
+ * @param {Object} expr SymExpr
749
+ * @param {string} variable
750
+ * @param {number} value approach point (use Infinity for ∞)
751
+ * @param {number} [maxHopital=5] max applications of L'Hôpital
752
+ * @returns {{ limit: number|null, method: string, exists: boolean, leftLimit?: number, rightLimit?: number }}
753
+ */
754
+ export function computeLimit(expr, variable, value, maxHopital = 5) {
755
+ const eps = 1e-8;
756
+
757
+ const _eval = x => {
758
+ try { return symEval(expr, { [variable]: x }); } catch { return NaN; }
759
+ };
760
+
761
+ // Direct substitution
762
+ if (isFinite(value)) {
763
+ const v = _eval(value);
764
+ if (isFinite(v)) return { limit: v, method: 'direct substitution', exists: true };
765
+ }
766
+
767
+ // Two-sided check
768
+ const approach = isFinite(value) ? value : (value > 0 ? 1e15 : -1e15);
769
+ const delta = isFinite(value) ? eps : 1e13;
770
+
771
+ const left = _eval(approach - delta);
772
+ const right = _eval(approach + delta);
773
+
774
+ if (isFinite(left) && isFinite(right) && Math.abs(left - right) < 1e-6 * (1 + Math.abs(left))) {
775
+ return { limit: (left + right) / 2, method: 'two-sided approach', exists: true };
776
+ }
777
+
778
+ // L'Hôpital: only applicable for f/g form
779
+ if (expr.type === 'div') {
780
+ let top = expr.top, bot = expr.bot;
781
+ for (let i = 0; i < maxHopital; i++) {
782
+ const tVal = symEval(symSimplify(top), { [variable]: isFinite(value) ? value : 1e15 });
783
+ const bVal = symEval(symSimplify(bot), { [variable]: isFinite(value) ? value : 1e15 });
784
+ const indeterminate = (Math.abs(tVal) < 1e-6 && Math.abs(bVal) < 1e-6)
785
+ || (!isFinite(tVal) && !isFinite(bVal));
786
+ if (!indeterminate) break;
787
+ top = symSimplify(symDiff(top, variable));
788
+ bot = symSimplify(symDiff(bot, variable));
789
+ const candidate = symSimplify(symDiv(top, bot));
790
+ const result = symEval(candidate, { [variable]: isFinite(value) ? value : 1e15 });
791
+ if (isFinite(result)) {
792
+ return { limit: result, method: `L'Hôpital (${i + 1} application${i > 0 ? 's' : ''})`, exists: true };
793
+ }
794
+ }
795
+ }
796
+
797
+ if (!isFinite(left) && !isFinite(right)) {
798
+ const sign = left > 0 ? +Infinity : -Infinity;
799
+ return { limit: sign, method: 'diverges', exists: false, leftLimit: left, rightLimit: right };
800
+ }
801
+
802
+ return { limit: null, method: 'left and right limits differ', exists: false, leftLimit: left, rightLimit: right };
803
+ }
804
+
805
+ // ============================================================================
806
+ // PARTIAL DIFFERENTIATION (multivariable symbolic)
807
+ // ============================================================================
808
+
809
+ /**
810
+ * Compute all first-order partial derivatives of expr wrt each variable.
811
+ * @param {Object} expr SymExpr (may contain multiple variables)
812
+ * @param {string[]} variables e.g. ['x', 'y', 'z']
813
+ * @returns {Object} { x: SymExpr, y: SymExpr, ... }
814
+ */
815
+ export function symGradient(expr, variables) {
816
+ const result = {};
817
+ for (const v of variables) {
818
+ result[v] = symSimplify(symDiff(expr, v));
819
+ }
820
+ return result;
821
+ }
822
+
823
+ /**
824
+ * Compute the Hessian matrix of second-order partials.
825
+ * @param {Object} expr
826
+ * @param {string[]} variables
827
+ * @returns {Object[][]} 2-D array of SymExprs
828
+ */
829
+ export function symHessian(expr, variables) {
830
+ const n = variables.length;
831
+ const H = Array.from({ length: n }, () => Array(n).fill(null));
832
+ for (let i = 0; i < n; i++) {
833
+ const fi = symSimplify(symDiff(expr, variables[i]));
834
+ for (let j = 0; j < n; j++) {
835
+ H[i][j] = symSimplify(symDiff(fi, variables[j]));
836
+ }
837
+ }
838
+ return H;
839
+ }
840
+
841
+ /**
842
+ * Evaluate symbolic gradient at a numeric point.
843
+ * @param {Object} expr
844
+ * @param {string[]} variables
845
+ * @param {Object} point e.g. { x: 1, y: 2 }
846
+ * @returns {number[]} gradient vector
847
+ */
848
+ export function evalGradient(expr, variables, point) {
849
+ return variables.map(v => symEval(symSimplify(symDiff(expr, v)), point));
850
+ }
851
+
852
+ /**
853
+ * Evaluate symbolic Hessian at a numeric point.
854
+ * @returns {number[][]}
855
+ */
856
+ export function evalHessian(expr, variables, point) {
857
+ return symHessian(expr, variables).map(row =>
858
+ row.map(e => symEval(e, point))
859
+ );
860
+ }
861
+
862
+ // ============================================================================
863
+ // CRITICAL POINTS (single variable, symbolic)
864
+ // ============================================================================
865
+
866
+ /**
867
+ * Find critical points of expr where d/dx = 0 via numerical root finding.
868
+ * @param {Object} expr SymExpr
869
+ * @param {string} variable
870
+ * @param {number[]} [range=[-10,10]]
871
+ * @param {number} [samples=2000]
872
+ * @returns {{ points: number[], classifications: Object[] }}
873
+ */
874
+ export function symFindCriticalPoints(expr, variable, range = [-10, 10], samples = 2000) {
875
+ const deriv = symSimplify(symDiff(expr, variable));
876
+ const deriv2 = symSimplify(symDiff(deriv, variable));
877
+ const [lo, hi] = range;
878
+ const step = (hi - lo) / samples;
879
+ const roots = [];
880
+
881
+ let prev = symEval(deriv, { [variable]: lo });
882
+ for (let i = 1; i <= samples; i++) {
883
+ const x = lo + i * step;
884
+ let curr;
885
+ try { curr = symEval(deriv, { [variable]: x }); } catch { prev = NaN; continue; }
886
+ if (isFinite(prev) && isFinite(curr) && prev * curr < 0) {
887
+ // Bisect
888
+ let a = x - step, b = x;
889
+ for (let k = 0; k < 52; k++) {
890
+ const m = (a + b) / 2;
891
+ const vm = symEval(deriv, { [variable]: m });
892
+ if (Math.abs(vm) < 1e-12) { a = m; break; }
893
+ (symEval(deriv, { [variable]: a }) * vm < 0) ? (b = m) : (a = m);
894
+ }
895
+ const root = (a + b) / 2;
896
+ if (!roots.some(r => Math.abs(r - root) < 1e-6)) roots.push(root);
897
+ }
898
+ prev = curr;
899
+ }
900
+
901
+ const classifications = roots.map(pt => {
902
+ const fVal = symEval(expr, { [variable]: pt });
903
+ const d2Val = symEval(deriv2, { [variable]: pt });
904
+ let type;
905
+ if (d2Val > 1e-10) type = 'local minimum';
906
+ else if (d2Val < -1e-10) type = 'local maximum';
907
+ else type = 'inflection or higher-order';
908
+ return { x: pt, f: fVal, d2: d2Val, type };
909
+ });
910
+
911
+ return { points: roots, classifications };
912
+ }
913
+
914
+ // ============================================================================
915
+ // IMPLICIT DIFFERENTIATION
916
+ // ============================================================================
917
+
918
+ /**
919
+ * Compute dy/dx for an implicit equation F(x, y) = 0.
920
+ * Result: dy/dx = -∂F/∂x / ∂F/∂y
921
+ *
922
+ * @param {Object} F SymExpr representing F(x,y)
923
+ * @param {string} [xVar='x']
924
+ * @param {string} [yVar='y']
925
+ * @returns {Object} SymExpr for dy/dx
926
+ */
927
+ export function implicitDiff(F, xVar = 'x', yVar = 'y') {
928
+ const Fx = symSimplify(symDiff(F, xVar));
929
+ const Fy = symSimplify(symDiff(F, yVar));
930
+ return symSimplify(symDiv(symNeg(Fx), Fy));
931
+ }
932
+
933
+ // ============================================================================
934
+ // CURVE ANALYSIS (inflection points, concavity, asymptotes)
935
+ // ============================================================================
936
+
937
+ /**
938
+ * Full curve analysis on a single-variable SymExpr.
939
+ * @param {Object} expr SymExpr in terms of `variable`
940
+ * @param {string} variable
941
+ * @param {number[]} [range=[-10,10]]
942
+ * @returns {{
943
+ * criticalPoints, derivative, secondDerivative,
944
+ * inflectionPoints, intervals, asymptotes
945
+ * }}
946
+ */
947
+ export function analyzeCurve(expr, variable, range = [-10, 10]) {
948
+ const d1 = symSimplify(symDiff(expr, variable));
949
+ const d2 = symSimplify(symDiff(d1, variable));
950
+
951
+ const { classifications } = symFindCriticalPoints(expr, variable, range);
952
+ const inflectionData = symFindCriticalPoints(d1, variable, range);
953
+
954
+ // Monotonicity intervals
955
+ const testPoints = [];
956
+ const allX = [-999, ...classifications.map(c => c.x), ...inflectionData.points, 999].sort((a, b) => a - b);
957
+ for (let i = 0; i < allX.length - 1; i++) {
958
+ const mid = (allX[i] + allX[i + 1]) / 2;
959
+ let sign;
960
+ try { sign = symEval(d1, { [variable]: mid }); } catch { continue; }
961
+ testPoints.push({ from: allX[i], to: allX[i + 1], increasing: sign > 0 });
962
+ }
963
+
964
+ // Vertical asymptotes: sample many points for near-blow-up
965
+ const asymptotes = [];
966
+ const [lo, hi] = range;
967
+ const step = (hi - lo) / 5000;
968
+ let prevV = null;
969
+ for (let i = 0; i <= 5000; i++) {
970
+ const x = lo + i * step;
971
+ let v; try { v = symEval(expr, { [variable]: x }); } catch { v = Infinity; }
972
+ if (prevV !== null && isFinite(prevV) && !isFinite(v)) {
973
+ asymptotes.push({ type: 'vertical', x });
974
+ }
975
+ prevV = v;
976
+ }
977
+
978
+ return {
979
+ derivative: d1, secondDerivative: d2,
980
+ criticalPoints: classifications,
981
+ inflectionPoints: inflectionData.classifications.map(c => ({ x: c.x, f: symEval(expr, { [variable]: c.x }) })),
982
+ intervals: testPoints,
983
+ asymptotes,
984
+ };
985
+ }
986
+
987
+
988
+ // ============================================================================
989
+ // INTEGRATION BY PARTS (LIATE heuristic)
990
+ // ============================================================================
991
+
992
+ /**
993
+ * Attempt integration by parts: ∫u·dv = u·v − ∫v·du
994
+ * Uses LIATE ordering to choose u automatically.
995
+ * Iterates up to maxDepth times to handle repeated integration by parts.
996
+ *
997
+ * Returns { result: SymExpr, steps: string[] } or null if unable.
998
+ *
999
+ * @param {Object} expr product SymExpr (type === 'mul')
1000
+ * @param {string} variable
1001
+ * @param {number} [maxDepth=3]
1002
+ */
1003
+ export function integrationByParts(expr, variable, maxDepth = 3) {
1004
+ const steps = [];
1005
+ return _ibpRecurse(expr, variable, maxDepth, steps)
1006
+ ? { result: symSimplify(_ibpRecurse(expr, variable, maxDepth, steps)), steps }
1007
+ : null;
1008
+ }
1009
+
1010
+ function _liateRank(e) {
1011
+ if (e.type === 'fn' && ['ln','log10','asin','acos','atan'].includes(e.name)) return 0; // L/I
1012
+ if (e.type === 'var') return 2; // A (algebraic)
1013
+ if (e.type === 'pow' && e.base.type === 'var') return 2;
1014
+ if (e.type === 'fn' && ['sin','cos','tan'].includes(e.name)) return 3; // T
1015
+ if (e.type === 'fn' && e.name === 'exp') return 4; // E
1016
+ if (e.type === 'const') return 5;
1017
+ return 1;
1018
+ }
1019
+
1020
+ function _ibpRecurse(expr, variable, depth, steps) {
1021
+ if (depth === 0) return null;
1022
+ // We need a product. If not, wrap as 1·expr.
1023
+ let left = expr, right = { type: 'const', value: 1 };
1024
+ if (expr.type === 'mul') { left = expr.left; right = expr.right; }
1025
+
1026
+ // Choose u and dv by LIATE
1027
+ const [u, dv] = _liateRank(left) <= _liateRank(right)
1028
+ ? [left, right]
1029
+ : [right, left];
1030
+
1031
+ // v = ∫dv dx
1032
+ const v = symIntegrate(dv, variable);
1033
+ if (!v) return null;
1034
+
1035
+ // du = d/dx (u) dx
1036
+ const du = symSimplify(symDiff(u, variable));
1037
+
1038
+ // ∫u·dv = u·v − ∫v·du
1039
+ const vDu = symSimplify(symMul(v, du));
1040
+ const vDuInt = symIntegrate(vDu, variable);
1041
+
1042
+ steps.push(`u = ${symToLatex(u)}, dv = ${symToLatex(dv)}`);
1043
+ steps.push(`v = ${symToLatex(v)}, du = ${symToLatex(du)}`);
1044
+
1045
+ if (vDuInt) {
1046
+ steps.push(`∫v·du = ${symToLatex(vDuInt)}`);
1047
+ return symSub(symMul(u, v), vDuInt);
1048
+ }
1049
+
1050
+ // Try recursively if ∫v·du is itself a product
1051
+ if (vDu.type === 'mul') {
1052
+ const inner = _ibpRecurse(vDu, variable, depth - 1, steps);
1053
+ if (inner) return symSub(symMul(u, v), inner);
1054
+ }
1055
+
1056
+ return null;
1057
+ }
1058
+
1059
+ // ============================================================================
1060
+ // PARTIAL FRACTION DECOMPOSITION (symbolic)
1061
+ // ============================================================================
1062
+
1063
+ /**
1064
+ * Attempt partial fraction decomposition of a rational expression (top/bot).
1065
+ * Handles linear factors in the denominator up to degree 4.
1066
+ *
1067
+ * Algorithm:
1068
+ * 1. Find rational roots of denominator by rational root theorem + bisection
1069
+ * 2. For each root r: extract factor (x − r), compute residue A/(x−r)
1070
+ * 3. Return sum of partial fractions + irreducible remainder
1071
+ *
1072
+ * @param {Object} top SymExpr numerator
1073
+ * @param {Object} bot SymExpr denominator polynomial in `variable`
1074
+ * @param {string} variable
1075
+ * @returns {{ terms: Array<{coeff:number, root:number}>, remainder: Object|null, latex: string }}
1076
+ */
1077
+ export function partialFractions(top, bot, variable) {
1078
+ const x = variable;
1079
+ const evalAt = v => symEval(symSimplify(bot), { [x]: v });
1080
+ const evalTop = v => symEval(symSimplify(top), { [x]: v });
1081
+
1082
+ // Find roots of denominator numerically
1083
+ const roots = [];
1084
+ const range = 20, steps = 4000;
1085
+ const h = (2 * range) / steps;
1086
+ let prev = evalAt(-range);
1087
+ for (let i = 1; i <= steps; i++) {
1088
+ const xi = -range + i * h;
1089
+ const curr = evalAt(xi);
1090
+ if (isFinite(prev) && isFinite(curr) && prev * curr < 0) {
1091
+ let a = xi - h, b = xi;
1092
+ for (let k = 0; k < 60; k++) {
1093
+ const m = (a + b) / 2;
1094
+ const fm = evalAt(m);
1095
+ if (Math.abs(fm) < 1e-12) { a = m; break; }
1096
+ evalAt(a) * fm < 0 ? (b = m) : (a = m);
1097
+ }
1098
+ const root = (a + b) / 2;
1099
+ if (!roots.some(r => Math.abs(r - root) < 1e-6)) roots.push(root);
1100
+ }
1101
+ prev = curr;
1102
+ }
1103
+
1104
+ // Residue for each simple root: A_i = top(r_i) / bot'(r_i)
1105
+ const botDeriv = symSimplify(symDiff(bot, x));
1106
+ const terms = roots.map(r => {
1107
+ const botD = symEval(botDeriv, { [x]: r });
1108
+ const A = Math.abs(botD) > 1e-14 ? evalTop(r) / botD : 0;
1109
+ return { coeff: A, root: r };
1110
+ });
1111
+
1112
+ // Build LaTeX
1113
+ const latexParts = terms.map(({ coeff, root }) => {
1114
+ const c = _roundLatex(coeff);
1115
+ const r = _roundLatex(root);
1116
+ const sign = root >= 0 ? `-${r}` : `+${Math.abs(root).toFixed(4)}`;
1117
+ return `\\frac{${c}}{${x} ${sign}}`;
1118
+ });
1119
+
1120
+ return {
1121
+ terms,
1122
+ latex: latexParts.join(' + ').replace(/\+ -/g, '- ') || '0',
1123
+ roots,
1124
+ };
1125
+ }
1126
+
1127
+ function _roundLatex(v) {
1128
+ if (Math.abs(v - Math.round(v)) < 1e-8) return String(Math.round(v));
1129
+ return v.toFixed(4);
1130
+ }
1131
+
1132
+ // ============================================================================
1133
+ // FOURIER SERIES (symbolic coefficients)
1134
+ // ============================================================================
1135
+
1136
+ /**
1137
+ * Compute Fourier series coefficients of a SymExpr on [−L, L].
1138
+ * a₀ = (1/2L) ∫_{−L}^{L} f(x) dx
1139
+ * aₙ = (1/L) ∫_{−L}^{L} f(x) cos(nπx/L) dx
1140
+ * bₙ = (1/L) ∫_{−L}^{L} f(x) sin(nπx/L) dx
1141
+ *
1142
+ * Uses numerical integration (symbolic would require product integrals).
1143
+ *
1144
+ * @param {Object} expr
1145
+ * @param {string} variable
1146
+ * @param {number} L half-period
1147
+ * @param {number} N number of harmonics
1148
+ * @returns {{ a0, an: number[], bn: number[], evaluate: (x) => number, latex: string }}
1149
+ */
1150
+ export function fourierSeries(expr, variable, L = Math.PI, N = 10) {
1151
+ const _int = (f, a, b, n = 2000) => {
1152
+ const h = (b - a) / n;
1153
+ let s = f(a) + f(b);
1154
+ for (let i = 1; i < n; i++) s += (i % 2 === 0 ? 2 : 4) * f(a + i * h);
1155
+ return (h / 3) * s;
1156
+ };
1157
+ const f = x => symEval(expr, { [variable]: x });
1158
+
1159
+ const a0 = (1 / (2 * L)) * _int(f, -L, L);
1160
+ const an = [], bn = [];
1161
+ for (let n = 1; n <= N; n++) {
1162
+ an.push((1 / L) * _int(x => f(x) * Math.cos(n * Math.PI * x / L), -L, L));
1163
+ bn.push((1 / L) * _int(x => f(x) * Math.sin(n * Math.PI * x / L), -L, L));
1164
+ }
1165
+
1166
+ const evaluate = x => {
1167
+ let s = a0;
1168
+ for (let n = 0; n < N; n++) {
1169
+ s += an[n] * Math.cos((n + 1) * Math.PI * x / L)
1170
+ + bn[n] * Math.sin((n + 1) * Math.PI * x / L);
1171
+ }
1172
+ return s;
1173
+ };
1174
+
1175
+ // Build LaTeX string (non-zero terms only)
1176
+ const fmt = v => Math.abs(v) < 1e-10 ? null : _roundLatex(v);
1177
+ const parts = [fmt(a0) ? String(fmt(a0)) : null];
1178
+ for (let n = 1; n <= N; n++) {
1179
+ const a = fmt(an[n - 1]), b = fmt(bn[n - 1]);
1180
+ const arg = L === Math.PI ? `${n}${variable}` : `\\frac{${n}\\pi ${variable}}{${_roundLatex(L)}}`;
1181
+ if (a) parts.push(`${a}\\cos\\left(${arg}\\right)`);
1182
+ if (b) parts.push(`${b}\\sin\\left(${arg}\\right)`);
1183
+ }
1184
+ const latex = parts.filter(Boolean).join(' + ').replace(/\+ -/g, '- ') || '0';
1185
+
1186
+ return { a0, an, bn, evaluate, latex, N, L };
1187
+ }
1188
+
1189
+ // ============================================================================
1190
+ // LAPLACE TRANSFORM (table-based, symbolic)
1191
+ // ============================================================================
1192
+
1193
+ /**
1194
+ * Compute the Laplace transform of a SymExpr using a built-in table.
1195
+ * L{f(t)} = F(s)
1196
+ *
1197
+ * Supported forms: constants, powers of t, exp, sin, cos, sinh, cosh,
1198
+ * their products with exp (first shifting theorem), and sums thereof.
1199
+ *
1200
+ * @param {Object} expr SymExpr in terms of `timeVar`
1201
+ * @param {string} [timeVar='t']
1202
+ * @param {string} [freqVar='s']
1203
+ * @returns {{ expr: Object|null, latex: string|null, method: string }}
1204
+ */
1205
+ export function laplaceTransform(expr, timeVar = 't', freqVar = 's') {
1206
+ const s = symVar(freqVar);
1207
+ const result = _laplaceRule(expr, timeVar, s);
1208
+ if (!result) return { expr: null, latex: null, method: 'not found in table' };
1209
+ const simplified = symSimplify(result);
1210
+ return { expr: simplified, latex: symToLatex(simplified), method: 'table lookup' };
1211
+ }
1212
+
1213
+ function _laplaceRule(e, t, s) {
1214
+ // L{c} = c/s
1215
+ if (e.type === 'const') return symDiv(e, s);
1216
+
1217
+ // L{t^n} = n!/s^(n+1)
1218
+ if (e.type === 'var' && e.name === t) return symDiv(symConst(1), symPow(s, symConst(2)));
1219
+ if (e.type === 'pow' && e.base.type === 'var' && e.base.name === t && e.exp.type === 'const') {
1220
+ const n = e.exp.value;
1221
+ if (n >= 0 && Number.isInteger(n)) {
1222
+ const fac = Array.from({ length: n }, (_, i) => i + 1).reduce((a, b) => a * b, 1);
1223
+ return symDiv(symConst(fac), symPow(s, symConst(n + 1)));
1224
+ }
1225
+ }
1226
+
1227
+ // L{e^(at)} = 1/(s−a)
1228
+ if (e.type === 'fn' && e.name === 'exp') {
1229
+ const arg = e.arg;
1230
+ if (arg.type === 'mul' && arg.left.type === 'const' && arg.right.type === 'var' && arg.right.name === t) {
1231
+ const a = symConst(arg.left.value);
1232
+ return symDiv(symConst(1), symSub(s, a));
1233
+ }
1234
+ if (arg.type === 'var' && arg.name === t)
1235
+ return symDiv(symConst(1), symSub(s, symConst(1)));
1236
+ // e^(at) with a negative
1237
+ if (arg.type === 'neg' && arg.arg.type === 'var' && arg.arg.name === t)
1238
+ return symDiv(symConst(1), symAdd(s, symConst(1)));
1239
+ }
1240
+
1241
+ // L{sin(ωt)} = ω/(s²+ω²)
1242
+ if (e.type === 'fn' && e.name === 'sin') {
1243
+ const omega = _extractLinearCoeff(e.arg, t);
1244
+ if (omega !== null) {
1245
+ const w2 = symConst(omega * omega);
1246
+ return symDiv(symConst(omega), symAdd(symPow(s, symConst(2)), w2));
1247
+ }
1248
+ }
1249
+
1250
+ // L{cos(ωt)} = s/(s²+ω²)
1251
+ if (e.type === 'fn' && e.name === 'cos') {
1252
+ const omega = _extractLinearCoeff(e.arg, t);
1253
+ if (omega !== null) {
1254
+ const w2 = symConst(omega * omega);
1255
+ return symDiv(s, symAdd(symPow(s, symConst(2)), w2));
1256
+ }
1257
+ }
1258
+
1259
+ // L{sinh(ωt)} = ω/(s²−ω²)
1260
+ if (e.type === 'fn' && e.name === 'sinh') {
1261
+ const omega = _extractLinearCoeff(e.arg, t);
1262
+ if (omega !== null) {
1263
+ const w2 = symConst(omega * omega);
1264
+ return symDiv(symConst(omega), symSub(symPow(s, symConst(2)), w2));
1265
+ }
1266
+ }
1267
+
1268
+ // L{cosh(ωt)} = s/(s²−ω²)
1269
+ if (e.type === 'fn' && e.name === 'cosh') {
1270
+ const omega = _extractLinearCoeff(e.arg, t);
1271
+ if (omega !== null) {
1272
+ const w2 = symConst(omega * omega);
1273
+ return symDiv(s, symSub(symPow(s, symConst(2)), w2));
1274
+ }
1275
+ }
1276
+
1277
+ // First shifting theorem: L{e^(at)·f(t)} = F(s−a)
1278
+ if (e.type === 'mul') {
1279
+ const [left, right] = [e.left, e.right];
1280
+ // Identify the exp factor
1281
+ const tryShift = (expE, fE) => {
1282
+ if (expE.type !== 'fn' || expE.name !== 'exp') return null;
1283
+ const a = _extractLinearCoeff(expE.arg, t);
1284
+ if (a === null) return null;
1285
+ const sShifted = symSub(s, symConst(a));
1286
+ const Fshifted = _laplaceRule(fE, t, sShifted);
1287
+ return Fshifted;
1288
+ };
1289
+ return tryShift(left, right) || tryShift(right, left) || null;
1290
+ }
1291
+
1292
+ // Linearity: L{f+g} = L{f}+L{g} and L{c·f} = c·L{f}
1293
+ if (e.type === 'add') {
1294
+ const lL = _laplaceRule(e.left, t, s);
1295
+ const rL = _laplaceRule(e.right, t, s);
1296
+ if (lL && rL) return symAdd(lL, rL);
1297
+ }
1298
+ if (e.type === 'neg') {
1299
+ const inner = _laplaceRule(e.arg, t, s);
1300
+ return inner ? symNeg(inner) : null;
1301
+ }
1302
+
1303
+ return null;
1304
+ }
1305
+
1306
+ function _extractLinearCoeff(arg, t) {
1307
+ if (arg.type === 'var' && arg.name === t) return 1;
1308
+ if (arg.type === 'mul' && arg.left.type === 'const' && arg.right.type === 'var' && arg.right.name === t)
1309
+ return arg.left.value;
1310
+ if (arg.type === 'neg' && arg.arg.type === 'var' && arg.arg.name === t) return -1;
1311
+ return null;
1312
+ }
1313
+
1314
+ // ============================================================================
1315
+ // Z-TRANSFORM (table-based)
1316
+ // ============================================================================
1317
+
1318
+ /**
1319
+ * One-sided Z-transform of common discrete-time sequences.
1320
+ * Z{x[n]} = X(z)
1321
+ *
1322
+ * Supported: unit impulse, unit step, ramp, a^n, sin/cos sequences.
1323
+ *
1324
+ * @param {string} seqName one of: 'impulse','step','ramp','exponential','sin','cos'
1325
+ * @param {Object} [params] e.g. { a: 0.5 } for 'exponential', { omega: 1 } for 'sin'/'cos'
1326
+ * @param {string} [freqVar='z']
1327
+ * @returns {{ expr: Object, latex: string }}
1328
+ */
1329
+ export function zTransform(seqName, params = {}, freqVar = 'z') {
1330
+ const z = symVar(freqVar);
1331
+ let expr;
1332
+ switch (seqName) {
1333
+ case 'impulse': // δ[n] → 1
1334
+ expr = symConst(1); break;
1335
+ case 'step': // u[n] → z/(z−1)
1336
+ expr = symDiv(z, symSub(z, symConst(1))); break;
1337
+ case 'ramp': // n·u[n] → z/(z−1)²
1338
+ expr = symDiv(z, symPow(symSub(z, symConst(1)), symConst(2))); break;
1339
+ case 'exponential': { // a^n·u[n] → z/(z−a)
1340
+ const a = symConst(params.a ?? 1);
1341
+ expr = symDiv(z, symSub(z, a)); break;
1342
+ }
1343
+ case 'sin': { // sin(ωn)·u[n] → z·sin(ω)/(z²−2z·cos(ω)+1)
1344
+ const w = params.omega ?? 1;
1345
+ const sw = symConst(Math.sin(w)), cw = symConst(Math.cos(w));
1346
+ expr = symDiv(
1347
+ symMul(z, sw),
1348
+ symAdd(symSub(symPow(z, symConst(2)), symMul(symConst(2), symMul(z, cw))), symConst(1))
1349
+ ); break;
1350
+ }
1351
+ case 'cos': { // cos(ωn)·u[n] → z(z−cos(ω))/(z²−2z·cos(ω)+1)
1352
+ const w = params.omega ?? 1;
1353
+ const cw = symConst(Math.cos(w));
1354
+ expr = symDiv(
1355
+ symMul(z, symSub(z, cw)),
1356
+ symAdd(symSub(symPow(z, symConst(2)), symMul(symConst(2), symMul(z, cw))), symConst(1))
1357
+ ); break;
1358
+ }
1359
+ default:
1360
+ return { expr: null, latex: null };
1361
+ }
1362
+ const simplified = symSimplify(expr);
1363
+ return { expr: simplified, latex: symToLatex(simplified) };
1364
+ }
1365
+
1366
+ // ============================================================================
1367
+ // SYMBOLIC EXPRESSION PARSING (from string)
1368
+ // ============================================================================
1369
+
1370
+ /**
1371
+ * Parse a simple math expression string into a SymExpr tree.
1372
+ * Supports: numbers, variables (single letters), +−*/^, parentheses,
1373
+ * and function names: sin, cos, tan, asin, acos, atan, sinh, cosh, tanh,
1374
+ * exp, ln, log, sqrt, abs.
1375
+ *
1376
+ * @param {string} src
1377
+ * @returns {Object} SymExpr
1378
+ */
1379
+ export function parseExpr(src) {
1380
+ const tokens = _tokenize(src.replace(/\s+/g, ''));
1381
+ let pos = 0;
1382
+
1383
+ function peek() { return tokens[pos]; }
1384
+ function consume(expected) {
1385
+ const t = tokens[pos++];
1386
+ if (expected && t !== expected) throw new Error(`parseExpr: expected "${expected}" got "${t}"`);
1387
+ return t;
1388
+ }
1389
+
1390
+ function parseAddSub() {
1391
+ let left = parseMulDiv();
1392
+ while (peek() === '+' || peek() === '-') {
1393
+ const op = consume();
1394
+ const right = parseMulDiv();
1395
+ left = op === '+' ? symAdd(left, right) : symSub(left, right);
1396
+ }
1397
+ return left;
1398
+ }
1399
+
1400
+ function parseMulDiv() {
1401
+ let left = parsePow();
1402
+ while (peek() === '*' || peek() === '/') {
1403
+ const op = consume();
1404
+ const right = parsePow();
1405
+ left = op === '*' ? symMul(left, right) : symDiv(left, right);
1406
+ }
1407
+ return left;
1408
+ }
1409
+
1410
+ function parsePow() {
1411
+ let base = parseUnary();
1412
+ if (peek() === '^') { consume(); base = symPow(base, parsePow()); }
1413
+ return base;
1414
+ }
1415
+
1416
+ function parseUnary() {
1417
+ if (peek() === '-') { consume(); return symNeg(parsePrimary()); }
1418
+ if (peek() === '+') { consume(); }
1419
+ return parsePrimary();
1420
+ }
1421
+
1422
+ function parsePrimary() {
1423
+ const t = peek();
1424
+ if (t === '(') {
1425
+ consume('(');
1426
+ const inner = parseAddSub();
1427
+ consume(')');
1428
+ return inner;
1429
+ }
1430
+ if (/^[0-9]/.test(t) || t === '.') {
1431
+ consume();
1432
+ return symConst(parseFloat(t));
1433
+ }
1434
+ // Function or variable
1435
+ if (/^[a-zA-Z]/.test(t)) {
1436
+ consume();
1437
+ const fnNames = ['sin','cos','tan','asin','acos','atan','sinh','cosh','tanh','exp','ln','log','sqrt','abs','log10'];
1438
+ if (fnNames.includes(t) && peek() === '(') {
1439
+ consume('(');
1440
+ const arg = parseAddSub();
1441
+ consume(')');
1442
+ return symFn(t === 'log' ? 'ln' : t, arg);
1443
+ }
1444
+ return symVar(t);
1445
+ }
1446
+ throw new Error(`parseExpr: unexpected token "${t}"`);
1447
+ }
1448
+
1449
+ const result = parseAddSub();
1450
+ if (pos < tokens.length) throw new Error(`parseExpr: unexpected token "${tokens[pos]}" at position ${pos}`);
1451
+ return result;
1452
+ }
1453
+
1454
+ function _tokenize(s) {
1455
+ const tokens = [];
1456
+ let i = 0;
1457
+ while (i < s.length) {
1458
+ if ('+-*/^()'.includes(s[i])) { tokens.push(s[i++]); continue; }
1459
+ if (/[0-9.]/.test(s[i])) {
1460
+ let n = '';
1461
+ while (i < s.length && /[0-9.]/.test(s[i])) n += s[i++];
1462
+ tokens.push(n); continue;
1463
+ }
1464
+ if (/[a-zA-Z]/.test(s[i])) {
1465
+ let w = '';
1466
+ while (i < s.length && /[a-zA-Z0-9_]/.test(s[i])) w += s[i++];
1467
+ tokens.push(w); continue;
1468
+ }
1469
+ i++;
1470
+ }
1471
+ return tokens;
1472
+ }
1473
+
1474
+ // ============================================================================
1475
+ // ARC LENGTH & SURFACE AREA (symbolic)
1476
+ // ============================================================================
1477
+
1478
+ /**
1479
+ * Arc length of y = f(x) from a to b: L = ∫√(1 + (f'(x))²) dx
1480
+ * Uses high-precision Gauss-Legendre quadrature (32 points).
1481
+ */
1482
+ export function arcLength(expr, variable, a, b) {
1483
+ const deriv = symSimplify(symDiff(expr, variable));
1484
+ const integrand = x => {
1485
+ const dy = symEval(deriv, { [variable]: x });
1486
+ return Math.sqrt(1 + dy * dy);
1487
+ };
1488
+ return _gaussLegendre(integrand, a, b, 32);
1489
+ }
1490
+
1491
+ /**
1492
+ * Surface area of revolution (about x-axis):
1493
+ * S = 2π ∫_a^b f(x) √(1 + (f'(x))²) dx
1494
+ */
1495
+ export function surfaceAreaOfRevolution(expr, variable, a, b) {
1496
+ const deriv = symSimplify(symDiff(expr, variable));
1497
+ const integrand = x => {
1498
+ const y = symEval(expr, { [variable]: x });
1499
+ const dy = symEval(deriv, { [variable]: x });
1500
+ return 2 * Math.PI * Math.abs(y) * Math.sqrt(1 + dy * dy);
1501
+ };
1502
+ return _gaussLegendre(integrand, a, b, 32);
1503
+ }
1504
+
1505
+ /**
1506
+ * Volume of solid of revolution (about x-axis) — Disk method:
1507
+ * V = π ∫_a^b (f(x))² dx
1508
+ */
1509
+ export function volumeOfRevolution(expr, variable, a, b) {
1510
+ const integrand = x => {
1511
+ const y = symEval(expr, { [variable]: x });
1512
+ return Math.PI * y * y;
1513
+ };
1514
+ return _gaussLegendre(integrand, a, b, 32);
1515
+ }
1516
+
1517
+ // 32-point Gauss-Legendre nodes & weights (precomputed)
1518
+ const _GL_NODES = [
1519
+ -0.9972638618494816,-0.9856115115452684,-0.9647622555875064,-0.9349060759377397,
1520
+ -0.8963211557660521,-0.8493676137325700,-0.7944837959679424,-0.7321821187402897,
1521
+ -0.6630442669302152,-0.5877157572407623,-0.5068999089322294,-0.4213512761306353,
1522
+ -0.3318686022821276,-0.2392873622521371,-0.1444719615827965,-0.0483076656877383,
1523
+ 0.0483076656877383, 0.1444719615827965, 0.2392873622521371, 0.3318686022821276,
1524
+ 0.4213512761306353, 0.5068999089322294, 0.5877157572407623, 0.6630442669302152,
1525
+ 0.7321821187402897, 0.7944837959679424, 0.8493676137325700, 0.8963211557660521,
1526
+ 0.9349060759377397, 0.9647622555875064, 0.9856115115452684, 0.9972638618494816
1527
+ ];
1528
+ const _GL_WEIGHTS = [
1529
+ 0.0070186100094491,0.0162743947309057,0.0253920653092621,0.0342738629130214,
1530
+ 0.0428358980222267,0.0509980592623762,0.0586840934785355,0.0658222227763618,
1531
+ 0.0723457941088485,0.0781938957870703,0.0833119242269467,0.0876520930044038,
1532
+ 0.0911738786957639,0.0938443990808046,0.0956387200792749,0.0965400885147278,
1533
+ 0.0965400885147278,0.0956387200792749,0.0938443990808046,0.0911738786957639,
1534
+ 0.0876520930044038,0.0833119242269467,0.0781938957870703,0.0723457941088485,
1535
+ 0.0658222227763618,0.0586840934785355,0.0509980592623762,0.0428358980222267,
1536
+ 0.0342738629130214,0.0253920653092621,0.0162743947309057,0.0070186100094491
1537
+ ];
1538
+
1539
+ function _gaussLegendre(f, a, b, n = 32) {
1540
+ const mid = (b + a) / 2, half = (b - a) / 2;
1541
+ let sum = 0;
1542
+ for (let i = 0; i < n; i++) {
1543
+ sum += _GL_WEIGHTS[i] * f(mid + half * _GL_NODES[i]);
1544
+ }
1545
+ return half * sum;
1546
+ }
1547
+
1548
+ // ============================================================================
1549
+ // DOUBLE & TRIPLE INTEGRALS (numerical)
1550
+ // ============================================================================
1551
+
1552
+ /**
1553
+ * Double integral ∫∫_R f(x,y) dA over a rectangular region [ax,bx] × [ay,by].
1554
+ * Uses 2D Gauss-Legendre quadrature.
1555
+ *
1556
+ * @param {Object} expr SymExpr in x, y
1557
+ * @param {string} xVar
1558
+ * @param {string} yVar
1559
+ * @param {number} ax lower x bound
1560
+ * @param {number} bx upper x bound
1561
+ * @param {number} ay lower y bound (can be a function of x: ay(x))
1562
+ * @param {number} by upper y bound (can be a function of x: by(x))
1563
+ * @param {number} [n=16] quadrature points per dimension
1564
+ */
1565
+ export function doubleIntegral(expr, xVar, yVar, ax, bx, ay, by, n = 16) {
1566
+ const ayFn = typeof ay === 'function' ? ay : () => ay;
1567
+ const byFn = typeof by === 'function' ? by : () => by;
1568
+ const nodes = _GL_NODES.slice(0, n), weights = _GL_WEIGHTS.slice(0, n);
1569
+ const midX = (bx + ax) / 2, halfX = (bx - ax) / 2;
1570
+ let total = 0;
1571
+ for (let i = 0; i < n; i++) {
1572
+ const x = midX + halfX * nodes[i];
1573
+ const yLo = ayFn(x), yHi = byFn(x);
1574
+ const midY = (yHi + yLo) / 2, halfY = (yHi - yLo) / 2;
1575
+ let inner = 0;
1576
+ for (let j = 0; j < n; j++) {
1577
+ const y = midY + halfY * nodes[j];
1578
+ inner += weights[j] * symEval(expr, { [xVar]: x, [yVar]: y });
1579
+ }
1580
+ total += weights[i] * halfY * inner;
1581
+ }
1582
+ return halfX * total;
1583
+ }
1584
+
1585
+ /**
1586
+ * Triple integral ∫∫∫ f(x,y,z) dV over a box [ax,bx]×[ay,by]×[az,bz].
1587
+ * Bounds ay/by/az/bz may be functions of outer variables.
1588
+ */
1589
+ export function tripleIntegral(expr, xVar, yVar, zVar, ax, bx, ay, by, az, bz, n = 8) {
1590
+ const ayFn = typeof ay === 'function' ? ay : () => ay;
1591
+ const byFn = typeof by === 'function' ? by : () => by;
1592
+ const azFn = typeof az === 'function' ? az : () => az;
1593
+ const bzFn = typeof bz === 'function' ? bz : () => bz;
1594
+ const nodes = _GL_NODES.slice(0, n), weights = _GL_WEIGHTS.slice(0, n);
1595
+ const mX = (bx + ax) / 2, hX = (bx - ax) / 2;
1596
+ let total = 0;
1597
+ for (let i = 0; i < n; i++) {
1598
+ const x = mX + hX * nodes[i];
1599
+ const yLo = ayFn(x), yHi = byFn(x);
1600
+ const mY = (yHi + yLo) / 2, hY = (yHi - yLo) / 2;
1601
+ let sumY = 0;
1602
+ for (let j = 0; j < n; j++) {
1603
+ const y = mY + hY * nodes[j];
1604
+ const zLo = azFn(x, y), zHi = bzFn(x, y);
1605
+ const mZ = (zHi + zLo) / 2, hZ = (zHi - zLo) / 2;
1606
+ let sumZ = 0;
1607
+ for (let k = 0; k < n; k++) {
1608
+ const z = mZ + hZ * nodes[k];
1609
+ sumZ += weights[k] * symEval(expr, { [xVar]: x, [yVar]: y, [zVar]: z });
1610
+ }
1611
+ sumY += weights[j] * hZ * sumZ;
1612
+ }
1613
+ total += weights[i] * hY * sumY;
1614
+ }
1615
+ return hX * total;
1616
+ }