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.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* exprify v1.0.
|
|
2
|
+
* exprify v1.0.3
|
|
3
3
|
* (c) 2026 Nirmal Paul and other contributors
|
|
4
4
|
*
|
|
5
5
|
* Released under the GPL-3.0 License
|
|
@@ -411,6 +411,60 @@
|
|
|
411
411
|
return final;
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
const isDenseMatrixWrapper = (value) =>
|
|
415
|
+
value &&
|
|
416
|
+
typeof value === "object" &&
|
|
417
|
+
value.exprify === "DenseMatrix" &&
|
|
418
|
+
"data" in value &&
|
|
419
|
+
"size" in value;
|
|
420
|
+
|
|
421
|
+
const cloneMatrixData = (value) => {
|
|
422
|
+
if (Array.isArray(value)) {
|
|
423
|
+
return value.map(cloneMatrixData);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return value;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const getMatrixSize = (data) => {
|
|
430
|
+
if (Array.isArray(data) && data.every(Array.isArray)) {
|
|
431
|
+
return [data.length, data[0]?.length || 0];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (Array.isArray(data)) {
|
|
435
|
+
return [data.length];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
throw new Error("Matrix data must be an array");
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const wrapDenseMatrix = (data) => ({
|
|
442
|
+
exprify: "DenseMatrix",
|
|
443
|
+
data: cloneMatrixData(data),
|
|
444
|
+
size: getMatrixSize(data)
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const unwrapDenseMatrix = (value) =>
|
|
448
|
+
isDenseMatrixWrapper(value) ? cloneMatrixData(value.data) : value;
|
|
449
|
+
|
|
450
|
+
const serializeExprifyValue = (value) => {
|
|
451
|
+
if (isDenseMatrixWrapper(value)) {
|
|
452
|
+
return JSON.stringify(value);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (Array.isArray(value) || (value && typeof value === "object")) {
|
|
456
|
+
return JSON.stringify(value, (_, current) => {
|
|
457
|
+
if (isDenseMatrixWrapper(current)) {
|
|
458
|
+
return current;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return current;
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return value;
|
|
466
|
+
};
|
|
467
|
+
|
|
414
468
|
function evaluateAST(node, context = {}) {
|
|
415
469
|
|
|
416
470
|
const vars = context.variables;
|
|
@@ -431,6 +485,7 @@
|
|
|
431
485
|
Array.isArray(v) && v.length > 0 && v.every(Array.isArray);
|
|
432
486
|
|
|
433
487
|
const normalizeMatrix = (value) => {
|
|
488
|
+
value = unwrapDenseMatrix(value);
|
|
434
489
|
if (isMatrix(value)) return value.map((row) => [...row]);
|
|
435
490
|
if (Array.isArray(value)) return [value];
|
|
436
491
|
throw new Error("Expected matrix-compatible value");
|
|
@@ -603,6 +658,16 @@
|
|
|
603
658
|
const simplifyComplex = (value) =>
|
|
604
659
|
value.im === 0 ? value.re : value;
|
|
605
660
|
|
|
661
|
+
const createFunctionScope = (params, args) => {
|
|
662
|
+
const scopedValues = {};
|
|
663
|
+
|
|
664
|
+
params.forEach((param, index) => {
|
|
665
|
+
scopedValues[param] = args[index];
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
return scopedValues;
|
|
669
|
+
};
|
|
670
|
+
|
|
606
671
|
const evalComplexBinary = (operator, left, right) => {
|
|
607
672
|
const a = toComplex(left);
|
|
608
673
|
const b = toComplex(right);
|
|
@@ -658,6 +723,9 @@
|
|
|
658
723
|
|
|
659
724
|
if (node.left.type === "Identifier") {
|
|
660
725
|
vars.set(node.left.name, value);
|
|
726
|
+
if (node.right.type === "ArrayExpression") {
|
|
727
|
+
return wrapDenseMatrix(unwrapDenseMatrix(value));
|
|
728
|
+
}
|
|
661
729
|
return value;
|
|
662
730
|
}
|
|
663
731
|
|
|
@@ -671,6 +739,20 @@
|
|
|
671
739
|
throw new Error("Invalid assignment target");
|
|
672
740
|
}
|
|
673
741
|
|
|
742
|
+
case "FunctionAssignmentExpression": {
|
|
743
|
+
if (node.operator !== "=") {
|
|
744
|
+
throw new Error(`Operator ${node.operator} is not supported for function definitions`);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const fn = (...args) => {
|
|
748
|
+
const scopedContext = context.withScope(createFunctionScope(node.params, args));
|
|
749
|
+
return evaluateAST(node.right, scopedContext);
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
fns.register(node.left.name, fn);
|
|
753
|
+
return fn;
|
|
754
|
+
}
|
|
755
|
+
|
|
674
756
|
/* ===== UNARY ===== */
|
|
675
757
|
case "UnaryExpression": {
|
|
676
758
|
const val = evaluateAST(node.argument, context);
|
|
@@ -691,7 +773,7 @@
|
|
|
691
773
|
let left = evaluateAST(node.left, context);
|
|
692
774
|
let right = evaluateAST(node.right, context);
|
|
693
775
|
|
|
694
|
-
//
|
|
776
|
+
// UNIT handling
|
|
695
777
|
if (isUnitObj(left) || isUnitObj(right)) {
|
|
696
778
|
|
|
697
779
|
if (!units) throw new Error("Unit system not available");
|
|
@@ -1441,6 +1523,7 @@
|
|
|
1441
1523
|
}
|
|
1442
1524
|
|
|
1443
1525
|
function validateSquareMatrix(matrix) {
|
|
1526
|
+
matrix = unwrapDenseMatrix(matrix);
|
|
1444
1527
|
if (!Array.isArray(matrix) || matrix.length === 0) {
|
|
1445
1528
|
throw new Error("det() expects a non-empty matrix");
|
|
1446
1529
|
}
|
|
@@ -1464,6 +1547,7 @@
|
|
|
1464
1547
|
}
|
|
1465
1548
|
|
|
1466
1549
|
function determinant(matrix) {
|
|
1550
|
+
matrix = unwrapDenseMatrix(matrix);
|
|
1467
1551
|
validateSquareMatrix(matrix);
|
|
1468
1552
|
|
|
1469
1553
|
if (matrix.length === 1) {
|
|
@@ -1483,6 +1567,334 @@
|
|
|
1483
1567
|
}, 0);
|
|
1484
1568
|
}
|
|
1485
1569
|
|
|
1570
|
+
function asMatrixData(value) {
|
|
1571
|
+
const data = unwrapDenseMatrix(value);
|
|
1572
|
+
if (!Array.isArray(data)) {
|
|
1573
|
+
throw new Error("Expected matrix data");
|
|
1574
|
+
}
|
|
1575
|
+
return data;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function solveLinearSystem(coefficients, constants) {
|
|
1579
|
+
const n = coefficients.length;
|
|
1580
|
+
const augmented = coefficients.map((row, rowIndex) => [...row, constants[rowIndex]]);
|
|
1581
|
+
|
|
1582
|
+
for (let pivot = 0; pivot < n; pivot++) {
|
|
1583
|
+
let maxRow = pivot;
|
|
1584
|
+
let maxValue = Math.abs(augmented[pivot][pivot]);
|
|
1585
|
+
|
|
1586
|
+
for (let row = pivot + 1; row < n; row++) {
|
|
1587
|
+
const current = Math.abs(augmented[row][pivot]);
|
|
1588
|
+
if (current > maxValue) {
|
|
1589
|
+
maxValue = current;
|
|
1590
|
+
maxRow = row;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
if (maxValue === 0) {
|
|
1595
|
+
throw new Error("Linear system is singular");
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (maxRow !== pivot) {
|
|
1599
|
+
[augmented[pivot], augmented[maxRow]] = [augmented[maxRow], augmented[pivot]];
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
const pivotValue = augmented[pivot][pivot];
|
|
1603
|
+
for (let col = pivot; col <= n; col++) {
|
|
1604
|
+
augmented[pivot][col] /= pivotValue;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
for (let row = 0; row < n; row++) {
|
|
1608
|
+
if (row === pivot) continue;
|
|
1609
|
+
const factor = augmented[row][pivot];
|
|
1610
|
+
for (let col = pivot; col <= n; col++) {
|
|
1611
|
+
augmented[row][col] -= factor * augmented[pivot][col];
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
return augmented.map((row) => row[n]);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function lupDecomposition(input) {
|
|
1620
|
+
const matrix = asMatrixData(input).map((row) => [...row]);
|
|
1621
|
+
validateSquareMatrix(matrix);
|
|
1622
|
+
|
|
1623
|
+
const n = matrix.length;
|
|
1624
|
+
const permutation = Array.from({ length: n }, (_, index) => index);
|
|
1625
|
+
|
|
1626
|
+
for (let pivot = 0; pivot < n; pivot++) {
|
|
1627
|
+
let maxRow = pivot;
|
|
1628
|
+
let maxValue = Math.abs(matrix[pivot][pivot]);
|
|
1629
|
+
|
|
1630
|
+
for (let row = pivot + 1; row < n; row++) {
|
|
1631
|
+
const current = Math.abs(matrix[row][pivot]);
|
|
1632
|
+
if (current > maxValue) {
|
|
1633
|
+
maxValue = current;
|
|
1634
|
+
maxRow = row;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (maxValue === 0) {
|
|
1639
|
+
throw new Error("Matrix is singular");
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (maxRow !== pivot) {
|
|
1643
|
+
[matrix[pivot], matrix[maxRow]] = [matrix[maxRow], matrix[pivot]];
|
|
1644
|
+
[permutation[pivot], permutation[maxRow]] = [permutation[maxRow], permutation[pivot]];
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
for (let row = pivot + 1; row < n; row++) {
|
|
1648
|
+
matrix[row][pivot] /= matrix[pivot][pivot];
|
|
1649
|
+
for (let col = pivot + 1; col < n; col++) {
|
|
1650
|
+
matrix[row][col] -= matrix[row][pivot] * matrix[pivot][col];
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
const L = matrix.map((row, rowIndex) =>
|
|
1656
|
+
row.map((value, colIndex) => {
|
|
1657
|
+
if (rowIndex === colIndex) return 1;
|
|
1658
|
+
if (rowIndex > colIndex) return value;
|
|
1659
|
+
return 0;
|
|
1660
|
+
})
|
|
1661
|
+
);
|
|
1662
|
+
|
|
1663
|
+
const U = matrix.map((row, rowIndex) =>
|
|
1664
|
+
row.map((value, colIndex) => (rowIndex <= colIndex ? value : 0))
|
|
1665
|
+
);
|
|
1666
|
+
|
|
1667
|
+
return {
|
|
1668
|
+
L: wrapDenseMatrix(L),
|
|
1669
|
+
U: wrapDenseMatrix(U),
|
|
1670
|
+
p: permutation
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function linearSolve(aInput, bInput) {
|
|
1675
|
+
const { L, U, p } = lupDecomposition(aInput);
|
|
1676
|
+
const a = asMatrixData(aInput);
|
|
1677
|
+
const bData = asMatrixData(bInput);
|
|
1678
|
+
const bVector = Array.isArray(bData[0]) ? bData.map((row) => row[0]) : bData;
|
|
1679
|
+
|
|
1680
|
+
if (a.length !== bVector.length) {
|
|
1681
|
+
throw new Error("Right-hand side dimension mismatch");
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const permutedB = p.map((index) => bVector[index]);
|
|
1685
|
+
const y = new Array(a.length).fill(0);
|
|
1686
|
+
|
|
1687
|
+
for (let row = 0; row < a.length; row++) {
|
|
1688
|
+
y[row] = permutedB[row];
|
|
1689
|
+
for (let col = 0; col < row; col++) {
|
|
1690
|
+
y[row] -= L.data[row][col] * y[col];
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
const x = new Array(a.length).fill(0);
|
|
1695
|
+
for (let row = a.length - 1; row >= 0; row--) {
|
|
1696
|
+
x[row] = y[row];
|
|
1697
|
+
for (let col = row + 1; col < a.length; col++) {
|
|
1698
|
+
x[row] -= U.data[row][col] * x[col];
|
|
1699
|
+
}
|
|
1700
|
+
x[row] /= U.data[row][row];
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
return wrapDenseMatrix(x.map((value) => [value]));
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function solveLyapunov(aInput, qInput) {
|
|
1707
|
+
const A = asMatrixData(aInput).map((row) => [...row]);
|
|
1708
|
+
const Q = asMatrixData(qInput).map((row) => [...row]);
|
|
1709
|
+
validateSquareMatrix(A);
|
|
1710
|
+
validateSquareMatrix(Q);
|
|
1711
|
+
|
|
1712
|
+
const n = A.length;
|
|
1713
|
+
if (Q.length !== n) {
|
|
1714
|
+
throw new Error("A and Q must have the same dimensions");
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
const coefficients = [];
|
|
1718
|
+
const constants = [];
|
|
1719
|
+
|
|
1720
|
+
for (let row = 0; row < n; row++) {
|
|
1721
|
+
for (let col = 0; col < n; col++) {
|
|
1722
|
+
const equation = new Array(n * n).fill(0);
|
|
1723
|
+
|
|
1724
|
+
for (let k = 0; k < n; k++) {
|
|
1725
|
+
equation[k * n + col] += A[row][k];
|
|
1726
|
+
equation[row * n + k] += A[col][k];
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
coefficients.push(equation);
|
|
1730
|
+
constants.push(-Q[row][col]);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const solution = solveLinearSystem(coefficients, constants);
|
|
1735
|
+
const X = [];
|
|
1736
|
+
|
|
1737
|
+
for (let row = 0; row < n; row++) {
|
|
1738
|
+
X.push(solution.slice(row * n, (row + 1) * n));
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
return wrapDenseMatrix(X);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function evaluatePolynomial(coefficients, x) {
|
|
1745
|
+
return coefficients.reduce((sum, coefficient, index) => sum + (coefficient * (x ** index)), 0);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function syntheticDivide(coefficients, root) {
|
|
1749
|
+
const descending = [...coefficients].reverse();
|
|
1750
|
+
const quotient = [descending[0]];
|
|
1751
|
+
|
|
1752
|
+
for (let index = 1; index < descending.length - 1; index++) {
|
|
1753
|
+
quotient.push(descending[index] + (quotient[index - 1] * root));
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
const remainder = descending[descending.length - 1] + (quotient[quotient.length - 1] * root);
|
|
1757
|
+
return {
|
|
1758
|
+
quotient: quotient.reverse(),
|
|
1759
|
+
remainder
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
function solveQuadratic(coefficients) {
|
|
1764
|
+
const [c, b, a] = coefficients;
|
|
1765
|
+
const discriminant = (b ** 2) - (4 * a * c);
|
|
1766
|
+
if (discriminant < 0) {
|
|
1767
|
+
throw new Error("Only real roots are supported");
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const sqrtDisc = Math.sqrt(discriminant);
|
|
1771
|
+
return [
|
|
1772
|
+
(-b + sqrtDisc) / (2 * a),
|
|
1773
|
+
(-b - sqrtDisc) / (2 * a)
|
|
1774
|
+
];
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function polynomialRoots(...coefficients) {
|
|
1778
|
+
while (coefficients.length > 1 && coefficients[coefficients.length - 1] === 0) {
|
|
1779
|
+
coefficients.pop();
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
const degree = coefficients.length - 1;
|
|
1783
|
+
if (degree < 1) {
|
|
1784
|
+
throw new Error("polynomialRoot() expects at least a linear polynomial");
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
if (degree === 1) {
|
|
1788
|
+
const [b, a] = coefficients;
|
|
1789
|
+
return [-b / a];
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
if (degree === 2) {
|
|
1793
|
+
return solveQuadratic(coefficients);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
if (degree === 3) {
|
|
1797
|
+
const constant = coefficients[0];
|
|
1798
|
+
coefficients[3];
|
|
1799
|
+
const candidates = [];
|
|
1800
|
+
const limit = Math.abs(constant);
|
|
1801
|
+
|
|
1802
|
+
for (let divisor = 1; divisor <= Math.max(1, limit); divisor++) {
|
|
1803
|
+
if (limit % divisor === 0) {
|
|
1804
|
+
candidates.push(divisor, -divisor);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
for (const candidate of candidates) {
|
|
1809
|
+
if (evaluatePolynomial(coefficients, candidate) === 0) {
|
|
1810
|
+
const reduced = syntheticDivide(coefficients, candidate);
|
|
1811
|
+
const remainingRoots = solveQuadratic(reduced.quotient);
|
|
1812
|
+
return [candidate, ...remainingRoots];
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
throw new Error("polynomialRoot() currently supports degree up to 3");
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function dotProduct(a, b) {
|
|
1821
|
+
return a.reduce((sum, value, index) => sum + (value * b[index]), 0);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function vectorNorm(vector) {
|
|
1825
|
+
return Math.sqrt(dotProduct(vector, vector));
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function scaleVector(vector, scalar) {
|
|
1829
|
+
return vector.map((value) => value * scalar);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function subtractVectors(a, b) {
|
|
1833
|
+
return a.map((value, index) => value - b[index]);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function transpose(matrix) {
|
|
1837
|
+
return matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex]));
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function qrDecomposition(input) {
|
|
1841
|
+
const A = asMatrixData(input).map((row) => [...row]);
|
|
1842
|
+
if (!A.length || !A.every((row) => row.length === A[0].length)) {
|
|
1843
|
+
throw new Error("qr() expects a rectangular matrix");
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
const rowCount = A.length;
|
|
1847
|
+
const colCount = A[0].length;
|
|
1848
|
+
const columns = transpose(A);
|
|
1849
|
+
const qColumns = [];
|
|
1850
|
+
|
|
1851
|
+
for (let col = 0; col < colCount; col++) {
|
|
1852
|
+
let vector = [...columns[col]];
|
|
1853
|
+
|
|
1854
|
+
for (let existing = 0; existing < qColumns.length; existing++) {
|
|
1855
|
+
const projection = dotProduct(qColumns[existing], columns[col]);
|
|
1856
|
+
vector = subtractVectors(vector, scaleVector(qColumns[existing], projection));
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
const norm = vectorNorm(vector);
|
|
1860
|
+
if (norm === 0) {
|
|
1861
|
+
throw new Error("qr() requires linearly independent columns");
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
qColumns.push(scaleVector(vector, 1 / norm));
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
for (let basisIndex = 0; qColumns.length < rowCount && basisIndex < rowCount; basisIndex++) {
|
|
1868
|
+
let candidate = Array.from({ length: rowCount }, (_, index) => (index === basisIndex ? 1 : 0));
|
|
1869
|
+
|
|
1870
|
+
for (const column of qColumns) {
|
|
1871
|
+
const projection = dotProduct(column, candidate);
|
|
1872
|
+
candidate = subtractVectors(candidate, scaleVector(column, projection));
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
const norm = vectorNorm(candidate);
|
|
1876
|
+
if (norm > 1e-10) {
|
|
1877
|
+
qColumns.push(scaleVector(candidate, 1 / norm));
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
const Q = Array.from({ length: rowCount }, (_, rowIndex) =>
|
|
1882
|
+
qColumns.map((column) => column[rowIndex])
|
|
1883
|
+
);
|
|
1884
|
+
|
|
1885
|
+
const fullR = Array.from({ length: rowCount }, () => Array(colCount).fill(0));
|
|
1886
|
+
for (let row = 0; row < rowCount; row++) {
|
|
1887
|
+
for (let col = 0; col < colCount; col++) {
|
|
1888
|
+
fullR[row][col] = dotProduct(qColumns[row], columns[col]);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
return {
|
|
1893
|
+
Q: wrapDenseMatrix(Q),
|
|
1894
|
+
R: wrapDenseMatrix(fullR)
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1486
1898
|
function splitTerms(expression) {
|
|
1487
1899
|
const normalized = expression.replace(/\s+/g, "");
|
|
1488
1900
|
if (!normalized) {
|
|
@@ -1619,6 +2031,11 @@
|
|
|
1619
2031
|
|
|
1620
2032
|
pow: (a, b) => a ** b,
|
|
1621
2033
|
det: (matrix) => determinant(matrix),
|
|
2034
|
+
polynomialRoot: (...coefficients) => polynomialRoots(...coefficients),
|
|
2035
|
+
lsolve: (a, b) => linearSolve(a, b),
|
|
2036
|
+
lup: (matrix) => lupDecomposition(matrix),
|
|
2037
|
+
lyap: (a, q) => solveLyapunov(a, q),
|
|
2038
|
+
qr: (matrix) => qrDecomposition(matrix),
|
|
1622
2039
|
simplify: (expression) => {
|
|
1623
2040
|
if (typeof expression !== "string") {
|
|
1624
2041
|
throw new Error("simplify() expects an expression string");
|
|
@@ -1778,7 +2195,7 @@
|
|
|
1778
2195
|
case "Identifier":
|
|
1779
2196
|
return { type: "Identifier", name: token.name };
|
|
1780
2197
|
|
|
1781
|
-
case "Function":
|
|
2198
|
+
case "Function":
|
|
1782
2199
|
return {
|
|
1783
2200
|
type: "Identifier",
|
|
1784
2201
|
name: token.name
|
|
@@ -2183,6 +2600,29 @@
|
|
|
2183
2600
|
) {
|
|
2184
2601
|
const operator = tokens[current - 1].value;
|
|
2185
2602
|
|
|
2603
|
+
if (left.type === "CallExpression") {
|
|
2604
|
+
const isFunctionTarget =
|
|
2605
|
+
left.callee?.type === "Identifier" &&
|
|
2606
|
+
left.arguments.every((arg) => arg.type === "Identifier");
|
|
2607
|
+
|
|
2608
|
+
if (!isFunctionTarget) {
|
|
2609
|
+
throw new Error("Invalid function definition");
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
const right = parseAssignment();
|
|
2613
|
+
|
|
2614
|
+
return {
|
|
2615
|
+
type: "FunctionAssignmentExpression",
|
|
2616
|
+
operator,
|
|
2617
|
+
left: {
|
|
2618
|
+
type: "Identifier",
|
|
2619
|
+
name: left.callee.name
|
|
2620
|
+
},
|
|
2621
|
+
params: left.arguments.map((arg) => arg.name),
|
|
2622
|
+
right
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2186
2626
|
if (
|
|
2187
2627
|
left.type !== "Identifier" &&
|
|
2188
2628
|
left.type !== "MemberExpression" &&
|
|
@@ -2248,6 +2688,18 @@
|
|
|
2248
2688
|
return `${real} ${sign} ${imagPart}`;
|
|
2249
2689
|
};
|
|
2250
2690
|
|
|
2691
|
+
const formatScalar = (value) => {
|
|
2692
|
+
if (typeof value !== "number") {
|
|
2693
|
+
return String(value);
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
if (Number.isInteger(value)) {
|
|
2697
|
+
return String(value);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
return Number(value.toFixed(14)).toString();
|
|
2701
|
+
};
|
|
2702
|
+
|
|
2251
2703
|
const formatResult = (value) => {
|
|
2252
2704
|
if (isComplex(value)) {
|
|
2253
2705
|
return formatComplex(value);
|
|
@@ -2257,12 +2709,20 @@
|
|
|
2257
2709
|
return `${value.value} ${value.unit}`;
|
|
2258
2710
|
}
|
|
2259
2711
|
|
|
2712
|
+
if (isDenseMatrixWrapper(value)) {
|
|
2713
|
+
return serializeExprifyValue(value);
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2260
2716
|
if (isMatrix(value)) {
|
|
2261
|
-
return value.map((row) => row.join("\t")).join("\n");
|
|
2717
|
+
return value.map((row) => row.map(formatScalar).join("\t")).join("\n");
|
|
2262
2718
|
}
|
|
2263
2719
|
|
|
2264
2720
|
if (Array.isArray(value)) {
|
|
2265
|
-
return
|
|
2721
|
+
return JSON.stringify(value);
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
if (value && typeof value === "object") {
|
|
2725
|
+
return serializeExprifyValue(value);
|
|
2266
2726
|
}
|
|
2267
2727
|
|
|
2268
2728
|
return value;
|
|
@@ -2276,6 +2736,213 @@
|
|
|
2276
2736
|
this.functions = createFunctionRegistry(internalFunctions);
|
|
2277
2737
|
this.variables = createVarStore();
|
|
2278
2738
|
this._cache = new Map();
|
|
2739
|
+
this.variables.set("pi", Math.PI);
|
|
2740
|
+
this.variables.set("e", Math.E);
|
|
2741
|
+
this.addFunction("parse", (expression) => {
|
|
2742
|
+
if (typeof expression !== "string") {
|
|
2743
|
+
throw new Error("parse() expects an expression string");
|
|
2744
|
+
}
|
|
2745
|
+
return expression;
|
|
2746
|
+
});
|
|
2747
|
+
this.addFunction("leafCount", (value) => {
|
|
2748
|
+
const countLeafTokens = (expression) => {
|
|
2749
|
+
const strippedKeys = expression.replace(/(^|[{,]\s*)[a-zA-Z_][a-zA-Z0-9_]*\s*:/g, "$1");
|
|
2750
|
+
const matches = strippedKeys.match(/\d+(\.\d+)?(e[+-]?\d+)?n?|[a-zA-Z_][a-zA-Z0-9_]*/gi);
|
|
2751
|
+
return matches ? matches.length : 0;
|
|
2752
|
+
};
|
|
2753
|
+
|
|
2754
|
+
let ast = value;
|
|
2755
|
+
if (typeof value === "string") {
|
|
2756
|
+
try {
|
|
2757
|
+
ast = this.parse(value).ast;
|
|
2758
|
+
} catch {
|
|
2759
|
+
return countLeafTokens(value);
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
const countLeaves = (node) => {
|
|
2764
|
+
if (!node || typeof node !== "object") return 0;
|
|
2765
|
+
|
|
2766
|
+
switch (node.type) {
|
|
2767
|
+
case "Literal":
|
|
2768
|
+
case "ImaginaryLiteral":
|
|
2769
|
+
case "UnitLiteral":
|
|
2770
|
+
case "Identifier":
|
|
2771
|
+
return 1;
|
|
2772
|
+
default:
|
|
2773
|
+
return Object.values(node).reduce((sum, child) => {
|
|
2774
|
+
if (Array.isArray(child)) {
|
|
2775
|
+
return sum + child.reduce((inner, item) => inner + countLeaves(item), 0);
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
return sum + countLeaves(child);
|
|
2779
|
+
}, 0);
|
|
2780
|
+
}
|
|
2781
|
+
};
|
|
2782
|
+
|
|
2783
|
+
return countLeaves(ast);
|
|
2784
|
+
});
|
|
2785
|
+
this.addFunction("matrix", (value) => wrapDenseMatrix(value));
|
|
2786
|
+
this.addFunction("sparse", (value) => wrapDenseMatrix(value));
|
|
2787
|
+
this.addFunction("rationalize", (expression, withDetails = false) => {
|
|
2788
|
+
if (typeof expression !== "string") {
|
|
2789
|
+
throw new Error("rationalize() expects an expression string");
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
const normalizedExpression = expression
|
|
2793
|
+
.replace(/\s+/g, "")
|
|
2794
|
+
.replace(/(\d)([a-zA-Z(])/g, "$1*$2")
|
|
2795
|
+
.replace(/([a-zA-Z)])(\d)/g, "$1*$2");
|
|
2796
|
+
|
|
2797
|
+
const polyKey = (powers) => JSON.stringify(Object.entries(powers).sort(([a], [b]) => a.localeCompare(b)));
|
|
2798
|
+
const keyToPowers = (key) => Object.fromEntries(JSON.parse(key));
|
|
2799
|
+
const constPoly = (value) => new Map([[polyKey({}), value]]);
|
|
2800
|
+
const varPoly = (name) => new Map([[polyKey({ [name]: 1 }), 1]]);
|
|
2801
|
+
const cleanPoly = (poly) => new Map([...poly.entries()].filter(([, coeff]) => coeff !== 0));
|
|
2802
|
+
const addPoly = (a, b, sign = 1) => {
|
|
2803
|
+
const result = new Map(a);
|
|
2804
|
+
for (const [key, coeff] of b.entries()) {
|
|
2805
|
+
result.set(key, (result.get(key) || 0) + (sign * coeff));
|
|
2806
|
+
}
|
|
2807
|
+
return cleanPoly(result);
|
|
2808
|
+
};
|
|
2809
|
+
const multiplyPoly = (a, b) => {
|
|
2810
|
+
const result = new Map();
|
|
2811
|
+
for (const [keyA, coeffA] of a.entries()) {
|
|
2812
|
+
const powersA = keyToPowers(keyA);
|
|
2813
|
+
for (const [keyB, coeffB] of b.entries()) {
|
|
2814
|
+
const powersB = keyToPowers(keyB);
|
|
2815
|
+
const merged = { ...powersA };
|
|
2816
|
+
for (const [name, power] of Object.entries(powersB)) {
|
|
2817
|
+
merged[name] = (merged[name] || 0) + power;
|
|
2818
|
+
}
|
|
2819
|
+
const key = polyKey(merged);
|
|
2820
|
+
result.set(key, (result.get(key) || 0) + (coeffA * coeffB));
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
return cleanPoly(result);
|
|
2824
|
+
};
|
|
2825
|
+
const powPoly = (poly, exponent) => {
|
|
2826
|
+
let result = constPoly(1);
|
|
2827
|
+
for (let index = 0; index < exponent; index++) {
|
|
2828
|
+
result = multiplyPoly(result, poly);
|
|
2829
|
+
}
|
|
2830
|
+
return result;
|
|
2831
|
+
};
|
|
2832
|
+
const rational = (num, den = constPoly(1)) => ({ num, den });
|
|
2833
|
+
const addRat = (a, b, sign = 1) => rational(
|
|
2834
|
+
addPoly(
|
|
2835
|
+
multiplyPoly(a.num, b.den),
|
|
2836
|
+
multiplyPoly(b.num, a.den),
|
|
2837
|
+
sign
|
|
2838
|
+
),
|
|
2839
|
+
multiplyPoly(a.den, b.den)
|
|
2840
|
+
);
|
|
2841
|
+
const mulRat = (a, b) => rational(multiplyPoly(a.num, b.num), multiplyPoly(a.den, b.den));
|
|
2842
|
+
const divRat = (a, b) => rational(multiplyPoly(a.num, b.den), multiplyPoly(a.den, b.num));
|
|
2843
|
+
const negRat = (value) => rational(addPoly(new Map(), value.num, -1), value.den);
|
|
2844
|
+
const astToRat = (node) => {
|
|
2845
|
+
switch (node.type) {
|
|
2846
|
+
case "Literal":
|
|
2847
|
+
return rational(constPoly(node.value));
|
|
2848
|
+
case "Identifier":
|
|
2849
|
+
return rational(varPoly(node.name));
|
|
2850
|
+
case "UnaryExpression":
|
|
2851
|
+
if (node.operator === "-") return negRat(astToRat(node.argument));
|
|
2852
|
+
throw new Error("Unsupported unary operator");
|
|
2853
|
+
case "BinaryExpression": {
|
|
2854
|
+
const left = astToRat(node.left);
|
|
2855
|
+
const right = astToRat(node.right);
|
|
2856
|
+
switch (node.operator) {
|
|
2857
|
+
case "+": return addRat(left, right);
|
|
2858
|
+
case "-": return addRat(left, right, -1);
|
|
2859
|
+
case "*": return mulRat(left, right);
|
|
2860
|
+
case "/": return divRat(left, right);
|
|
2861
|
+
case "^": {
|
|
2862
|
+
if (node.right.type !== "Literal" || !Number.isInteger(node.right.value) || node.right.value < 0) {
|
|
2863
|
+
throw new Error("Unsupported exponent");
|
|
2864
|
+
}
|
|
2865
|
+
return rational(
|
|
2866
|
+
powPoly(left.num, node.right.value),
|
|
2867
|
+
powPoly(left.den, node.right.value)
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
default:
|
|
2871
|
+
throw new Error("Unsupported operator in rationalize()");
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
default:
|
|
2875
|
+
throw new Error("Unsupported expression in rationalize()");
|
|
2876
|
+
}
|
|
2877
|
+
};
|
|
2878
|
+
const formatPoly = (poly) => {
|
|
2879
|
+
const entries = [...poly.entries()]
|
|
2880
|
+
.filter(([, coeff]) => coeff !== 0)
|
|
2881
|
+
.sort(([keyA], [keyB]) => {
|
|
2882
|
+
const powersA = keyToPowers(keyA);
|
|
2883
|
+
const powersB = keyToPowers(keyB);
|
|
2884
|
+
const firstVarA = Object.keys(powersA).sort()[0] || "";
|
|
2885
|
+
const firstVarB = Object.keys(powersB).sort()[0] || "";
|
|
2886
|
+
|
|
2887
|
+
if (firstVarA !== firstVarB) {
|
|
2888
|
+
return firstVarA.localeCompare(firstVarB);
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
const degreeA = Object.values(powersA).reduce((sum, value) => sum + value, 0);
|
|
2892
|
+
const degreeB = Object.values(powersB).reduce((sum, value) => sum + value, 0);
|
|
2893
|
+
return degreeB - degreeA;
|
|
2894
|
+
});
|
|
2895
|
+
|
|
2896
|
+
if (!entries.length) return "0";
|
|
2897
|
+
|
|
2898
|
+
return entries.map(([key, coeff], index) => {
|
|
2899
|
+
const powers = keyToPowers(key);
|
|
2900
|
+
const absCoeff = Math.abs(coeff);
|
|
2901
|
+
const variablePart = Object.entries(powers)
|
|
2902
|
+
.map(([name, power]) => power === 1 ? name : `${name} ^ ${power}`)
|
|
2903
|
+
.join(" * ");
|
|
2904
|
+
let body = variablePart;
|
|
2905
|
+
|
|
2906
|
+
if (!body) {
|
|
2907
|
+
body = `${absCoeff}`;
|
|
2908
|
+
} else if (absCoeff !== 1) {
|
|
2909
|
+
body = `${absCoeff} * ${body}`;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
if (index === 0) {
|
|
2913
|
+
return coeff < 0 ? `- ${body}`.replace("- ", "-") : body;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
return coeff < 0 ? `- ${body}` : `+ ${body}`;
|
|
2917
|
+
}).join(" ");
|
|
2918
|
+
};
|
|
2919
|
+
|
|
2920
|
+
const ast = this.parse(normalizedExpression).ast;
|
|
2921
|
+
const result = astToRat(ast);
|
|
2922
|
+
const numerator = formatPoly(result.num);
|
|
2923
|
+
const denominator = formatPoly(result.den);
|
|
2924
|
+
const variableSet = new Set();
|
|
2925
|
+
|
|
2926
|
+
for (const poly of [result.num, result.den]) {
|
|
2927
|
+
for (const key of poly.keys()) {
|
|
2928
|
+
for (const name of Object.keys(keyToPowers(key))) {
|
|
2929
|
+
variableSet.add(name);
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
if (!withDetails) {
|
|
2935
|
+
return `(${numerator}) / (${denominator})`;
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
return {
|
|
2939
|
+
numerator,
|
|
2940
|
+
denominator,
|
|
2941
|
+
coefficients: [],
|
|
2942
|
+
variables: [...variableSet].sort(),
|
|
2943
|
+
expression: `(${numerator}) / (${denominator})`
|
|
2944
|
+
};
|
|
2945
|
+
});
|
|
2279
2946
|
}
|
|
2280
2947
|
|
|
2281
2948
|
setVariable(name, value) {
|