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/dist/exprify.cjs.js
CHANGED
|
@@ -400,6 +400,60 @@ function tokenize(expr, context = {}) {
|
|
|
400
400
|
return final;
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
const isDenseMatrixWrapper = (value) =>
|
|
404
|
+
value &&
|
|
405
|
+
typeof value === "object" &&
|
|
406
|
+
value.exprify === "DenseMatrix" &&
|
|
407
|
+
"data" in value &&
|
|
408
|
+
"size" in value;
|
|
409
|
+
|
|
410
|
+
const cloneMatrixData = (value) => {
|
|
411
|
+
if (Array.isArray(value)) {
|
|
412
|
+
return value.map(cloneMatrixData);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return value;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const getMatrixSize = (data) => {
|
|
419
|
+
if (Array.isArray(data) && data.every(Array.isArray)) {
|
|
420
|
+
return [data.length, data[0]?.length || 0];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (Array.isArray(data)) {
|
|
424
|
+
return [data.length];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
throw new Error("Matrix data must be an array");
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const wrapDenseMatrix = (data) => ({
|
|
431
|
+
exprify: "DenseMatrix",
|
|
432
|
+
data: cloneMatrixData(data),
|
|
433
|
+
size: getMatrixSize(data)
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const unwrapDenseMatrix = (value) =>
|
|
437
|
+
isDenseMatrixWrapper(value) ? cloneMatrixData(value.data) : value;
|
|
438
|
+
|
|
439
|
+
const serializeExprifyValue = (value) => {
|
|
440
|
+
if (isDenseMatrixWrapper(value)) {
|
|
441
|
+
return JSON.stringify(value);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (Array.isArray(value) || (value && typeof value === "object")) {
|
|
445
|
+
return JSON.stringify(value, (_, current) => {
|
|
446
|
+
if (isDenseMatrixWrapper(current)) {
|
|
447
|
+
return current;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return current;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return value;
|
|
455
|
+
};
|
|
456
|
+
|
|
403
457
|
function evaluateAST(node, context = {}) {
|
|
404
458
|
|
|
405
459
|
const vars = context.variables;
|
|
@@ -420,6 +474,7 @@ function evaluateAST(node, context = {}) {
|
|
|
420
474
|
Array.isArray(v) && v.length > 0 && v.every(Array.isArray);
|
|
421
475
|
|
|
422
476
|
const normalizeMatrix = (value) => {
|
|
477
|
+
value = unwrapDenseMatrix(value);
|
|
423
478
|
if (isMatrix(value)) return value.map((row) => [...row]);
|
|
424
479
|
if (Array.isArray(value)) return [value];
|
|
425
480
|
throw new Error("Expected matrix-compatible value");
|
|
@@ -592,6 +647,16 @@ function evaluateAST(node, context = {}) {
|
|
|
592
647
|
const simplifyComplex = (value) =>
|
|
593
648
|
value.im === 0 ? value.re : value;
|
|
594
649
|
|
|
650
|
+
const createFunctionScope = (params, args) => {
|
|
651
|
+
const scopedValues = {};
|
|
652
|
+
|
|
653
|
+
params.forEach((param, index) => {
|
|
654
|
+
scopedValues[param] = args[index];
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
return scopedValues;
|
|
658
|
+
};
|
|
659
|
+
|
|
595
660
|
const evalComplexBinary = (operator, left, right) => {
|
|
596
661
|
const a = toComplex(left);
|
|
597
662
|
const b = toComplex(right);
|
|
@@ -647,6 +712,9 @@ function evaluateAST(node, context = {}) {
|
|
|
647
712
|
|
|
648
713
|
if (node.left.type === "Identifier") {
|
|
649
714
|
vars.set(node.left.name, value);
|
|
715
|
+
if (node.right.type === "ArrayExpression") {
|
|
716
|
+
return wrapDenseMatrix(unwrapDenseMatrix(value));
|
|
717
|
+
}
|
|
650
718
|
return value;
|
|
651
719
|
}
|
|
652
720
|
|
|
@@ -660,6 +728,20 @@ function evaluateAST(node, context = {}) {
|
|
|
660
728
|
throw new Error("Invalid assignment target");
|
|
661
729
|
}
|
|
662
730
|
|
|
731
|
+
case "FunctionAssignmentExpression": {
|
|
732
|
+
if (node.operator !== "=") {
|
|
733
|
+
throw new Error(`Operator ${node.operator} is not supported for function definitions`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const fn = (...args) => {
|
|
737
|
+
const scopedContext = context.withScope(createFunctionScope(node.params, args));
|
|
738
|
+
return evaluateAST(node.right, scopedContext);
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
fns.register(node.left.name, fn);
|
|
742
|
+
return fn;
|
|
743
|
+
}
|
|
744
|
+
|
|
663
745
|
/* ===== UNARY ===== */
|
|
664
746
|
case "UnaryExpression": {
|
|
665
747
|
const val = evaluateAST(node.argument, context);
|
|
@@ -680,7 +762,7 @@ function evaluateAST(node, context = {}) {
|
|
|
680
762
|
let left = evaluateAST(node.left, context);
|
|
681
763
|
let right = evaluateAST(node.right, context);
|
|
682
764
|
|
|
683
|
-
//
|
|
765
|
+
// UNIT handling
|
|
684
766
|
if (isUnitObj(left) || isUnitObj(right)) {
|
|
685
767
|
|
|
686
768
|
if (!units) throw new Error("Unit system not available");
|
|
@@ -1430,6 +1512,7 @@ function createFunctionRegistry(initial = {}) {
|
|
|
1430
1512
|
}
|
|
1431
1513
|
|
|
1432
1514
|
function validateSquareMatrix(matrix) {
|
|
1515
|
+
matrix = unwrapDenseMatrix(matrix);
|
|
1433
1516
|
if (!Array.isArray(matrix) || matrix.length === 0) {
|
|
1434
1517
|
throw new Error("det() expects a non-empty matrix");
|
|
1435
1518
|
}
|
|
@@ -1453,6 +1536,7 @@ function validateSquareMatrix(matrix) {
|
|
|
1453
1536
|
}
|
|
1454
1537
|
|
|
1455
1538
|
function determinant(matrix) {
|
|
1539
|
+
matrix = unwrapDenseMatrix(matrix);
|
|
1456
1540
|
validateSquareMatrix(matrix);
|
|
1457
1541
|
|
|
1458
1542
|
if (matrix.length === 1) {
|
|
@@ -1472,6 +1556,334 @@ function determinant(matrix) {
|
|
|
1472
1556
|
}, 0);
|
|
1473
1557
|
}
|
|
1474
1558
|
|
|
1559
|
+
function asMatrixData(value) {
|
|
1560
|
+
const data = unwrapDenseMatrix(value);
|
|
1561
|
+
if (!Array.isArray(data)) {
|
|
1562
|
+
throw new Error("Expected matrix data");
|
|
1563
|
+
}
|
|
1564
|
+
return data;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function solveLinearSystem(coefficients, constants) {
|
|
1568
|
+
const n = coefficients.length;
|
|
1569
|
+
const augmented = coefficients.map((row, rowIndex) => [...row, constants[rowIndex]]);
|
|
1570
|
+
|
|
1571
|
+
for (let pivot = 0; pivot < n; pivot++) {
|
|
1572
|
+
let maxRow = pivot;
|
|
1573
|
+
let maxValue = Math.abs(augmented[pivot][pivot]);
|
|
1574
|
+
|
|
1575
|
+
for (let row = pivot + 1; row < n; row++) {
|
|
1576
|
+
const current = Math.abs(augmented[row][pivot]);
|
|
1577
|
+
if (current > maxValue) {
|
|
1578
|
+
maxValue = current;
|
|
1579
|
+
maxRow = row;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
if (maxValue === 0) {
|
|
1584
|
+
throw new Error("Linear system is singular");
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (maxRow !== pivot) {
|
|
1588
|
+
[augmented[pivot], augmented[maxRow]] = [augmented[maxRow], augmented[pivot]];
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
const pivotValue = augmented[pivot][pivot];
|
|
1592
|
+
for (let col = pivot; col <= n; col++) {
|
|
1593
|
+
augmented[pivot][col] /= pivotValue;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
for (let row = 0; row < n; row++) {
|
|
1597
|
+
if (row === pivot) continue;
|
|
1598
|
+
const factor = augmented[row][pivot];
|
|
1599
|
+
for (let col = pivot; col <= n; col++) {
|
|
1600
|
+
augmented[row][col] -= factor * augmented[pivot][col];
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
return augmented.map((row) => row[n]);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function lupDecomposition(input) {
|
|
1609
|
+
const matrix = asMatrixData(input).map((row) => [...row]);
|
|
1610
|
+
validateSquareMatrix(matrix);
|
|
1611
|
+
|
|
1612
|
+
const n = matrix.length;
|
|
1613
|
+
const permutation = Array.from({ length: n }, (_, index) => index);
|
|
1614
|
+
|
|
1615
|
+
for (let pivot = 0; pivot < n; pivot++) {
|
|
1616
|
+
let maxRow = pivot;
|
|
1617
|
+
let maxValue = Math.abs(matrix[pivot][pivot]);
|
|
1618
|
+
|
|
1619
|
+
for (let row = pivot + 1; row < n; row++) {
|
|
1620
|
+
const current = Math.abs(matrix[row][pivot]);
|
|
1621
|
+
if (current > maxValue) {
|
|
1622
|
+
maxValue = current;
|
|
1623
|
+
maxRow = row;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
if (maxValue === 0) {
|
|
1628
|
+
throw new Error("Matrix is singular");
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
if (maxRow !== pivot) {
|
|
1632
|
+
[matrix[pivot], matrix[maxRow]] = [matrix[maxRow], matrix[pivot]];
|
|
1633
|
+
[permutation[pivot], permutation[maxRow]] = [permutation[maxRow], permutation[pivot]];
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
for (let row = pivot + 1; row < n; row++) {
|
|
1637
|
+
matrix[row][pivot] /= matrix[pivot][pivot];
|
|
1638
|
+
for (let col = pivot + 1; col < n; col++) {
|
|
1639
|
+
matrix[row][col] -= matrix[row][pivot] * matrix[pivot][col];
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
const L = matrix.map((row, rowIndex) =>
|
|
1645
|
+
row.map((value, colIndex) => {
|
|
1646
|
+
if (rowIndex === colIndex) return 1;
|
|
1647
|
+
if (rowIndex > colIndex) return value;
|
|
1648
|
+
return 0;
|
|
1649
|
+
})
|
|
1650
|
+
);
|
|
1651
|
+
|
|
1652
|
+
const U = matrix.map((row, rowIndex) =>
|
|
1653
|
+
row.map((value, colIndex) => (rowIndex <= colIndex ? value : 0))
|
|
1654
|
+
);
|
|
1655
|
+
|
|
1656
|
+
return {
|
|
1657
|
+
L: wrapDenseMatrix(L),
|
|
1658
|
+
U: wrapDenseMatrix(U),
|
|
1659
|
+
p: permutation
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function linearSolve(aInput, bInput) {
|
|
1664
|
+
const { L, U, p } = lupDecomposition(aInput);
|
|
1665
|
+
const a = asMatrixData(aInput);
|
|
1666
|
+
const bData = asMatrixData(bInput);
|
|
1667
|
+
const bVector = Array.isArray(bData[0]) ? bData.map((row) => row[0]) : bData;
|
|
1668
|
+
|
|
1669
|
+
if (a.length !== bVector.length) {
|
|
1670
|
+
throw new Error("Right-hand side dimension mismatch");
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
const permutedB = p.map((index) => bVector[index]);
|
|
1674
|
+
const y = new Array(a.length).fill(0);
|
|
1675
|
+
|
|
1676
|
+
for (let row = 0; row < a.length; row++) {
|
|
1677
|
+
y[row] = permutedB[row];
|
|
1678
|
+
for (let col = 0; col < row; col++) {
|
|
1679
|
+
y[row] -= L.data[row][col] * y[col];
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const x = new Array(a.length).fill(0);
|
|
1684
|
+
for (let row = a.length - 1; row >= 0; row--) {
|
|
1685
|
+
x[row] = y[row];
|
|
1686
|
+
for (let col = row + 1; col < a.length; col++) {
|
|
1687
|
+
x[row] -= U.data[row][col] * x[col];
|
|
1688
|
+
}
|
|
1689
|
+
x[row] /= U.data[row][row];
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
return wrapDenseMatrix(x.map((value) => [value]));
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function solveLyapunov(aInput, qInput) {
|
|
1696
|
+
const A = asMatrixData(aInput).map((row) => [...row]);
|
|
1697
|
+
const Q = asMatrixData(qInput).map((row) => [...row]);
|
|
1698
|
+
validateSquareMatrix(A);
|
|
1699
|
+
validateSquareMatrix(Q);
|
|
1700
|
+
|
|
1701
|
+
const n = A.length;
|
|
1702
|
+
if (Q.length !== n) {
|
|
1703
|
+
throw new Error("A and Q must have the same dimensions");
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
const coefficients = [];
|
|
1707
|
+
const constants = [];
|
|
1708
|
+
|
|
1709
|
+
for (let row = 0; row < n; row++) {
|
|
1710
|
+
for (let col = 0; col < n; col++) {
|
|
1711
|
+
const equation = new Array(n * n).fill(0);
|
|
1712
|
+
|
|
1713
|
+
for (let k = 0; k < n; k++) {
|
|
1714
|
+
equation[k * n + col] += A[row][k];
|
|
1715
|
+
equation[row * n + k] += A[col][k];
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
coefficients.push(equation);
|
|
1719
|
+
constants.push(-Q[row][col]);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
const solution = solveLinearSystem(coefficients, constants);
|
|
1724
|
+
const X = [];
|
|
1725
|
+
|
|
1726
|
+
for (let row = 0; row < n; row++) {
|
|
1727
|
+
X.push(solution.slice(row * n, (row + 1) * n));
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
return wrapDenseMatrix(X);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function evaluatePolynomial(coefficients, x) {
|
|
1734
|
+
return coefficients.reduce((sum, coefficient, index) => sum + (coefficient * (x ** index)), 0);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function syntheticDivide(coefficients, root) {
|
|
1738
|
+
const descending = [...coefficients].reverse();
|
|
1739
|
+
const quotient = [descending[0]];
|
|
1740
|
+
|
|
1741
|
+
for (let index = 1; index < descending.length - 1; index++) {
|
|
1742
|
+
quotient.push(descending[index] + (quotient[index - 1] * root));
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
const remainder = descending[descending.length - 1] + (quotient[quotient.length - 1] * root);
|
|
1746
|
+
return {
|
|
1747
|
+
quotient: quotient.reverse(),
|
|
1748
|
+
remainder
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function solveQuadratic(coefficients) {
|
|
1753
|
+
const [c, b, a] = coefficients;
|
|
1754
|
+
const discriminant = (b ** 2) - (4 * a * c);
|
|
1755
|
+
if (discriminant < 0) {
|
|
1756
|
+
throw new Error("Only real roots are supported");
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const sqrtDisc = Math.sqrt(discriminant);
|
|
1760
|
+
return [
|
|
1761
|
+
(-b + sqrtDisc) / (2 * a),
|
|
1762
|
+
(-b - sqrtDisc) / (2 * a)
|
|
1763
|
+
];
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function polynomialRoots(...coefficients) {
|
|
1767
|
+
while (coefficients.length > 1 && coefficients[coefficients.length - 1] === 0) {
|
|
1768
|
+
coefficients.pop();
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const degree = coefficients.length - 1;
|
|
1772
|
+
if (degree < 1) {
|
|
1773
|
+
throw new Error("polynomialRoot() expects at least a linear polynomial");
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
if (degree === 1) {
|
|
1777
|
+
const [b, a] = coefficients;
|
|
1778
|
+
return [-b / a];
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
if (degree === 2) {
|
|
1782
|
+
return solveQuadratic(coefficients);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
if (degree === 3) {
|
|
1786
|
+
const constant = coefficients[0];
|
|
1787
|
+
coefficients[3];
|
|
1788
|
+
const candidates = [];
|
|
1789
|
+
const limit = Math.abs(constant);
|
|
1790
|
+
|
|
1791
|
+
for (let divisor = 1; divisor <= Math.max(1, limit); divisor++) {
|
|
1792
|
+
if (limit % divisor === 0) {
|
|
1793
|
+
candidates.push(divisor, -divisor);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
for (const candidate of candidates) {
|
|
1798
|
+
if (evaluatePolynomial(coefficients, candidate) === 0) {
|
|
1799
|
+
const reduced = syntheticDivide(coefficients, candidate);
|
|
1800
|
+
const remainingRoots = solveQuadratic(reduced.quotient);
|
|
1801
|
+
return [candidate, ...remainingRoots];
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
throw new Error("polynomialRoot() currently supports degree up to 3");
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
function dotProduct(a, b) {
|
|
1810
|
+
return a.reduce((sum, value, index) => sum + (value * b[index]), 0);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function vectorNorm(vector) {
|
|
1814
|
+
return Math.sqrt(dotProduct(vector, vector));
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
function scaleVector(vector, scalar) {
|
|
1818
|
+
return vector.map((value) => value * scalar);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function subtractVectors(a, b) {
|
|
1822
|
+
return a.map((value, index) => value - b[index]);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function transpose(matrix) {
|
|
1826
|
+
return matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex]));
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function qrDecomposition(input) {
|
|
1830
|
+
const A = asMatrixData(input).map((row) => [...row]);
|
|
1831
|
+
if (!A.length || !A.every((row) => row.length === A[0].length)) {
|
|
1832
|
+
throw new Error("qr() expects a rectangular matrix");
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const rowCount = A.length;
|
|
1836
|
+
const colCount = A[0].length;
|
|
1837
|
+
const columns = transpose(A);
|
|
1838
|
+
const qColumns = [];
|
|
1839
|
+
|
|
1840
|
+
for (let col = 0; col < colCount; col++) {
|
|
1841
|
+
let vector = [...columns[col]];
|
|
1842
|
+
|
|
1843
|
+
for (let existing = 0; existing < qColumns.length; existing++) {
|
|
1844
|
+
const projection = dotProduct(qColumns[existing], columns[col]);
|
|
1845
|
+
vector = subtractVectors(vector, scaleVector(qColumns[existing], projection));
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
const norm = vectorNorm(vector);
|
|
1849
|
+
if (norm === 0) {
|
|
1850
|
+
throw new Error("qr() requires linearly independent columns");
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
qColumns.push(scaleVector(vector, 1 / norm));
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
for (let basisIndex = 0; qColumns.length < rowCount && basisIndex < rowCount; basisIndex++) {
|
|
1857
|
+
let candidate = Array.from({ length: rowCount }, (_, index) => (index === basisIndex ? 1 : 0));
|
|
1858
|
+
|
|
1859
|
+
for (const column of qColumns) {
|
|
1860
|
+
const projection = dotProduct(column, candidate);
|
|
1861
|
+
candidate = subtractVectors(candidate, scaleVector(column, projection));
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
const norm = vectorNorm(candidate);
|
|
1865
|
+
if (norm > 1e-10) {
|
|
1866
|
+
qColumns.push(scaleVector(candidate, 1 / norm));
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
const Q = Array.from({ length: rowCount }, (_, rowIndex) =>
|
|
1871
|
+
qColumns.map((column) => column[rowIndex])
|
|
1872
|
+
);
|
|
1873
|
+
|
|
1874
|
+
const fullR = Array.from({ length: rowCount }, () => Array(colCount).fill(0));
|
|
1875
|
+
for (let row = 0; row < rowCount; row++) {
|
|
1876
|
+
for (let col = 0; col < colCount; col++) {
|
|
1877
|
+
fullR[row][col] = dotProduct(qColumns[row], columns[col]);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
return {
|
|
1882
|
+
Q: wrapDenseMatrix(Q),
|
|
1883
|
+
R: wrapDenseMatrix(fullR)
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1475
1887
|
function splitTerms(expression) {
|
|
1476
1888
|
const normalized = expression.replace(/\s+/g, "");
|
|
1477
1889
|
if (!normalized) {
|
|
@@ -1608,6 +2020,11 @@ const internalFunctions = {
|
|
|
1608
2020
|
|
|
1609
2021
|
pow: (a, b) => a ** b,
|
|
1610
2022
|
det: (matrix) => determinant(matrix),
|
|
2023
|
+
polynomialRoot: (...coefficients) => polynomialRoots(...coefficients),
|
|
2024
|
+
lsolve: (a, b) => linearSolve(a, b),
|
|
2025
|
+
lup: (matrix) => lupDecomposition(matrix),
|
|
2026
|
+
lyap: (a, q) => solveLyapunov(a, q),
|
|
2027
|
+
qr: (matrix) => qrDecomposition(matrix),
|
|
1611
2028
|
simplify: (expression) => {
|
|
1612
2029
|
if (typeof expression !== "string") {
|
|
1613
2030
|
throw new Error("simplify() expects an expression string");
|
|
@@ -1767,7 +2184,7 @@ function buildAST(tokens) {
|
|
|
1767
2184
|
case "Identifier":
|
|
1768
2185
|
return { type: "Identifier", name: token.name };
|
|
1769
2186
|
|
|
1770
|
-
case "Function":
|
|
2187
|
+
case "Function":
|
|
1771
2188
|
return {
|
|
1772
2189
|
type: "Identifier",
|
|
1773
2190
|
name: token.name
|
|
@@ -2172,6 +2589,29 @@ function buildAST(tokens) {
|
|
|
2172
2589
|
) {
|
|
2173
2590
|
const operator = tokens[current - 1].value;
|
|
2174
2591
|
|
|
2592
|
+
if (left.type === "CallExpression") {
|
|
2593
|
+
const isFunctionTarget =
|
|
2594
|
+
left.callee?.type === "Identifier" &&
|
|
2595
|
+
left.arguments.every((arg) => arg.type === "Identifier");
|
|
2596
|
+
|
|
2597
|
+
if (!isFunctionTarget) {
|
|
2598
|
+
throw new Error("Invalid function definition");
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
const right = parseAssignment();
|
|
2602
|
+
|
|
2603
|
+
return {
|
|
2604
|
+
type: "FunctionAssignmentExpression",
|
|
2605
|
+
operator,
|
|
2606
|
+
left: {
|
|
2607
|
+
type: "Identifier",
|
|
2608
|
+
name: left.callee.name
|
|
2609
|
+
},
|
|
2610
|
+
params: left.arguments.map((arg) => arg.name),
|
|
2611
|
+
right
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2175
2615
|
if (
|
|
2176
2616
|
left.type !== "Identifier" &&
|
|
2177
2617
|
left.type !== "MemberExpression" &&
|
|
@@ -2237,6 +2677,18 @@ const formatComplex = (value) => {
|
|
|
2237
2677
|
return `${real} ${sign} ${imagPart}`;
|
|
2238
2678
|
};
|
|
2239
2679
|
|
|
2680
|
+
const formatScalar = (value) => {
|
|
2681
|
+
if (typeof value !== "number") {
|
|
2682
|
+
return String(value);
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
if (Number.isInteger(value)) {
|
|
2686
|
+
return String(value);
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
return Number(value.toFixed(14)).toString();
|
|
2690
|
+
};
|
|
2691
|
+
|
|
2240
2692
|
const formatResult = (value) => {
|
|
2241
2693
|
if (isComplex(value)) {
|
|
2242
2694
|
return formatComplex(value);
|
|
@@ -2246,12 +2698,20 @@ const formatResult = (value) => {
|
|
|
2246
2698
|
return `${value.value} ${value.unit}`;
|
|
2247
2699
|
}
|
|
2248
2700
|
|
|
2701
|
+
if (isDenseMatrixWrapper(value)) {
|
|
2702
|
+
return serializeExprifyValue(value);
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2249
2705
|
if (isMatrix(value)) {
|
|
2250
|
-
return value.map((row) => row.join("\t")).join("\n");
|
|
2706
|
+
return value.map((row) => row.map(formatScalar).join("\t")).join("\n");
|
|
2251
2707
|
}
|
|
2252
2708
|
|
|
2253
2709
|
if (Array.isArray(value)) {
|
|
2254
|
-
return
|
|
2710
|
+
return JSON.stringify(value);
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
if (value && typeof value === "object") {
|
|
2714
|
+
return serializeExprifyValue(value);
|
|
2255
2715
|
}
|
|
2256
2716
|
|
|
2257
2717
|
return value;
|
|
@@ -2265,6 +2725,213 @@ class exprify {
|
|
|
2265
2725
|
this.functions = createFunctionRegistry(internalFunctions);
|
|
2266
2726
|
this.variables = createVarStore();
|
|
2267
2727
|
this._cache = new Map();
|
|
2728
|
+
this.variables.set("pi", Math.PI);
|
|
2729
|
+
this.variables.set("e", Math.E);
|
|
2730
|
+
this.addFunction("parse", (expression) => {
|
|
2731
|
+
if (typeof expression !== "string") {
|
|
2732
|
+
throw new Error("parse() expects an expression string");
|
|
2733
|
+
}
|
|
2734
|
+
return expression;
|
|
2735
|
+
});
|
|
2736
|
+
this.addFunction("leafCount", (value) => {
|
|
2737
|
+
const countLeafTokens = (expression) => {
|
|
2738
|
+
const strippedKeys = expression.replace(/(^|[{,]\s*)[a-zA-Z_][a-zA-Z0-9_]*\s*:/g, "$1");
|
|
2739
|
+
const matches = strippedKeys.match(/\d+(\.\d+)?(e[+-]?\d+)?n?|[a-zA-Z_][a-zA-Z0-9_]*/gi);
|
|
2740
|
+
return matches ? matches.length : 0;
|
|
2741
|
+
};
|
|
2742
|
+
|
|
2743
|
+
let ast = value;
|
|
2744
|
+
if (typeof value === "string") {
|
|
2745
|
+
try {
|
|
2746
|
+
ast = this.parse(value).ast;
|
|
2747
|
+
} catch {
|
|
2748
|
+
return countLeafTokens(value);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
const countLeaves = (node) => {
|
|
2753
|
+
if (!node || typeof node !== "object") return 0;
|
|
2754
|
+
|
|
2755
|
+
switch (node.type) {
|
|
2756
|
+
case "Literal":
|
|
2757
|
+
case "ImaginaryLiteral":
|
|
2758
|
+
case "UnitLiteral":
|
|
2759
|
+
case "Identifier":
|
|
2760
|
+
return 1;
|
|
2761
|
+
default:
|
|
2762
|
+
return Object.values(node).reduce((sum, child) => {
|
|
2763
|
+
if (Array.isArray(child)) {
|
|
2764
|
+
return sum + child.reduce((inner, item) => inner + countLeaves(item), 0);
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
return sum + countLeaves(child);
|
|
2768
|
+
}, 0);
|
|
2769
|
+
}
|
|
2770
|
+
};
|
|
2771
|
+
|
|
2772
|
+
return countLeaves(ast);
|
|
2773
|
+
});
|
|
2774
|
+
this.addFunction("matrix", (value) => wrapDenseMatrix(value));
|
|
2775
|
+
this.addFunction("sparse", (value) => wrapDenseMatrix(value));
|
|
2776
|
+
this.addFunction("rationalize", (expression, withDetails = false) => {
|
|
2777
|
+
if (typeof expression !== "string") {
|
|
2778
|
+
throw new Error("rationalize() expects an expression string");
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
const normalizedExpression = expression
|
|
2782
|
+
.replace(/\s+/g, "")
|
|
2783
|
+
.replace(/(\d)([a-zA-Z(])/g, "$1*$2")
|
|
2784
|
+
.replace(/([a-zA-Z)])(\d)/g, "$1*$2");
|
|
2785
|
+
|
|
2786
|
+
const polyKey = (powers) => JSON.stringify(Object.entries(powers).sort(([a], [b]) => a.localeCompare(b)));
|
|
2787
|
+
const keyToPowers = (key) => Object.fromEntries(JSON.parse(key));
|
|
2788
|
+
const constPoly = (value) => new Map([[polyKey({}), value]]);
|
|
2789
|
+
const varPoly = (name) => new Map([[polyKey({ [name]: 1 }), 1]]);
|
|
2790
|
+
const cleanPoly = (poly) => new Map([...poly.entries()].filter(([, coeff]) => coeff !== 0));
|
|
2791
|
+
const addPoly = (a, b, sign = 1) => {
|
|
2792
|
+
const result = new Map(a);
|
|
2793
|
+
for (const [key, coeff] of b.entries()) {
|
|
2794
|
+
result.set(key, (result.get(key) || 0) + (sign * coeff));
|
|
2795
|
+
}
|
|
2796
|
+
return cleanPoly(result);
|
|
2797
|
+
};
|
|
2798
|
+
const multiplyPoly = (a, b) => {
|
|
2799
|
+
const result = new Map();
|
|
2800
|
+
for (const [keyA, coeffA] of a.entries()) {
|
|
2801
|
+
const powersA = keyToPowers(keyA);
|
|
2802
|
+
for (const [keyB, coeffB] of b.entries()) {
|
|
2803
|
+
const powersB = keyToPowers(keyB);
|
|
2804
|
+
const merged = { ...powersA };
|
|
2805
|
+
for (const [name, power] of Object.entries(powersB)) {
|
|
2806
|
+
merged[name] = (merged[name] || 0) + power;
|
|
2807
|
+
}
|
|
2808
|
+
const key = polyKey(merged);
|
|
2809
|
+
result.set(key, (result.get(key) || 0) + (coeffA * coeffB));
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
return cleanPoly(result);
|
|
2813
|
+
};
|
|
2814
|
+
const powPoly = (poly, exponent) => {
|
|
2815
|
+
let result = constPoly(1);
|
|
2816
|
+
for (let index = 0; index < exponent; index++) {
|
|
2817
|
+
result = multiplyPoly(result, poly);
|
|
2818
|
+
}
|
|
2819
|
+
return result;
|
|
2820
|
+
};
|
|
2821
|
+
const rational = (num, den = constPoly(1)) => ({ num, den });
|
|
2822
|
+
const addRat = (a, b, sign = 1) => rational(
|
|
2823
|
+
addPoly(
|
|
2824
|
+
multiplyPoly(a.num, b.den),
|
|
2825
|
+
multiplyPoly(b.num, a.den),
|
|
2826
|
+
sign
|
|
2827
|
+
),
|
|
2828
|
+
multiplyPoly(a.den, b.den)
|
|
2829
|
+
);
|
|
2830
|
+
const mulRat = (a, b) => rational(multiplyPoly(a.num, b.num), multiplyPoly(a.den, b.den));
|
|
2831
|
+
const divRat = (a, b) => rational(multiplyPoly(a.num, b.den), multiplyPoly(a.den, b.num));
|
|
2832
|
+
const negRat = (value) => rational(addPoly(new Map(), value.num, -1), value.den);
|
|
2833
|
+
const astToRat = (node) => {
|
|
2834
|
+
switch (node.type) {
|
|
2835
|
+
case "Literal":
|
|
2836
|
+
return rational(constPoly(node.value));
|
|
2837
|
+
case "Identifier":
|
|
2838
|
+
return rational(varPoly(node.name));
|
|
2839
|
+
case "UnaryExpression":
|
|
2840
|
+
if (node.operator === "-") return negRat(astToRat(node.argument));
|
|
2841
|
+
throw new Error("Unsupported unary operator");
|
|
2842
|
+
case "BinaryExpression": {
|
|
2843
|
+
const left = astToRat(node.left);
|
|
2844
|
+
const right = astToRat(node.right);
|
|
2845
|
+
switch (node.operator) {
|
|
2846
|
+
case "+": return addRat(left, right);
|
|
2847
|
+
case "-": return addRat(left, right, -1);
|
|
2848
|
+
case "*": return mulRat(left, right);
|
|
2849
|
+
case "/": return divRat(left, right);
|
|
2850
|
+
case "^": {
|
|
2851
|
+
if (node.right.type !== "Literal" || !Number.isInteger(node.right.value) || node.right.value < 0) {
|
|
2852
|
+
throw new Error("Unsupported exponent");
|
|
2853
|
+
}
|
|
2854
|
+
return rational(
|
|
2855
|
+
powPoly(left.num, node.right.value),
|
|
2856
|
+
powPoly(left.den, node.right.value)
|
|
2857
|
+
);
|
|
2858
|
+
}
|
|
2859
|
+
default:
|
|
2860
|
+
throw new Error("Unsupported operator in rationalize()");
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
default:
|
|
2864
|
+
throw new Error("Unsupported expression in rationalize()");
|
|
2865
|
+
}
|
|
2866
|
+
};
|
|
2867
|
+
const formatPoly = (poly) => {
|
|
2868
|
+
const entries = [...poly.entries()]
|
|
2869
|
+
.filter(([, coeff]) => coeff !== 0)
|
|
2870
|
+
.sort(([keyA], [keyB]) => {
|
|
2871
|
+
const powersA = keyToPowers(keyA);
|
|
2872
|
+
const powersB = keyToPowers(keyB);
|
|
2873
|
+
const firstVarA = Object.keys(powersA).sort()[0] || "";
|
|
2874
|
+
const firstVarB = Object.keys(powersB).sort()[0] || "";
|
|
2875
|
+
|
|
2876
|
+
if (firstVarA !== firstVarB) {
|
|
2877
|
+
return firstVarA.localeCompare(firstVarB);
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
const degreeA = Object.values(powersA).reduce((sum, value) => sum + value, 0);
|
|
2881
|
+
const degreeB = Object.values(powersB).reduce((sum, value) => sum + value, 0);
|
|
2882
|
+
return degreeB - degreeA;
|
|
2883
|
+
});
|
|
2884
|
+
|
|
2885
|
+
if (!entries.length) return "0";
|
|
2886
|
+
|
|
2887
|
+
return entries.map(([key, coeff], index) => {
|
|
2888
|
+
const powers = keyToPowers(key);
|
|
2889
|
+
const absCoeff = Math.abs(coeff);
|
|
2890
|
+
const variablePart = Object.entries(powers)
|
|
2891
|
+
.map(([name, power]) => power === 1 ? name : `${name} ^ ${power}`)
|
|
2892
|
+
.join(" * ");
|
|
2893
|
+
let body = variablePart;
|
|
2894
|
+
|
|
2895
|
+
if (!body) {
|
|
2896
|
+
body = `${absCoeff}`;
|
|
2897
|
+
} else if (absCoeff !== 1) {
|
|
2898
|
+
body = `${absCoeff} * ${body}`;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
if (index === 0) {
|
|
2902
|
+
return coeff < 0 ? `- ${body}`.replace("- ", "-") : body;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
return coeff < 0 ? `- ${body}` : `+ ${body}`;
|
|
2906
|
+
}).join(" ");
|
|
2907
|
+
};
|
|
2908
|
+
|
|
2909
|
+
const ast = this.parse(normalizedExpression).ast;
|
|
2910
|
+
const result = astToRat(ast);
|
|
2911
|
+
const numerator = formatPoly(result.num);
|
|
2912
|
+
const denominator = formatPoly(result.den);
|
|
2913
|
+
const variableSet = new Set();
|
|
2914
|
+
|
|
2915
|
+
for (const poly of [result.num, result.den]) {
|
|
2916
|
+
for (const key of poly.keys()) {
|
|
2917
|
+
for (const name of Object.keys(keyToPowers(key))) {
|
|
2918
|
+
variableSet.add(name);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
if (!withDetails) {
|
|
2924
|
+
return `(${numerator}) / (${denominator})`;
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
return {
|
|
2928
|
+
numerator,
|
|
2929
|
+
denominator,
|
|
2930
|
+
coefficients: [],
|
|
2931
|
+
variables: [...variableSet].sort(),
|
|
2932
|
+
expression: `(${numerator}) / (${denominator})`
|
|
2933
|
+
};
|
|
2934
|
+
});
|
|
2268
2935
|
}
|
|
2269
2936
|
|
|
2270
2937
|
setVariable(name, value) {
|