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.
- package/package.json +54 -23
- package/slang-complex.js +954 -0
- package/slang-linalg.js +1156 -0
- package/slang-math.js +83 -8
- package/slang-ode.js +1082 -0
- package/slang-stats.js +1206 -0
- package/slang-symbolic.js +1616 -0
|
@@ -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
|
+
}
|