@geotechcli/core 0.4.107 → 0.4.108
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/dist/fem/engineering-evidence.d.ts +65 -0
- package/dist/fem/engineering-evidence.d.ts.map +1 -1
- package/dist/fem/engineering-evidence.js +287 -9
- package/dist/fem/engineering-evidence.js.map +1 -1
- package/dist/fem/index.d.ts +3 -2
- package/dist/fem/index.d.ts.map +1 -1
- package/dist/fem/index.js +2 -1
- package/dist/fem/index.js.map +1 -1
- package/dist/fem/plane-strain-assembly.d.ts +35 -4
- package/dist/fem/plane-strain-assembly.d.ts.map +1 -1
- package/dist/fem/plane-strain-assembly.js +160 -10
- package/dist/fem/plane-strain-assembly.js.map +1 -1
- package/dist/fem/production-readiness.d.ts.map +1 -1
- package/dist/fem/production-readiness.js +6 -1
- package/dist/fem/production-readiness.js.map +1 -1
- package/dist/fem/routing.js +4 -4
- package/dist/fem/routing.js.map +1 -1
- package/dist/fem/sparse-linear-algebra.d.ts +47 -0
- package/dist/fem/sparse-linear-algebra.d.ts.map +1 -0
- package/dist/fem/sparse-linear-algebra.js +290 -0
- package/dist/fem/sparse-linear-algebra.js.map +1 -0
- package/dist/meta/metadata.json +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_FEM_CONVERGENCE_POLICY } from './engineering-evidence.js';
|
|
2
|
+
import { buildCsrFromTriplets, solveCsrConjugateGradient, } from './sparse-linear-algebra.js';
|
|
2
3
|
const GAUSS_POINTS = [
|
|
3
4
|
[-1 / Math.sqrt(3), -1 / Math.sqrt(3), 1],
|
|
4
5
|
[1 / Math.sqrt(3), -1 / Math.sqrt(3), 1],
|
|
@@ -6,6 +7,7 @@ const GAUSS_POINTS = [
|
|
|
6
7
|
[-1 / Math.sqrt(3), 1 / Math.sqrt(3), 1],
|
|
7
8
|
];
|
|
8
9
|
const MAX_DENSE_DOF_COUNT = 800;
|
|
10
|
+
const MAX_SPARSE_EXPERIMENTAL_DOF_COUNT = 5_000;
|
|
9
11
|
const MAX_BIOT_TIME_STEP_GROWTH_RATIO = 8;
|
|
10
12
|
function assertFinite(value, label) {
|
|
11
13
|
if (!Number.isFinite(value))
|
|
@@ -284,6 +286,9 @@ function matVec(matrix, vector) {
|
|
|
284
286
|
function dot(a, b) {
|
|
285
287
|
return a.reduce((sum, value, index) => sum + value * b[index], 0);
|
|
286
288
|
}
|
|
289
|
+
function vectorNorm(values) {
|
|
290
|
+
return Math.sqrt(values.reduce((sum, value) => sum + value * value, 0));
|
|
291
|
+
}
|
|
287
292
|
function elementMatrices(input) {
|
|
288
293
|
const { nodes, material, thicknessM } = input;
|
|
289
294
|
const d = planeStrainD(material);
|
|
@@ -1344,7 +1349,7 @@ export function runPlaneStrainQuad4Assembly(model) {
|
|
|
1344
1349
|
policy,
|
|
1345
1350
|
};
|
|
1346
1351
|
}
|
|
1347
|
-
function assemblePlaneStrainSystem(model) {
|
|
1352
|
+
function assemblePlaneStrainSystem(model, options = {}) {
|
|
1348
1353
|
if (model.schemaVersion !== 'fem-plane-strain-model.v1') {
|
|
1349
1354
|
throw new Error('Only fem-plane-strain-model.v1 is supported.');
|
|
1350
1355
|
}
|
|
@@ -1370,10 +1375,17 @@ function assemblePlaneStrainSystem(model) {
|
|
|
1370
1375
|
const nodeIndexById = new Map(model.nodes.map((node, index) => [node.id, index]));
|
|
1371
1376
|
const materialById = new Map(model.materials.map((material) => [material.id, material]));
|
|
1372
1377
|
const dofCount = model.nodes.length * 2;
|
|
1373
|
-
|
|
1378
|
+
const storage = options.storage ?? 'dense-and-triplets';
|
|
1379
|
+
if (storage === 'dense-and-triplets' && dofCount > MAX_DENSE_DOF_COUNT) {
|
|
1374
1380
|
throw new Error(`Plane-strain dense assembly is capped at ${MAX_DENSE_DOF_COUNT} DOFs for benchmark-scale evidence runs.`);
|
|
1375
1381
|
}
|
|
1376
|
-
|
|
1382
|
+
if (storage === 'triplets-only' && dofCount > MAX_SPARSE_EXPERIMENTAL_DOF_COUNT) {
|
|
1383
|
+
throw new Error(`Plane-strain sparse experimental assembly is capped at ${MAX_SPARSE_EXPERIMENTAL_DOF_COUNT} DOFs until production solver benchmarks are approved.`);
|
|
1384
|
+
}
|
|
1385
|
+
const stiffness = storage === 'dense-and-triplets'
|
|
1386
|
+
? Array.from({ length: dofCount }, () => new Array(dofCount).fill(0))
|
|
1387
|
+
: undefined;
|
|
1388
|
+
const stiffnessTriplets = [];
|
|
1377
1389
|
const loads = new Array(dofCount).fill(0);
|
|
1378
1390
|
for (const node of model.nodes) {
|
|
1379
1391
|
assertFinite(node.xM, `node ${node.id} xM`);
|
|
@@ -1422,7 +1434,12 @@ function assemblePlaneStrainSystem(model) {
|
|
|
1422
1434
|
const globalDofs = nodeIndices.flatMap((index) => [dofIndex(index, 'ux'), dofIndex(index, 'uy')]);
|
|
1423
1435
|
for (let localRow = 0; localRow < 8; localRow += 1) {
|
|
1424
1436
|
for (let localCol = 0; localCol < 8; localCol += 1) {
|
|
1425
|
-
|
|
1437
|
+
const row = globalDofs[localRow];
|
|
1438
|
+
const col = globalDofs[localCol];
|
|
1439
|
+
const value = elementData.stiffness[localRow][localCol];
|
|
1440
|
+
stiffnessTriplets.push({ row, col, value });
|
|
1441
|
+
if (stiffness)
|
|
1442
|
+
stiffness[row][col] += value;
|
|
1426
1443
|
}
|
|
1427
1444
|
}
|
|
1428
1445
|
elementGaussCache.push({
|
|
@@ -1463,6 +1480,7 @@ function assemblePlaneStrainSystem(model) {
|
|
|
1463
1480
|
policy,
|
|
1464
1481
|
materialById,
|
|
1465
1482
|
stiffness,
|
|
1483
|
+
stiffnessTriplets,
|
|
1466
1484
|
loads,
|
|
1467
1485
|
prescribed,
|
|
1468
1486
|
freeDofs,
|
|
@@ -1596,19 +1614,115 @@ function druckerPragerResidualHistoryEntry(iteration, evaluation, policy) {
|
|
|
1596
1614
|
converged: isDruckerPragerStepConverged(evaluation, policy),
|
|
1597
1615
|
};
|
|
1598
1616
|
}
|
|
1599
|
-
function druckerPragerTerminationReason(evaluation, policy, converged, iterations) {
|
|
1617
|
+
function druckerPragerTerminationReason(evaluation, policy, converged, iterations, linearSolverFailure) {
|
|
1600
1618
|
if (converged)
|
|
1601
1619
|
return 'converged';
|
|
1620
|
+
if (linearSolverFailure)
|
|
1621
|
+
return 'linear_solver_nonconverged';
|
|
1602
1622
|
if (iterations >= policy.maxIterations)
|
|
1603
1623
|
return 'max_iterations';
|
|
1604
1624
|
if (evaluation.residualNormRatio > policy.forceBalanceTolerance)
|
|
1605
1625
|
return 'force_residual_exceeded';
|
|
1606
1626
|
return 'yield_residual_exceeded';
|
|
1607
1627
|
}
|
|
1628
|
+
function normalizeLinearSolverKind(solver) {
|
|
1629
|
+
const resolved = solver ?? 'dense-gaussian';
|
|
1630
|
+
if (resolved !== 'dense-gaussian' && resolved !== 'sparse-csr-cg') {
|
|
1631
|
+
throw new Error('linearSolver must be dense-gaussian or sparse-csr-cg.');
|
|
1632
|
+
}
|
|
1633
|
+
return resolved;
|
|
1634
|
+
}
|
|
1635
|
+
function denseNonzeroCount(matrix) {
|
|
1636
|
+
return matrix.reduce((count, row) => count + row.filter((value) => Math.abs(value) > 0).length, 0);
|
|
1637
|
+
}
|
|
1638
|
+
function buildReducedCsr(input) {
|
|
1639
|
+
const freeIndexByDof = new Map(input.freeDofs.map((dof, index) => [dof, index]));
|
|
1640
|
+
const reducedTriplets = input.triplets.flatMap((entry) => {
|
|
1641
|
+
const row = freeIndexByDof.get(entry.row);
|
|
1642
|
+
const col = freeIndexByDof.get(entry.col);
|
|
1643
|
+
return row != null && col != null ? [{ row, col, value: entry.value }] : [];
|
|
1644
|
+
});
|
|
1645
|
+
return buildCsrFromTriplets({
|
|
1646
|
+
rowCount: input.freeDofs.length,
|
|
1647
|
+
colCount: input.freeDofs.length,
|
|
1648
|
+
triplets: reducedTriplets,
|
|
1649
|
+
dropTolerance: 0,
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
function solveDenseDruckerPragerCorrection(input) {
|
|
1653
|
+
const correction = solveDenseLinearSystem(input.reducedK, [...input.rhs]);
|
|
1654
|
+
const solvedRhs = matVec(input.reducedK, correction);
|
|
1655
|
+
const residual = solvedRhs.map((value, index) => value - input.rhs[index]);
|
|
1656
|
+
const rhsNorm = Math.max(vectorNorm(input.rhs), 1);
|
|
1657
|
+
const finalResidualNorm = vectorNorm(residual);
|
|
1658
|
+
const correctionNorm = vectorNorm(correction);
|
|
1659
|
+
return {
|
|
1660
|
+
correction,
|
|
1661
|
+
audit: {
|
|
1662
|
+
schemaVersion: 'fem-plane-strain-linear-solver-audit.v1',
|
|
1663
|
+
solver: 'dense-gaussian',
|
|
1664
|
+
matrixDofCount: input.rhs.length,
|
|
1665
|
+
nonzeroCount: denseNonzeroCount(input.reducedK),
|
|
1666
|
+
iterations: input.rhs.length,
|
|
1667
|
+
tolerance: input.tolerance,
|
|
1668
|
+
maxIterations: input.maxIterations,
|
|
1669
|
+
initialResidualNorm: vectorNorm(input.rhs),
|
|
1670
|
+
finalResidualNorm,
|
|
1671
|
+
residualNormRatio: finalResidualNorm / rhsNorm,
|
|
1672
|
+
correctionNormM: correctionNorm,
|
|
1673
|
+
correctionNormRatio: correctionNorm / Math.max(vectorNorm(input.currentFreeDisplacement), 1e-12),
|
|
1674
|
+
converged: true,
|
|
1675
|
+
},
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function solveSparseDruckerPragerCorrection(input) {
|
|
1679
|
+
const solved = solveCsrConjugateGradient(input.reducedK, input.rhs, {
|
|
1680
|
+
tolerance: input.tolerance,
|
|
1681
|
+
maxIterations: input.maxIterations,
|
|
1682
|
+
preconditioner: 'jacobi',
|
|
1683
|
+
});
|
|
1684
|
+
const correctionNorm = vectorNorm(solved.solution);
|
|
1685
|
+
return {
|
|
1686
|
+
correction: solved.solution,
|
|
1687
|
+
audit: {
|
|
1688
|
+
schemaVersion: 'fem-plane-strain-linear-solver-audit.v1',
|
|
1689
|
+
solver: 'sparse-csr-cg',
|
|
1690
|
+
matrixDofCount: input.rhs.length,
|
|
1691
|
+
nonzeroCount: input.reducedK.nonzeroCount,
|
|
1692
|
+
iterations: solved.iterations,
|
|
1693
|
+
tolerance: solved.tolerance,
|
|
1694
|
+
maxIterations: solved.maxIterations,
|
|
1695
|
+
initialResidualNorm: solved.initialResidualNorm,
|
|
1696
|
+
finalResidualNorm: solved.finalResidualNorm,
|
|
1697
|
+
residualNormRatio: solved.residualNormRatio,
|
|
1698
|
+
correctionNormM: correctionNorm,
|
|
1699
|
+
correctionNormRatio: correctionNorm / Math.max(vectorNorm(input.currentFreeDisplacement), 1e-12),
|
|
1700
|
+
converged: solved.converged,
|
|
1701
|
+
...(solved.failureReason ? { failureReason: solved.failureReason } : {}),
|
|
1702
|
+
},
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1608
1705
|
export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
|
|
1609
|
-
const
|
|
1706
|
+
const linearSolver = normalizeLinearSolverKind(options.linearSolver);
|
|
1707
|
+
const system = assemblePlaneStrainSystem(model, {
|
|
1708
|
+
storage: linearSolver === 'sparse-csr-cg' ? 'triplets-only' : 'dense-and-triplets',
|
|
1709
|
+
});
|
|
1610
1710
|
const loadStepFractions = normalizeLoadStepFractions(options.loadStepFractions);
|
|
1611
|
-
const
|
|
1711
|
+
const linearSolverTolerance = options.linearSolverTolerance ?? Math.min(1e-10, system.policy.forceBalanceTolerance / 10);
|
|
1712
|
+
if (!Number.isFinite(linearSolverTolerance) || linearSolverTolerance <= 0) {
|
|
1713
|
+
throw new Error('linearSolverTolerance must be a finite positive number.');
|
|
1714
|
+
}
|
|
1715
|
+
const linearSolverMaxIterations = options.linearSolverMaxIterations ?? Math.max(100, system.freeDofs.length * 10);
|
|
1716
|
+
assertPositiveInteger(linearSolverMaxIterations, 'linearSolverMaxIterations');
|
|
1717
|
+
const reducedDenseK = linearSolver === 'dense-gaussian'
|
|
1718
|
+
? system.freeDofs.map((row) => system.freeDofs.map((col) => system.stiffness[row][col]))
|
|
1719
|
+
: undefined;
|
|
1720
|
+
const reducedSparseK = linearSolver === 'sparse-csr-cg' && system.freeDofs.length > 0
|
|
1721
|
+
? buildReducedCsr({
|
|
1722
|
+
freeDofs: system.freeDofs,
|
|
1723
|
+
triplets: system.stiffnessTriplets,
|
|
1724
|
+
})
|
|
1725
|
+
: undefined;
|
|
1612
1726
|
const displacement = new Array(system.dofCount).fill(0);
|
|
1613
1727
|
let finalEvaluation;
|
|
1614
1728
|
const loadSteps = [];
|
|
@@ -1621,12 +1735,35 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
|
|
|
1621
1735
|
const residualHistory = [
|
|
1622
1736
|
druckerPragerResidualHistoryEntry(iterations, evaluation, system.policy),
|
|
1623
1737
|
];
|
|
1738
|
+
const linearSolverAudits = [];
|
|
1739
|
+
let linearSolverFailure;
|
|
1624
1740
|
while (!converged && iterations < system.policy.maxIterations) {
|
|
1625
1741
|
iterations += 1;
|
|
1626
1742
|
if (system.freeDofs.length === 0)
|
|
1627
1743
|
break;
|
|
1628
1744
|
const correctionRhs = system.freeDofs.map((index) => -evaluation.residual[index]);
|
|
1629
|
-
const
|
|
1745
|
+
const currentFreeDisplacement = system.freeDofs.map((index) => displacement[index]);
|
|
1746
|
+
const solved = linearSolver === 'sparse-csr-cg'
|
|
1747
|
+
? solveSparseDruckerPragerCorrection({
|
|
1748
|
+
reducedK: reducedSparseK,
|
|
1749
|
+
rhs: correctionRhs,
|
|
1750
|
+
currentFreeDisplacement,
|
|
1751
|
+
tolerance: linearSolverTolerance,
|
|
1752
|
+
maxIterations: linearSolverMaxIterations,
|
|
1753
|
+
})
|
|
1754
|
+
: solveDenseDruckerPragerCorrection({
|
|
1755
|
+
reducedK: reducedDenseK,
|
|
1756
|
+
rhs: correctionRhs,
|
|
1757
|
+
currentFreeDisplacement,
|
|
1758
|
+
tolerance: linearSolverTolerance,
|
|
1759
|
+
maxIterations: linearSolverMaxIterations,
|
|
1760
|
+
});
|
|
1761
|
+
linearSolverAudits.push(solved.audit);
|
|
1762
|
+
if (!solved.audit.converged) {
|
|
1763
|
+
linearSolverFailure = solved.audit;
|
|
1764
|
+
break;
|
|
1765
|
+
}
|
|
1766
|
+
const correction = solved.correction;
|
|
1630
1767
|
for (const [correctionIndex, dof] of system.freeDofs.entries()) {
|
|
1631
1768
|
displacement[dof] += correction[correctionIndex];
|
|
1632
1769
|
}
|
|
@@ -1637,7 +1774,8 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
|
|
|
1637
1774
|
residualHistory.push(druckerPragerResidualHistoryEntry(iterations, evaluation, system.policy));
|
|
1638
1775
|
}
|
|
1639
1776
|
finalEvaluation = evaluation;
|
|
1640
|
-
const terminationReason = druckerPragerTerminationReason(evaluation, system.policy, converged, iterations);
|
|
1777
|
+
const terminationReason = druckerPragerTerminationReason(evaluation, system.policy, converged, iterations, linearSolverFailure);
|
|
1778
|
+
const lastLinearAudit = linearSolverAudits.at(-1);
|
|
1641
1779
|
loadSteps.push({
|
|
1642
1780
|
step: stepIndex + 1,
|
|
1643
1781
|
loadFactor: round(loadFactor, 8),
|
|
@@ -1648,8 +1786,14 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
|
|
|
1648
1786
|
maxYieldResidualRatio: round(evaluation.maxYieldResidualRatio, 12),
|
|
1649
1787
|
maxEquivalentPlasticStrain: round(evaluation.maxEquivalentPlasticStrain, 12),
|
|
1650
1788
|
plasticGaussPointCount: evaluation.plasticGaussPointCount,
|
|
1789
|
+
linearSolver,
|
|
1790
|
+
linearIterations: linearSolverAudits.reduce((sum, audit) => sum + audit.iterations, 0),
|
|
1791
|
+
linearResidualNormRatio: round(lastLinearAudit?.residualNormRatio ?? 0, 12),
|
|
1792
|
+
correctionNormRatio: round(lastLinearAudit?.correctionNormRatio ?? 0, 12),
|
|
1793
|
+
linearSolverAudits,
|
|
1651
1794
|
converged,
|
|
1652
1795
|
terminationReason,
|
|
1796
|
+
...(linearSolverFailure?.failureReason ? { failureReason: linearSolverFailure.failureReason } : {}),
|
|
1653
1797
|
residualHistory,
|
|
1654
1798
|
});
|
|
1655
1799
|
}
|
|
@@ -1672,6 +1816,10 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
|
|
|
1672
1816
|
dofCount: system.dofCount,
|
|
1673
1817
|
freeDofCount: system.freeDofs.length,
|
|
1674
1818
|
constrainedDofCount: system.prescribed.size,
|
|
1819
|
+
linearSolver,
|
|
1820
|
+
nonlinearAlgorithm: 'modified-newton',
|
|
1821
|
+
globalTangent: 'elastic',
|
|
1822
|
+
materialIntegration: 'total-strain-drucker-prager-projection',
|
|
1675
1823
|
loadSteps,
|
|
1676
1824
|
maxFreeResidualKn: round(finalEvaluation.maxFreeResidualKn, 12),
|
|
1677
1825
|
residualNormRatio: round(finalEvaluation.residualNormRatio, 12),
|
|
@@ -1695,7 +1843,9 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
|
|
|
1695
1843
|
limitations: [
|
|
1696
1844
|
'Benchmark-scale modified-Newton plane-strain plasticity evidence kernel only.',
|
|
1697
1845
|
...(failedStep ? ['Nonconverged load-step result is reported fail-closed and must not be treated as an accepted engineering solve.'] : []),
|
|
1698
|
-
|
|
1846
|
+
linearSolver === 'sparse-csr-cg'
|
|
1847
|
+
? 'Uses an experimental CSR Conjugate Gradient linear solve audit, elastic global tangent, and Gauss-point Drucker-Prager stress projection; no production consistent tangent, hardening calibration, staged activation, pore-pressure DOF, or route-backed result manifest is provided.'
|
|
1848
|
+
: 'Uses elastic global tangent with Gauss-point Drucker-Prager stress projection; no production consistent tangent, production sparse solver, hardening calibration, staged activation, pore-pressure DOF, or route-backed result manifest is provided.',
|
|
1699
1849
|
'Use for deterministic evidence and regression tests only until independent published/commercial benchmark comparison and licensed production approval gates are complete.',
|
|
1700
1850
|
],
|
|
1701
1851
|
};
|