exprify 1.0.1 → 1.0.3
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/README.md +100 -33
- package/dist/exprify.cjs.js +671 -4
- package/dist/exprify.cjs.js.map +1 -1
- package/dist/exprify.esm.js +671 -4
- package/dist/exprify.esm.js.map +1 -1
- package/dist/exprify.js +672 -5
- package/dist/exprify.js.map +1 -1
- package/dist/exprify.min.js +2 -2
- package/dist/exprify.min.js.map +1 -1
- package/package.json +1 -1
- package/src/core/Exprify.js +231 -2
- package/src/function/internal.js +342 -0
- package/src/parser/astBuild.js +24 -1
- package/src/parser/evaluator.js +31 -1
- package/src/utils/matrix.js +53 -0
- package/.gitattributes +0 -2
- package/.github/workflows/ci.yml +0 -40
- package/.github/workflows/npm-publish.yml +0 -38
- package/.github/workflows/security-audit.yml +0 -34
- package/CHANGELOG.md +0 -11
- package/doc/tokenType.txt +0 -48
- package/rollup.config.js +0 -80
- package/test/browser.html +0 -23
- package/test/exprify.test.js +0 -140
package/package.json
CHANGED
package/src/core/Exprify.js
CHANGED
|
@@ -10,6 +10,7 @@ import { globalUnits } from "../utils/globalUnits.js";
|
|
|
10
10
|
import { createVarStore } from "../variables/store.js";
|
|
11
11
|
import { createFunctionRegistry } from "../function/registry.js";
|
|
12
12
|
import { internalFunctions } from "../function/internal.js";
|
|
13
|
+
import { isDenseMatrixWrapper, serializeExprifyValue, wrapDenseMatrix } from "../utils/matrix.js";
|
|
13
14
|
|
|
14
15
|
import { buildAST } from "../parser/astBuild.js";
|
|
15
16
|
|
|
@@ -42,6 +43,18 @@ const formatComplex = (value) => {
|
|
|
42
43
|
return `${real} ${sign} ${imagPart}`;
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
const formatScalar = (value) => {
|
|
47
|
+
if (typeof value !== "number") {
|
|
48
|
+
return String(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (Number.isInteger(value)) {
|
|
52
|
+
return String(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Number(value.toFixed(14)).toString();
|
|
56
|
+
};
|
|
57
|
+
|
|
45
58
|
const formatResult = (value) => {
|
|
46
59
|
if (isComplex(value)) {
|
|
47
60
|
return formatComplex(value);
|
|
@@ -51,12 +64,20 @@ const formatResult = (value) => {
|
|
|
51
64
|
return `${value.value} ${value.unit}`;
|
|
52
65
|
}
|
|
53
66
|
|
|
67
|
+
if (isDenseMatrixWrapper(value)) {
|
|
68
|
+
return serializeExprifyValue(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
if (isMatrix(value)) {
|
|
55
|
-
return value.map((row) => row.join("\t")).join("\n");
|
|
72
|
+
return value.map((row) => row.map(formatScalar).join("\t")).join("\n");
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
if (Array.isArray(value)) {
|
|
59
|
-
return
|
|
76
|
+
return JSON.stringify(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (value && typeof value === "object") {
|
|
80
|
+
return serializeExprifyValue(value);
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
return value;
|
|
@@ -70,6 +91,214 @@ class exprify {
|
|
|
70
91
|
this.functions = createFunctionRegistry(internalFunctions);
|
|
71
92
|
this.variables = createVarStore();
|
|
72
93
|
this._cache = new Map();
|
|
94
|
+
this.variables.set("pi", Math.PI);
|
|
95
|
+
this.variables.set("e", Math.E);
|
|
96
|
+
this.addFunction("parse", (expression) => {
|
|
97
|
+
if (typeof expression !== "string") {
|
|
98
|
+
throw new Error("parse() expects an expression string");
|
|
99
|
+
}
|
|
100
|
+
return expression;
|
|
101
|
+
});
|
|
102
|
+
this.addFunction("leafCount", (value) => {
|
|
103
|
+
const countLeafTokens = (expression) => {
|
|
104
|
+
const strippedKeys = expression.replace(/(^|[{,]\s*)[a-zA-Z_][a-zA-Z0-9_]*\s*:/g, "$1");
|
|
105
|
+
const matches = strippedKeys.match(/\d+(\.\d+)?(e[+-]?\d+)?n?|[a-zA-Z_][a-zA-Z0-9_]*/gi);
|
|
106
|
+
return matches ? matches.length : 0;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let ast = value;
|
|
110
|
+
if (typeof value === "string") {
|
|
111
|
+
try {
|
|
112
|
+
ast = this.parse(value).ast;
|
|
113
|
+
} catch {
|
|
114
|
+
return countLeafTokens(value);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const countLeaves = (node) => {
|
|
119
|
+
if (!node || typeof node !== "object") return 0;
|
|
120
|
+
|
|
121
|
+
switch (node.type) {
|
|
122
|
+
case "Literal":
|
|
123
|
+
case "ImaginaryLiteral":
|
|
124
|
+
case "UnitLiteral":
|
|
125
|
+
case "Identifier":
|
|
126
|
+
return 1;
|
|
127
|
+
default:
|
|
128
|
+
return Object.values(node).reduce((sum, child) => {
|
|
129
|
+
if (Array.isArray(child)) {
|
|
130
|
+
return sum + child.reduce((inner, item) => inner + countLeaves(item), 0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return sum + countLeaves(child);
|
|
134
|
+
}, 0);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return countLeaves(ast);
|
|
139
|
+
});
|
|
140
|
+
this.addFunction("matrix", (value) => wrapDenseMatrix(value));
|
|
141
|
+
this.addFunction("sparse", (value) => wrapDenseMatrix(value));
|
|
142
|
+
this.addFunction("rationalize", (expression, withDetails = false) => {
|
|
143
|
+
if (typeof expression !== "string") {
|
|
144
|
+
throw new Error("rationalize() expects an expression string");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const normalizedExpression = expression
|
|
148
|
+
.replace(/\s+/g, "")
|
|
149
|
+
.replace(/(\d)([a-zA-Z(])/g, "$1*$2")
|
|
150
|
+
.replace(/([a-zA-Z)])(\d)/g, "$1*$2");
|
|
151
|
+
|
|
152
|
+
const polyKey = (powers) => JSON.stringify(Object.entries(powers).sort(([a], [b]) => a.localeCompare(b)));
|
|
153
|
+
const keyToPowers = (key) => Object.fromEntries(JSON.parse(key));
|
|
154
|
+
const makePoly = (terms = new Map()) => terms;
|
|
155
|
+
const constPoly = (value) => new Map([[polyKey({}), value]]);
|
|
156
|
+
const varPoly = (name) => new Map([[polyKey({ [name]: 1 }), 1]]);
|
|
157
|
+
const cleanPoly = (poly) => new Map([...poly.entries()].filter(([, coeff]) => coeff !== 0));
|
|
158
|
+
const addPoly = (a, b, sign = 1) => {
|
|
159
|
+
const result = new Map(a);
|
|
160
|
+
for (const [key, coeff] of b.entries()) {
|
|
161
|
+
result.set(key, (result.get(key) || 0) + (sign * coeff));
|
|
162
|
+
}
|
|
163
|
+
return cleanPoly(result);
|
|
164
|
+
};
|
|
165
|
+
const multiplyPoly = (a, b) => {
|
|
166
|
+
const result = new Map();
|
|
167
|
+
for (const [keyA, coeffA] of a.entries()) {
|
|
168
|
+
const powersA = keyToPowers(keyA);
|
|
169
|
+
for (const [keyB, coeffB] of b.entries()) {
|
|
170
|
+
const powersB = keyToPowers(keyB);
|
|
171
|
+
const merged = { ...powersA };
|
|
172
|
+
for (const [name, power] of Object.entries(powersB)) {
|
|
173
|
+
merged[name] = (merged[name] || 0) + power;
|
|
174
|
+
}
|
|
175
|
+
const key = polyKey(merged);
|
|
176
|
+
result.set(key, (result.get(key) || 0) + (coeffA * coeffB));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return cleanPoly(result);
|
|
180
|
+
};
|
|
181
|
+
const powPoly = (poly, exponent) => {
|
|
182
|
+
let result = constPoly(1);
|
|
183
|
+
for (let index = 0; index < exponent; index++) {
|
|
184
|
+
result = multiplyPoly(result, poly);
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
};
|
|
188
|
+
const rational = (num, den = constPoly(1)) => ({ num, den });
|
|
189
|
+
const addRat = (a, b, sign = 1) => rational(
|
|
190
|
+
addPoly(
|
|
191
|
+
multiplyPoly(a.num, b.den),
|
|
192
|
+
multiplyPoly(b.num, a.den),
|
|
193
|
+
sign
|
|
194
|
+
),
|
|
195
|
+
multiplyPoly(a.den, b.den)
|
|
196
|
+
);
|
|
197
|
+
const mulRat = (a, b) => rational(multiplyPoly(a.num, b.num), multiplyPoly(a.den, b.den));
|
|
198
|
+
const divRat = (a, b) => rational(multiplyPoly(a.num, b.den), multiplyPoly(a.den, b.num));
|
|
199
|
+
const negRat = (value) => rational(addPoly(new Map(), value.num, -1), value.den);
|
|
200
|
+
const astToRat = (node) => {
|
|
201
|
+
switch (node.type) {
|
|
202
|
+
case "Literal":
|
|
203
|
+
return rational(constPoly(node.value));
|
|
204
|
+
case "Identifier":
|
|
205
|
+
return rational(varPoly(node.name));
|
|
206
|
+
case "UnaryExpression":
|
|
207
|
+
if (node.operator === "-") return negRat(astToRat(node.argument));
|
|
208
|
+
throw new Error("Unsupported unary operator");
|
|
209
|
+
case "BinaryExpression": {
|
|
210
|
+
const left = astToRat(node.left);
|
|
211
|
+
const right = astToRat(node.right);
|
|
212
|
+
switch (node.operator) {
|
|
213
|
+
case "+": return addRat(left, right);
|
|
214
|
+
case "-": return addRat(left, right, -1);
|
|
215
|
+
case "*": return mulRat(left, right);
|
|
216
|
+
case "/": return divRat(left, right);
|
|
217
|
+
case "^": {
|
|
218
|
+
if (node.right.type !== "Literal" || !Number.isInteger(node.right.value) || node.right.value < 0) {
|
|
219
|
+
throw new Error("Unsupported exponent");
|
|
220
|
+
}
|
|
221
|
+
return rational(
|
|
222
|
+
powPoly(left.num, node.right.value),
|
|
223
|
+
powPoly(left.den, node.right.value)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
default:
|
|
227
|
+
throw new Error("Unsupported operator in rationalize()");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
default:
|
|
231
|
+
throw new Error("Unsupported expression in rationalize()");
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const formatPoly = (poly) => {
|
|
235
|
+
const entries = [...poly.entries()]
|
|
236
|
+
.filter(([, coeff]) => coeff !== 0)
|
|
237
|
+
.sort(([keyA], [keyB]) => {
|
|
238
|
+
const powersA = keyToPowers(keyA);
|
|
239
|
+
const powersB = keyToPowers(keyB);
|
|
240
|
+
const firstVarA = Object.keys(powersA).sort()[0] || "";
|
|
241
|
+
const firstVarB = Object.keys(powersB).sort()[0] || "";
|
|
242
|
+
|
|
243
|
+
if (firstVarA !== firstVarB) {
|
|
244
|
+
return firstVarA.localeCompare(firstVarB);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const degreeA = Object.values(powersA).reduce((sum, value) => sum + value, 0);
|
|
248
|
+
const degreeB = Object.values(powersB).reduce((sum, value) => sum + value, 0);
|
|
249
|
+
return degreeB - degreeA;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (!entries.length) return "0";
|
|
253
|
+
|
|
254
|
+
return entries.map(([key, coeff], index) => {
|
|
255
|
+
const powers = keyToPowers(key);
|
|
256
|
+
const absCoeff = Math.abs(coeff);
|
|
257
|
+
const variablePart = Object.entries(powers)
|
|
258
|
+
.map(([name, power]) => power === 1 ? name : `${name} ^ ${power}`)
|
|
259
|
+
.join(" * ");
|
|
260
|
+
let body = variablePart;
|
|
261
|
+
|
|
262
|
+
if (!body) {
|
|
263
|
+
body = `${absCoeff}`;
|
|
264
|
+
} else if (absCoeff !== 1) {
|
|
265
|
+
body = `${absCoeff} * ${body}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (index === 0) {
|
|
269
|
+
return coeff < 0 ? `- ${body}`.replace("- ", "-") : body;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return coeff < 0 ? `- ${body}` : `+ ${body}`;
|
|
273
|
+
}).join(" ");
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const ast = this.parse(normalizedExpression).ast;
|
|
277
|
+
const result = astToRat(ast);
|
|
278
|
+
const numerator = formatPoly(result.num);
|
|
279
|
+
const denominator = formatPoly(result.den);
|
|
280
|
+
const variableSet = new Set();
|
|
281
|
+
|
|
282
|
+
for (const poly of [result.num, result.den]) {
|
|
283
|
+
for (const key of poly.keys()) {
|
|
284
|
+
for (const name of Object.keys(keyToPowers(key))) {
|
|
285
|
+
variableSet.add(name);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!withDetails) {
|
|
291
|
+
return `(${numerator}) / (${denominator})`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
numerator,
|
|
296
|
+
denominator,
|
|
297
|
+
coefficients: [],
|
|
298
|
+
variables: [...variableSet].sort(),
|
|
299
|
+
expression: `(${numerator}) / (${denominator})`
|
|
300
|
+
};
|
|
301
|
+
});
|
|
73
302
|
}
|
|
74
303
|
|
|
75
304
|
setVariable(name, value) {
|
package/src/function/internal.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { unwrapDenseMatrix, wrapDenseMatrix } from "../utils/matrix.js";
|
|
2
|
+
|
|
1
3
|
function validateSquareMatrix(matrix) {
|
|
4
|
+
matrix = unwrapDenseMatrix(matrix);
|
|
2
5
|
if (!Array.isArray(matrix) || matrix.length === 0) {
|
|
3
6
|
throw new Error("det() expects a non-empty matrix");
|
|
4
7
|
}
|
|
@@ -22,6 +25,7 @@ function validateSquareMatrix(matrix) {
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
function determinant(matrix) {
|
|
28
|
+
matrix = unwrapDenseMatrix(matrix);
|
|
25
29
|
validateSquareMatrix(matrix);
|
|
26
30
|
|
|
27
31
|
if (matrix.length === 1) {
|
|
@@ -41,6 +45,339 @@ function determinant(matrix) {
|
|
|
41
45
|
}, 0);
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
function toLinearArray(value) {
|
|
49
|
+
const unwrapped = unwrapDenseMatrix(value);
|
|
50
|
+
return Array.isArray(unwrapped) ? unwrapped : value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function asMatrixData(value) {
|
|
54
|
+
const data = unwrapDenseMatrix(value);
|
|
55
|
+
if (!Array.isArray(data)) {
|
|
56
|
+
throw new Error("Expected matrix data");
|
|
57
|
+
}
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function solveLinearSystem(coefficients, constants) {
|
|
62
|
+
const n = coefficients.length;
|
|
63
|
+
const augmented = coefficients.map((row, rowIndex) => [...row, constants[rowIndex]]);
|
|
64
|
+
|
|
65
|
+
for (let pivot = 0; pivot < n; pivot++) {
|
|
66
|
+
let maxRow = pivot;
|
|
67
|
+
let maxValue = Math.abs(augmented[pivot][pivot]);
|
|
68
|
+
|
|
69
|
+
for (let row = pivot + 1; row < n; row++) {
|
|
70
|
+
const current = Math.abs(augmented[row][pivot]);
|
|
71
|
+
if (current > maxValue) {
|
|
72
|
+
maxValue = current;
|
|
73
|
+
maxRow = row;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (maxValue === 0) {
|
|
78
|
+
throw new Error("Linear system is singular");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (maxRow !== pivot) {
|
|
82
|
+
[augmented[pivot], augmented[maxRow]] = [augmented[maxRow], augmented[pivot]];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const pivotValue = augmented[pivot][pivot];
|
|
86
|
+
for (let col = pivot; col <= n; col++) {
|
|
87
|
+
augmented[pivot][col] /= pivotValue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (let row = 0; row < n; row++) {
|
|
91
|
+
if (row === pivot) continue;
|
|
92
|
+
const factor = augmented[row][pivot];
|
|
93
|
+
for (let col = pivot; col <= n; col++) {
|
|
94
|
+
augmented[row][col] -= factor * augmented[pivot][col];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return augmented.map((row) => row[n]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function lupDecomposition(input) {
|
|
103
|
+
const matrix = asMatrixData(input).map((row) => [...row]);
|
|
104
|
+
validateSquareMatrix(matrix);
|
|
105
|
+
|
|
106
|
+
const n = matrix.length;
|
|
107
|
+
const permutation = Array.from({ length: n }, (_, index) => index);
|
|
108
|
+
|
|
109
|
+
for (let pivot = 0; pivot < n; pivot++) {
|
|
110
|
+
let maxRow = pivot;
|
|
111
|
+
let maxValue = Math.abs(matrix[pivot][pivot]);
|
|
112
|
+
|
|
113
|
+
for (let row = pivot + 1; row < n; row++) {
|
|
114
|
+
const current = Math.abs(matrix[row][pivot]);
|
|
115
|
+
if (current > maxValue) {
|
|
116
|
+
maxValue = current;
|
|
117
|
+
maxRow = row;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (maxValue === 0) {
|
|
122
|
+
throw new Error("Matrix is singular");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (maxRow !== pivot) {
|
|
126
|
+
[matrix[pivot], matrix[maxRow]] = [matrix[maxRow], matrix[pivot]];
|
|
127
|
+
[permutation[pivot], permutation[maxRow]] = [permutation[maxRow], permutation[pivot]];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (let row = pivot + 1; row < n; row++) {
|
|
131
|
+
matrix[row][pivot] /= matrix[pivot][pivot];
|
|
132
|
+
for (let col = pivot + 1; col < n; col++) {
|
|
133
|
+
matrix[row][col] -= matrix[row][pivot] * matrix[pivot][col];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const L = matrix.map((row, rowIndex) =>
|
|
139
|
+
row.map((value, colIndex) => {
|
|
140
|
+
if (rowIndex === colIndex) return 1;
|
|
141
|
+
if (rowIndex > colIndex) return value;
|
|
142
|
+
return 0;
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const U = matrix.map((row, rowIndex) =>
|
|
147
|
+
row.map((value, colIndex) => (rowIndex <= colIndex ? value : 0))
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
L: wrapDenseMatrix(L),
|
|
152
|
+
U: wrapDenseMatrix(U),
|
|
153
|
+
p: permutation
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function linearSolve(aInput, bInput) {
|
|
158
|
+
const { L, U, p } = lupDecomposition(aInput);
|
|
159
|
+
const a = asMatrixData(aInput);
|
|
160
|
+
const bData = asMatrixData(bInput);
|
|
161
|
+
const bVector = Array.isArray(bData[0]) ? bData.map((row) => row[0]) : bData;
|
|
162
|
+
|
|
163
|
+
if (a.length !== bVector.length) {
|
|
164
|
+
throw new Error("Right-hand side dimension mismatch");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const permutedB = p.map((index) => bVector[index]);
|
|
168
|
+
const y = new Array(a.length).fill(0);
|
|
169
|
+
|
|
170
|
+
for (let row = 0; row < a.length; row++) {
|
|
171
|
+
y[row] = permutedB[row];
|
|
172
|
+
for (let col = 0; col < row; col++) {
|
|
173
|
+
y[row] -= L.data[row][col] * y[col];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const x = new Array(a.length).fill(0);
|
|
178
|
+
for (let row = a.length - 1; row >= 0; row--) {
|
|
179
|
+
x[row] = y[row];
|
|
180
|
+
for (let col = row + 1; col < a.length; col++) {
|
|
181
|
+
x[row] -= U.data[row][col] * x[col];
|
|
182
|
+
}
|
|
183
|
+
x[row] /= U.data[row][row];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return wrapDenseMatrix(x.map((value) => [value]));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function solveLyapunov(aInput, qInput) {
|
|
190
|
+
const A = asMatrixData(aInput).map((row) => [...row]);
|
|
191
|
+
const Q = asMatrixData(qInput).map((row) => [...row]);
|
|
192
|
+
validateSquareMatrix(A);
|
|
193
|
+
validateSquareMatrix(Q);
|
|
194
|
+
|
|
195
|
+
const n = A.length;
|
|
196
|
+
if (Q.length !== n) {
|
|
197
|
+
throw new Error("A and Q must have the same dimensions");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const coefficients = [];
|
|
201
|
+
const constants = [];
|
|
202
|
+
|
|
203
|
+
for (let row = 0; row < n; row++) {
|
|
204
|
+
for (let col = 0; col < n; col++) {
|
|
205
|
+
const equation = new Array(n * n).fill(0);
|
|
206
|
+
|
|
207
|
+
for (let k = 0; k < n; k++) {
|
|
208
|
+
equation[k * n + col] += A[row][k];
|
|
209
|
+
equation[row * n + k] += A[col][k];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
coefficients.push(equation);
|
|
213
|
+
constants.push(-Q[row][col]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const solution = solveLinearSystem(coefficients, constants);
|
|
218
|
+
const X = [];
|
|
219
|
+
|
|
220
|
+
for (let row = 0; row < n; row++) {
|
|
221
|
+
X.push(solution.slice(row * n, (row + 1) * n));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return wrapDenseMatrix(X);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function evaluatePolynomial(coefficients, x) {
|
|
228
|
+
return coefficients.reduce((sum, coefficient, index) => sum + (coefficient * (x ** index)), 0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function syntheticDivide(coefficients, root) {
|
|
232
|
+
const descending = [...coefficients].reverse();
|
|
233
|
+
const quotient = [descending[0]];
|
|
234
|
+
|
|
235
|
+
for (let index = 1; index < descending.length - 1; index++) {
|
|
236
|
+
quotient.push(descending[index] + (quotient[index - 1] * root));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const remainder = descending[descending.length - 1] + (quotient[quotient.length - 1] * root);
|
|
240
|
+
return {
|
|
241
|
+
quotient: quotient.reverse(),
|
|
242
|
+
remainder
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function solveQuadratic(coefficients) {
|
|
247
|
+
const [c, b, a] = coefficients;
|
|
248
|
+
const discriminant = (b ** 2) - (4 * a * c);
|
|
249
|
+
if (discriminant < 0) {
|
|
250
|
+
throw new Error("Only real roots are supported");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const sqrtDisc = Math.sqrt(discriminant);
|
|
254
|
+
return [
|
|
255
|
+
(-b + sqrtDisc) / (2 * a),
|
|
256
|
+
(-b - sqrtDisc) / (2 * a)
|
|
257
|
+
];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function polynomialRoots(...coefficients) {
|
|
261
|
+
while (coefficients.length > 1 && coefficients[coefficients.length - 1] === 0) {
|
|
262
|
+
coefficients.pop();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const degree = coefficients.length - 1;
|
|
266
|
+
if (degree < 1) {
|
|
267
|
+
throw new Error("polynomialRoot() expects at least a linear polynomial");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (degree === 1) {
|
|
271
|
+
const [b, a] = coefficients;
|
|
272
|
+
return [-b / a];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (degree === 2) {
|
|
276
|
+
return solveQuadratic(coefficients);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (degree === 3) {
|
|
280
|
+
const constant = coefficients[0];
|
|
281
|
+
const leading = coefficients[3];
|
|
282
|
+
const candidates = [];
|
|
283
|
+
const limit = Math.abs(constant);
|
|
284
|
+
|
|
285
|
+
for (let divisor = 1; divisor <= Math.max(1, limit); divisor++) {
|
|
286
|
+
if (limit % divisor === 0) {
|
|
287
|
+
candidates.push(divisor, -divisor);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const candidate of candidates) {
|
|
292
|
+
if (evaluatePolynomial(coefficients, candidate) === 0) {
|
|
293
|
+
const reduced = syntheticDivide(coefficients, candidate);
|
|
294
|
+
const remainingRoots = solveQuadratic(reduced.quotient);
|
|
295
|
+
return [candidate, ...remainingRoots];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
throw new Error("polynomialRoot() currently supports degree up to 3");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function dotProduct(a, b) {
|
|
304
|
+
return a.reduce((sum, value, index) => sum + (value * b[index]), 0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function vectorNorm(vector) {
|
|
308
|
+
return Math.sqrt(dotProduct(vector, vector));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function scaleVector(vector, scalar) {
|
|
312
|
+
return vector.map((value) => value * scalar);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function subtractVectors(a, b) {
|
|
316
|
+
return a.map((value, index) => value - b[index]);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function transpose(matrix) {
|
|
320
|
+
return matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex]));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function qrDecomposition(input) {
|
|
324
|
+
const A = asMatrixData(input).map((row) => [...row]);
|
|
325
|
+
if (!A.length || !A.every((row) => row.length === A[0].length)) {
|
|
326
|
+
throw new Error("qr() expects a rectangular matrix");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const rowCount = A.length;
|
|
330
|
+
const colCount = A[0].length;
|
|
331
|
+
const columns = transpose(A);
|
|
332
|
+
const qColumns = [];
|
|
333
|
+
|
|
334
|
+
for (let col = 0; col < colCount; col++) {
|
|
335
|
+
let vector = [...columns[col]];
|
|
336
|
+
|
|
337
|
+
for (let existing = 0; existing < qColumns.length; existing++) {
|
|
338
|
+
const projection = dotProduct(qColumns[existing], columns[col]);
|
|
339
|
+
vector = subtractVectors(vector, scaleVector(qColumns[existing], projection));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const norm = vectorNorm(vector);
|
|
343
|
+
if (norm === 0) {
|
|
344
|
+
throw new Error("qr() requires linearly independent columns");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
qColumns.push(scaleVector(vector, 1 / norm));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (let basisIndex = 0; qColumns.length < rowCount && basisIndex < rowCount; basisIndex++) {
|
|
351
|
+
let candidate = Array.from({ length: rowCount }, (_, index) => (index === basisIndex ? 1 : 0));
|
|
352
|
+
|
|
353
|
+
for (const column of qColumns) {
|
|
354
|
+
const projection = dotProduct(column, candidate);
|
|
355
|
+
candidate = subtractVectors(candidate, scaleVector(column, projection));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const norm = vectorNorm(candidate);
|
|
359
|
+
if (norm > 1e-10) {
|
|
360
|
+
qColumns.push(scaleVector(candidate, 1 / norm));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const Q = Array.from({ length: rowCount }, (_, rowIndex) =>
|
|
365
|
+
qColumns.map((column) => column[rowIndex])
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const fullR = Array.from({ length: rowCount }, () => Array(colCount).fill(0));
|
|
369
|
+
for (let row = 0; row < rowCount; row++) {
|
|
370
|
+
for (let col = 0; col < colCount; col++) {
|
|
371
|
+
fullR[row][col] = dotProduct(qColumns[row], columns[col]);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
Q: wrapDenseMatrix(Q),
|
|
377
|
+
R: wrapDenseMatrix(fullR)
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
44
381
|
function splitTerms(expression) {
|
|
45
382
|
const normalized = expression.replace(/\s+/g, "");
|
|
46
383
|
if (!normalized) {
|
|
@@ -177,6 +514,11 @@ export const internalFunctions = {
|
|
|
177
514
|
|
|
178
515
|
pow: (a, b) => a ** b,
|
|
179
516
|
det: (matrix) => determinant(matrix),
|
|
517
|
+
polynomialRoot: (...coefficients) => polynomialRoots(...coefficients),
|
|
518
|
+
lsolve: (a, b) => linearSolve(a, b),
|
|
519
|
+
lup: (matrix) => lupDecomposition(matrix),
|
|
520
|
+
lyap: (a, q) => solveLyapunov(a, q),
|
|
521
|
+
qr: (matrix) => qrDecomposition(matrix),
|
|
180
522
|
simplify: (expression) => {
|
|
181
523
|
if (typeof expression !== "string") {
|
|
182
524
|
throw new Error("simplify() expects an expression string");
|
package/src/parser/astBuild.js
CHANGED
|
@@ -65,7 +65,7 @@ export function buildAST(tokens) {
|
|
|
65
65
|
case "Identifier":
|
|
66
66
|
return { type: "Identifier", name: token.name };
|
|
67
67
|
|
|
68
|
-
case "Function":
|
|
68
|
+
case "Function":
|
|
69
69
|
return {
|
|
70
70
|
type: "Identifier",
|
|
71
71
|
name: token.name
|
|
@@ -470,6 +470,29 @@ export function buildAST(tokens) {
|
|
|
470
470
|
) {
|
|
471
471
|
const operator = tokens[current - 1].value;
|
|
472
472
|
|
|
473
|
+
if (left.type === "CallExpression") {
|
|
474
|
+
const isFunctionTarget =
|
|
475
|
+
left.callee?.type === "Identifier" &&
|
|
476
|
+
left.arguments.every((arg) => arg.type === "Identifier");
|
|
477
|
+
|
|
478
|
+
if (!isFunctionTarget) {
|
|
479
|
+
throw new Error("Invalid function definition");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const right = parseAssignment();
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
type: "FunctionAssignmentExpression",
|
|
486
|
+
operator,
|
|
487
|
+
left: {
|
|
488
|
+
type: "Identifier",
|
|
489
|
+
name: left.callee.name
|
|
490
|
+
},
|
|
491
|
+
params: left.arguments.map((arg) => arg.name),
|
|
492
|
+
right
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
473
496
|
if (
|
|
474
497
|
left.type !== "Identifier" &&
|
|
475
498
|
left.type !== "MemberExpression" &&
|