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