@geotechcli/core 0.4.106 → 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/demo.d.ts.map +1 -1
- package/dist/fem/demo.js +4 -0
- package/dist/fem/demo.js.map +1 -1
- 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 +295 -15
- 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/nonlinear-column-solver.d.ts.map +1 -1
- package/dist/fem/nonlinear-column-solver.js +137 -6
- package/dist/fem/nonlinear-column-solver.js.map +1 -1
- package/dist/fem/plane-strain-assembly.d.ts +69 -3
- package/dist/fem/plane-strain-assembly.d.ts.map +1 -1
- package/dist/fem/plane-strain-assembly.js +267 -21
- 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 +8 -2
- 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/fem/types.d.ts +49 -0
- package/dist/fem/types.d.ts.map +1 -1
- package/dist/fem/validation.d.ts.map +1 -1
- package/dist/fem/validation.js +166 -2
- package/dist/fem/validation.js.map +1 -1
- 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,8 @@ 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;
|
|
11
|
+
const MAX_BIOT_TIME_STEP_GROWTH_RATIO = 8;
|
|
9
12
|
function assertFinite(value, label) {
|
|
10
13
|
if (!Number.isFinite(value))
|
|
11
14
|
throw new Error(`${label} must be finite.`);
|
|
@@ -62,6 +65,26 @@ function validateConvergencePolicy(policy) {
|
|
|
62
65
|
throw new Error('policy.minAcceptedSteps must be less than or equal to policy.maxIterations.');
|
|
63
66
|
}
|
|
64
67
|
}
|
|
68
|
+
function validateBiotTransientStepPolicy(timeStepsSeconds, policy) {
|
|
69
|
+
if (timeStepsSeconds.length < policy.minAcceptedSteps) {
|
|
70
|
+
throw new Error(`timeStepsSeconds must include at least ${policy.minAcceptedSteps} accepted transient steps for the Biot consolidation preview.`);
|
|
71
|
+
}
|
|
72
|
+
let previousTimeSeconds = 0;
|
|
73
|
+
let previousDeltaTimeSeconds;
|
|
74
|
+
for (const [index, timeSeconds] of timeStepsSeconds.entries()) {
|
|
75
|
+
assertFinitePositive(timeSeconds, `timeStepsSeconds.${index}`);
|
|
76
|
+
if (timeSeconds <= previousTimeSeconds) {
|
|
77
|
+
throw new Error(`timeStepsSeconds.${index} must be strictly increasing.`);
|
|
78
|
+
}
|
|
79
|
+
const deltaTimeSeconds = timeSeconds - previousTimeSeconds;
|
|
80
|
+
if (previousDeltaTimeSeconds != null &&
|
|
81
|
+
deltaTimeSeconds / previousDeltaTimeSeconds > MAX_BIOT_TIME_STEP_GROWTH_RATIO) {
|
|
82
|
+
throw new Error(`timeStepsSeconds.${index} step growth ratio must not exceed ${MAX_BIOT_TIME_STEP_GROWTH_RATIO} for the Biot consolidation preview.`);
|
|
83
|
+
}
|
|
84
|
+
previousTimeSeconds = timeSeconds;
|
|
85
|
+
previousDeltaTimeSeconds = deltaTimeSeconds;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
65
88
|
function round(value, digits = 10) {
|
|
66
89
|
const factor = 10 ** digits;
|
|
67
90
|
return Math.round(value * factor) / factor;
|
|
@@ -263,6 +286,9 @@ function matVec(matrix, vector) {
|
|
|
263
286
|
function dot(a, b) {
|
|
264
287
|
return a.reduce((sum, value, index) => sum + value * b[index], 0);
|
|
265
288
|
}
|
|
289
|
+
function vectorNorm(values) {
|
|
290
|
+
return Math.sqrt(values.reduce((sum, value) => sum + value * value, 0));
|
|
291
|
+
}
|
|
266
292
|
function elementMatrices(input) {
|
|
267
293
|
const { nodes, material, thicknessM } = input;
|
|
268
294
|
const d = planeStrainD(material);
|
|
@@ -654,6 +680,7 @@ export function runPlaneStrainBiotConsolidation(model) {
|
|
|
654
680
|
assertUniqueIds(model.elements, 'element');
|
|
655
681
|
const policy = model.policy ?? DEFAULT_FEM_CONVERGENCE_POLICY;
|
|
656
682
|
validateConvergencePolicy(policy);
|
|
683
|
+
validateBiotTransientStepPolicy(model.timeStepsSeconds, policy);
|
|
657
684
|
const nodeIndexById = new Map(model.nodes.map((node, index) => [node.id, index]));
|
|
658
685
|
const materialById = new Map(model.materials.map((material) => [material.id, material]));
|
|
659
686
|
const displacementDofCount = model.nodes.length * 2;
|
|
@@ -661,14 +688,6 @@ export function runPlaneStrainBiotConsolidation(model) {
|
|
|
661
688
|
if (displacementDofCount + porePressureDofCount > MAX_DENSE_DOF_COUNT) {
|
|
662
689
|
throw new Error(`Plane-strain Biot consolidation dense assembly is capped at ${MAX_DENSE_DOF_COUNT} coupled DOFs for benchmark-scale evidence runs.`);
|
|
663
690
|
}
|
|
664
|
-
let previousTimeSeconds = 0;
|
|
665
|
-
for (const [index, timeSeconds] of model.timeStepsSeconds.entries()) {
|
|
666
|
-
assertFinitePositive(timeSeconds, `timeStepsSeconds.${index}`);
|
|
667
|
-
if (timeSeconds <= previousTimeSeconds) {
|
|
668
|
-
throw new Error(`timeStepsSeconds.${index} must be strictly increasing.`);
|
|
669
|
-
}
|
|
670
|
-
previousTimeSeconds = timeSeconds;
|
|
671
|
-
}
|
|
672
691
|
for (const node of model.nodes) {
|
|
673
692
|
assertFinite(node.xM, `node ${node.id} xM`);
|
|
674
693
|
assertFinite(node.yM, `node ${node.id} yM`);
|
|
@@ -703,6 +722,9 @@ export function runPlaneStrainBiotConsolidation(model) {
|
|
|
703
722
|
throw new Error(`Unknown Biot nodal flux node: ${flux.nodeId}.`);
|
|
704
723
|
const flowM3PerS = flux.flowM3PerS ?? 0;
|
|
705
724
|
assertFinite(flowM3PerS, `nodal flux ${flux.nodeId}.flowM3PerS`);
|
|
725
|
+
if (flowM3PerS < 0) {
|
|
726
|
+
throw new Error(`nodal flux ${flux.nodeId}.flowM3PerS must be non-negative; extraction-driven suction is unsupported by this saturated excess-pressure Biot evidence kernel.`);
|
|
727
|
+
}
|
|
706
728
|
fluxes[nodeIndex] += flowM3PerS;
|
|
707
729
|
}
|
|
708
730
|
const elementGaussCache = [];
|
|
@@ -828,6 +850,11 @@ export function runPlaneStrainBiotConsolidation(model) {
|
|
|
828
850
|
displacement[index] = value;
|
|
829
851
|
for (const [index, value] of prescribedPressures)
|
|
830
852
|
porePressure[index] = value;
|
|
853
|
+
const pressureUpperBoundKpa = Math.max(initialPorePressureKpa, ...Array.from(prescribedPressures.values()));
|
|
854
|
+
const averagePressure = (dofs) => dofs.length > 0
|
|
855
|
+
? dofs.reduce((sum, index) => sum + porePressure[index], 0) / dofs.length
|
|
856
|
+
: 0;
|
|
857
|
+
const initialAverageFreePorePressureKpa = averagePressure(freePressureDofs);
|
|
831
858
|
let previousDisplacement = [...displacement];
|
|
832
859
|
let previousPorePressure = [...porePressure];
|
|
833
860
|
let lastMechanicalResidual = new Array(displacementDofCount).fill(0);
|
|
@@ -846,6 +873,14 @@ export function runPlaneStrainBiotConsolidation(model) {
|
|
|
846
873
|
couplingRateSumM3PerS: 0,
|
|
847
874
|
darcyFlowRateSumM3PerS: 0,
|
|
848
875
|
};
|
|
876
|
+
let lastPressureDiagnostics = {
|
|
877
|
+
averagePorePressureKpa: round(porePressure.reduce((sum, value) => sum + value, 0) / porePressure.length, 8),
|
|
878
|
+
averageFreePorePressureKpa: round(initialAverageFreePorePressureKpa, 8),
|
|
879
|
+
porePressureDissipationRatio: 0,
|
|
880
|
+
maxPorePressureChangeKpa: 0,
|
|
881
|
+
maxPorePressureChangeRateKpaPerS: 0,
|
|
882
|
+
pressureOvershootKpa: 0,
|
|
883
|
+
};
|
|
849
884
|
let lastMinPorePressureKpa = Math.min(...porePressure);
|
|
850
885
|
let lastMaxPorePressureKpa = Math.max(...porePressure);
|
|
851
886
|
const timeSteps = [];
|
|
@@ -966,6 +1001,23 @@ export function runPlaneStrainBiotConsolidation(model) {
|
|
|
966
1001
|
const massBalanceErrorRatio = freePressureResidualSum / pressureScale;
|
|
967
1002
|
const minPorePressureKpa = Math.min(...porePressure);
|
|
968
1003
|
const maxPorePressureKpa = Math.max(...porePressure);
|
|
1004
|
+
const pressureOvershootKpa = Math.max(0, maxPorePressureKpa - pressureUpperBoundKpa);
|
|
1005
|
+
if (pressureOvershootKpa > 1e-6) {
|
|
1006
|
+
throw new Error(`Plane-strain Biot step ${stepIndex + 1} pore pressure exceeded the initial/prescribed pressure envelope by ${pressureOvershootKpa} kPa.`);
|
|
1007
|
+
}
|
|
1008
|
+
const averagePorePressureKpa = porePressure.reduce((sum, value) => sum + value, 0) / porePressure.length;
|
|
1009
|
+
const averageFreePorePressureKpa = averagePressure(freePressureDofs);
|
|
1010
|
+
const dissipationReferenceKpa = Math.max(initialAverageFreePorePressureKpa, 1e-12);
|
|
1011
|
+
const porePressureDissipationRatio = Math.min(1, Math.max(0, (initialAverageFreePorePressureKpa - averageFreePorePressureKpa) / dissipationReferenceKpa));
|
|
1012
|
+
const maxPorePressureChangeKpa = Math.max(...porePressure.map((value, index) => Math.abs(value - previousPorePressure[index])));
|
|
1013
|
+
const pressureDiagnostics = {
|
|
1014
|
+
averagePorePressureKpa: round(averagePorePressureKpa, 8),
|
|
1015
|
+
averageFreePorePressureKpa: round(averageFreePorePressureKpa, 8),
|
|
1016
|
+
porePressureDissipationRatio: round(porePressureDissipationRatio, 12),
|
|
1017
|
+
maxPorePressureChangeKpa: round(maxPorePressureChangeKpa, 8),
|
|
1018
|
+
maxPorePressureChangeRateKpaPerS: Number((maxPorePressureChangeKpa / deltaTimeSeconds).toExponential(12)),
|
|
1019
|
+
pressureOvershootKpa: round(pressureOvershootKpa, 8),
|
|
1020
|
+
};
|
|
969
1021
|
const maxVerticalSettlementM = Math.max(0, -Math.min(...model.nodes.map((_, index) => displacement[dofIndex(index, 'uy')])));
|
|
970
1022
|
const converged = residualNormRatio <= policy.forceBalanceTolerance &&
|
|
971
1023
|
massBalanceErrorRatio <= policy.porePressureMassBalanceTolerance;
|
|
@@ -979,6 +1031,7 @@ export function runPlaneStrainBiotConsolidation(model) {
|
|
|
979
1031
|
freePorePressureResidualL1M3PerS: Number(freePressureResidualSum.toExponential(12)),
|
|
980
1032
|
massBalanceErrorRatio: round(massBalanceErrorRatio, 12),
|
|
981
1033
|
pressureAudit,
|
|
1034
|
+
pressureDiagnostics,
|
|
982
1035
|
minPorePressureKpa: round(minPorePressureKpa, 8),
|
|
983
1036
|
maxPorePressureKpa: round(maxPorePressureKpa, 8),
|
|
984
1037
|
maxVerticalSettlementM: round(maxVerticalSettlementM, 12),
|
|
@@ -994,6 +1047,7 @@ export function runPlaneStrainBiotConsolidation(model) {
|
|
|
994
1047
|
lastMaxFreePorePressureResidualM3PerS = maxFreePorePressureResidualM3PerS;
|
|
995
1048
|
lastFreePorePressureResidualL1M3PerS = freePressureResidualSum;
|
|
996
1049
|
lastPressureAudit = pressureAudit;
|
|
1050
|
+
lastPressureDiagnostics = pressureDiagnostics;
|
|
997
1051
|
lastMinPorePressureKpa = minPorePressureKpa;
|
|
998
1052
|
lastMaxPorePressureKpa = maxPorePressureKpa;
|
|
999
1053
|
}
|
|
@@ -1063,6 +1117,8 @@ export function runPlaneStrainBiotConsolidation(model) {
|
|
|
1063
1117
|
totalStressRelation: 'sigma_total_xx_yy = sigma_effective_xx_yy - alpha_B * p; shear unchanged',
|
|
1064
1118
|
darcyFluxRelation: 'q = -k/gamma_water * grad(p)',
|
|
1065
1119
|
storageConvention: 'specificStorage1PerM is head-based; pressure storage uses Ss / gamma_water',
|
|
1120
|
+
transientStepPolicy: 'fixed backward-Euler grid requires minAcceptedSteps and bounded step-growth ratio',
|
|
1121
|
+
maxTimeStepGrowthRatio: MAX_BIOT_TIME_STEP_GROWTH_RATIO,
|
|
1066
1122
|
gammaWaterKpaPerM: round(gammaWaterKpaPerM, 8),
|
|
1067
1123
|
},
|
|
1068
1124
|
nodes: model.nodes.map((node, index) => ({
|
|
@@ -1089,6 +1145,7 @@ export function runPlaneStrainBiotConsolidation(model) {
|
|
|
1089
1145
|
maxFreePorePressureResidualM3PerS: Number(lastMaxFreePorePressureResidualM3PerS.toExponential(12)),
|
|
1090
1146
|
freePorePressureResidualL1M3PerS: Number(lastFreePorePressureResidualL1M3PerS.toExponential(12)),
|
|
1091
1147
|
pressureAudit: lastPressureAudit,
|
|
1148
|
+
pressureDiagnostics: lastPressureDiagnostics,
|
|
1092
1149
|
massBalanceErrorRatio: round(lastMassBalanceErrorRatio, 12),
|
|
1093
1150
|
minPorePressureKpa: round(lastMinPorePressureKpa, 8),
|
|
1094
1151
|
maxPorePressureKpa: round(lastMaxPorePressureKpa, 8),
|
|
@@ -1292,7 +1349,7 @@ export function runPlaneStrainQuad4Assembly(model) {
|
|
|
1292
1349
|
policy,
|
|
1293
1350
|
};
|
|
1294
1351
|
}
|
|
1295
|
-
function assemblePlaneStrainSystem(model) {
|
|
1352
|
+
function assemblePlaneStrainSystem(model, options = {}) {
|
|
1296
1353
|
if (model.schemaVersion !== 'fem-plane-strain-model.v1') {
|
|
1297
1354
|
throw new Error('Only fem-plane-strain-model.v1 is supported.');
|
|
1298
1355
|
}
|
|
@@ -1318,10 +1375,17 @@ function assemblePlaneStrainSystem(model) {
|
|
|
1318
1375
|
const nodeIndexById = new Map(model.nodes.map((node, index) => [node.id, index]));
|
|
1319
1376
|
const materialById = new Map(model.materials.map((material) => [material.id, material]));
|
|
1320
1377
|
const dofCount = model.nodes.length * 2;
|
|
1321
|
-
|
|
1378
|
+
const storage = options.storage ?? 'dense-and-triplets';
|
|
1379
|
+
if (storage === 'dense-and-triplets' && dofCount > MAX_DENSE_DOF_COUNT) {
|
|
1322
1380
|
throw new Error(`Plane-strain dense assembly is capped at ${MAX_DENSE_DOF_COUNT} DOFs for benchmark-scale evidence runs.`);
|
|
1323
1381
|
}
|
|
1324
|
-
|
|
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 = [];
|
|
1325
1389
|
const loads = new Array(dofCount).fill(0);
|
|
1326
1390
|
for (const node of model.nodes) {
|
|
1327
1391
|
assertFinite(node.xM, `node ${node.id} xM`);
|
|
@@ -1370,7 +1434,12 @@ function assemblePlaneStrainSystem(model) {
|
|
|
1370
1434
|
const globalDofs = nodeIndices.flatMap((index) => [dofIndex(index, 'ux'), dofIndex(index, 'uy')]);
|
|
1371
1435
|
for (let localRow = 0; localRow < 8; localRow += 1) {
|
|
1372
1436
|
for (let localCol = 0; localCol < 8; localCol += 1) {
|
|
1373
|
-
|
|
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;
|
|
1374
1443
|
}
|
|
1375
1444
|
}
|
|
1376
1445
|
elementGaussCache.push({
|
|
@@ -1411,6 +1480,7 @@ function assemblePlaneStrainSystem(model) {
|
|
|
1411
1480
|
policy,
|
|
1412
1481
|
materialById,
|
|
1413
1482
|
stiffness,
|
|
1483
|
+
stiffnessTriplets,
|
|
1414
1484
|
loads,
|
|
1415
1485
|
prescribed,
|
|
1416
1486
|
freeDofs,
|
|
@@ -1528,10 +1598,131 @@ function normalizeLoadStepFractions(loadStepFractions) {
|
|
|
1528
1598
|
}
|
|
1529
1599
|
return fractions;
|
|
1530
1600
|
}
|
|
1601
|
+
function isDruckerPragerStepConverged(evaluation, policy) {
|
|
1602
|
+
return evaluation.residualNormRatio <= policy.forceBalanceTolerance &&
|
|
1603
|
+
evaluation.maxYieldResidualRatio <= policy.residualTolerance;
|
|
1604
|
+
}
|
|
1605
|
+
function druckerPragerResidualHistoryEntry(iteration, evaluation, policy) {
|
|
1606
|
+
return {
|
|
1607
|
+
iteration,
|
|
1608
|
+
maxFreeResidualKn: round(evaluation.maxFreeResidualKn, 12),
|
|
1609
|
+
residualNormRatio: round(evaluation.residualNormRatio, 12),
|
|
1610
|
+
forceBalanceTolerance: policy.forceBalanceTolerance,
|
|
1611
|
+
reactionBalanceRatio: round(evaluation.reactionBalanceRatio, 12),
|
|
1612
|
+
maxYieldResidualRatio: round(evaluation.maxYieldResidualRatio, 12),
|
|
1613
|
+
yieldResidualTolerance: policy.residualTolerance,
|
|
1614
|
+
converged: isDruckerPragerStepConverged(evaluation, policy),
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
function druckerPragerTerminationReason(evaluation, policy, converged, iterations, linearSolverFailure) {
|
|
1618
|
+
if (converged)
|
|
1619
|
+
return 'converged';
|
|
1620
|
+
if (linearSolverFailure)
|
|
1621
|
+
return 'linear_solver_nonconverged';
|
|
1622
|
+
if (iterations >= policy.maxIterations)
|
|
1623
|
+
return 'max_iterations';
|
|
1624
|
+
if (evaluation.residualNormRatio > policy.forceBalanceTolerance)
|
|
1625
|
+
return 'force_residual_exceeded';
|
|
1626
|
+
return 'yield_residual_exceeded';
|
|
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
|
+
}
|
|
1531
1705
|
export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
|
|
1532
|
-
const
|
|
1706
|
+
const linearSolver = normalizeLinearSolverKind(options.linearSolver);
|
|
1707
|
+
const system = assemblePlaneStrainSystem(model, {
|
|
1708
|
+
storage: linearSolver === 'sparse-csr-cg' ? 'triplets-only' : 'dense-and-triplets',
|
|
1709
|
+
});
|
|
1533
1710
|
const loadStepFractions = normalizeLoadStepFractions(options.loadStepFractions);
|
|
1534
|
-
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;
|
|
1535
1726
|
const displacement = new Array(system.dofCount).fill(0);
|
|
1536
1727
|
let finalEvaluation;
|
|
1537
1728
|
const loadSteps = [];
|
|
@@ -1540,24 +1731,51 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
|
|
|
1540
1731
|
displacement[index] = value * loadFactor;
|
|
1541
1732
|
let evaluation = evaluatePlaneStrainDruckerPragerState({ system, displacement, loadFactor });
|
|
1542
1733
|
let iterations = 0;
|
|
1543
|
-
let converged = evaluation
|
|
1544
|
-
|
|
1734
|
+
let converged = isDruckerPragerStepConverged(evaluation, system.policy);
|
|
1735
|
+
const residualHistory = [
|
|
1736
|
+
druckerPragerResidualHistoryEntry(iterations, evaluation, system.policy),
|
|
1737
|
+
];
|
|
1738
|
+
const linearSolverAudits = [];
|
|
1739
|
+
let linearSolverFailure;
|
|
1545
1740
|
while (!converged && iterations < system.policy.maxIterations) {
|
|
1546
1741
|
iterations += 1;
|
|
1547
1742
|
if (system.freeDofs.length === 0)
|
|
1548
1743
|
break;
|
|
1549
1744
|
const correctionRhs = system.freeDofs.map((index) => -evaluation.residual[index]);
|
|
1550
|
-
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;
|
|
1551
1767
|
for (const [correctionIndex, dof] of system.freeDofs.entries()) {
|
|
1552
1768
|
displacement[dof] += correction[correctionIndex];
|
|
1553
1769
|
}
|
|
1554
1770
|
for (const [index, value] of system.prescribed)
|
|
1555
1771
|
displacement[index] = value * loadFactor;
|
|
1556
1772
|
evaluation = evaluatePlaneStrainDruckerPragerState({ system, displacement, loadFactor });
|
|
1557
|
-
converged = evaluation
|
|
1558
|
-
|
|
1773
|
+
converged = isDruckerPragerStepConverged(evaluation, system.policy);
|
|
1774
|
+
residualHistory.push(druckerPragerResidualHistoryEntry(iterations, evaluation, system.policy));
|
|
1559
1775
|
}
|
|
1560
1776
|
finalEvaluation = evaluation;
|
|
1777
|
+
const terminationReason = druckerPragerTerminationReason(evaluation, system.policy, converged, iterations, linearSolverFailure);
|
|
1778
|
+
const lastLinearAudit = linearSolverAudits.at(-1);
|
|
1561
1779
|
loadSteps.push({
|
|
1562
1780
|
step: stepIndex + 1,
|
|
1563
1781
|
loadFactor: round(loadFactor, 8),
|
|
@@ -1568,12 +1786,22 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
|
|
|
1568
1786
|
maxYieldResidualRatio: round(evaluation.maxYieldResidualRatio, 12),
|
|
1569
1787
|
maxEquivalentPlasticStrain: round(evaluation.maxEquivalentPlasticStrain, 12),
|
|
1570
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,
|
|
1571
1794
|
converged,
|
|
1795
|
+
terminationReason,
|
|
1796
|
+
...(linearSolverFailure?.failureReason ? { failureReason: linearSolverFailure.failureReason } : {}),
|
|
1797
|
+
residualHistory,
|
|
1572
1798
|
});
|
|
1573
1799
|
}
|
|
1574
1800
|
if (!finalEvaluation) {
|
|
1575
1801
|
throw new Error('Plane-strain nonlinear load-step solver requires at least one load step.');
|
|
1576
1802
|
}
|
|
1803
|
+
const failedStep = loadSteps.find((step) => !step.converged);
|
|
1804
|
+
const status = failedStep ? 'nonconverged' : 'converged';
|
|
1577
1805
|
return {
|
|
1578
1806
|
schemaVersion: 'fem-plane-strain-drucker-prager-result.v1',
|
|
1579
1807
|
method: 'quad4-plane-strain-drucker-prager-modified-newton',
|
|
@@ -1588,6 +1816,10 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
|
|
|
1588
1816
|
dofCount: system.dofCount,
|
|
1589
1817
|
freeDofCount: system.freeDofs.length,
|
|
1590
1818
|
constrainedDofCount: system.prescribed.size,
|
|
1819
|
+
linearSolver,
|
|
1820
|
+
nonlinearAlgorithm: 'modified-newton',
|
|
1821
|
+
globalTangent: 'elastic',
|
|
1822
|
+
materialIntegration: 'total-strain-drucker-prager-projection',
|
|
1591
1823
|
loadSteps,
|
|
1592
1824
|
maxFreeResidualKn: round(finalEvaluation.maxFreeResidualKn, 12),
|
|
1593
1825
|
residualNormRatio: round(finalEvaluation.residualNormRatio, 12),
|
|
@@ -1595,11 +1827,25 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
|
|
|
1595
1827
|
maxYieldResidualRatio: round(finalEvaluation.maxYieldResidualRatio, 12),
|
|
1596
1828
|
maxEquivalentPlasticStrain: round(finalEvaluation.maxEquivalentPlasticStrain, 12),
|
|
1597
1829
|
plasticGaussPointCount: finalEvaluation.plasticGaussPointCount,
|
|
1598
|
-
converged:
|
|
1830
|
+
converged: status === 'converged',
|
|
1831
|
+
status,
|
|
1832
|
+
...(failedStep ? {
|
|
1833
|
+
failure: {
|
|
1834
|
+
step: failedStep.step,
|
|
1835
|
+
loadFactor: failedStep.loadFactor,
|
|
1836
|
+
terminationReason: failedStep.terminationReason,
|
|
1837
|
+
residualNormRatio: failedStep.residualNormRatio,
|
|
1838
|
+
maxYieldResidualRatio: failedStep.maxYieldResidualRatio,
|
|
1839
|
+
message: `Plane-strain Drucker-Prager load step ${failedStep.step} did not satisfy the configured convergence policy.`,
|
|
1840
|
+
},
|
|
1841
|
+
} : {}),
|
|
1599
1842
|
policy: system.policy,
|
|
1600
1843
|
limitations: [
|
|
1601
1844
|
'Benchmark-scale modified-Newton plane-strain plasticity evidence kernel only.',
|
|
1602
|
-
|
|
1845
|
+
...(failedStep ? ['Nonconverged load-step result is reported fail-closed and must not be treated as an accepted engineering solve.'] : []),
|
|
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.',
|
|
1603
1849
|
'Use for deterministic evidence and regression tests only until independent published/commercial benchmark comparison and licensed production approval gates are complete.',
|
|
1604
1850
|
],
|
|
1605
1851
|
};
|