@geotechcli/core 0.4.108 → 0.4.110

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.
@@ -180,52 +180,126 @@ function deviatoricNorm(values) {
180
180
  const mean = (values[0] + values[1] + values[2]) / 3;
181
181
  return Math.hypot(values[0] - mean, values[1] - mean, values[2] - mean);
182
182
  }
183
- function projectDruckerPragerStress(input) {
183
+ function principalTrace(values) {
184
+ return values[0] + values[1] + values[2];
185
+ }
186
+ function principalDeviator(values) {
187
+ const mean = principalTrace(values) / 3;
188
+ return [values[0] - mean, values[1] - mean, values[2] - mean];
189
+ }
190
+ function addPrincipal(a, b) {
191
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
192
+ }
193
+ function scalePrincipal(values, scale) {
194
+ return [values[0] * scale, values[1] * scale, values[2] * scale];
195
+ }
196
+ function initialDruckerPragerState() {
197
+ return {
198
+ strain: [0, 0, 0],
199
+ stressKpa: [0, 0, 0],
200
+ sigmaZKpa: 0,
201
+ equivalentPlasticStrain: 0,
202
+ plasticStrainPrincipal: [0, 0, 0],
203
+ volumetricPlasticStrain: 0,
204
+ };
205
+ }
206
+ function integrateDruckerPragerStress(input) {
184
207
  const { material, strain } = input;
185
208
  const params = druckerPragerParameters(material);
186
- const principal = principalCompressionFromPlaneStress(input.stress, input.sigmaZKpa);
209
+ const d = planeStrainD(material);
210
+ const strainIncrement = strain.map((value, index) => value - input.previous.strain[index]);
211
+ const stressIncrement = d.map((row) => row.reduce((sum, value, col) => sum + value * strainIncrement[col], 0));
212
+ const trialStress = [
213
+ input.previous.stressKpa[0] + stressIncrement[0],
214
+ input.previous.stressKpa[1] + stressIncrement[1],
215
+ input.previous.stressKpa[2] + stressIncrement[2],
216
+ ];
217
+ const trialSigmaZKpa = input.previous.sigmaZKpa + planeStrainSigmaZ(material, strainIncrement);
218
+ const principal = principalCompressionFromPlaneStress(trialStress, trialSigmaZKpa);
187
219
  const principalStress = principal.values;
188
- const trace = principalStress[0] + principalStress[1] + principalStress[2];
220
+ const trace = principalTrace(principalStress);
189
221
  const mean = trace / 3;
190
222
  const q = deviatoricNorm(principalStress);
191
- const intercept = params.compressionInterceptKpa;
223
+ const hardeningModulusKpa = material.hardeningModulusKpa ?? 0;
224
+ const intercept = params.compressionInterceptKpa +
225
+ hardeningModulusKpa * input.previous.equivalentPlasticStrain;
192
226
  const yieldValue = q - params.rho * trace - intercept;
193
227
  const yieldScale = Math.max(q, Math.abs(params.rho * trace), Math.abs(intercept), 1);
194
228
  if (yieldValue <= 0 || Math.abs(yieldValue) / yieldScale <= input.policy.residualTolerance) {
195
- return {
229
+ const nextState = {
196
230
  strain,
197
- stressKpa: input.stress,
198
- outOfPlaneStressKpa: input.sigmaZKpa,
199
- compressionPositivePrincipalStressKpa: principalStress,
200
- yieldValueKpa: yieldValue,
201
- yieldResidualRatio: Math.max(0, yieldValue) / yieldScale,
202
- plasticMultiplier: 0,
203
- equivalentPlasticStrain: 0,
204
- state: 'elastic',
231
+ stressKpa: trialStress,
232
+ sigmaZKpa: trialSigmaZKpa,
233
+ equivalentPlasticStrain: input.previous.equivalentPlasticStrain,
234
+ plasticStrainPrincipal: input.previous.plasticStrainPrincipal,
235
+ volumetricPlasticStrain: input.previous.volumetricPlasticStrain,
236
+ };
237
+ return {
238
+ nextState,
239
+ result: {
240
+ strain,
241
+ stressKpa: trialStress,
242
+ outOfPlaneStressKpa: trialSigmaZKpa,
243
+ compressionPositivePrincipalStressKpa: principalStress,
244
+ strainIncrement,
245
+ previousEquivalentPlasticStrain: input.previous.equivalentPlasticStrain,
246
+ equivalentPlasticStrainIncrement: 0,
247
+ plasticStrainPrincipal: nextState.plasticStrainPrincipal,
248
+ volumetricPlasticStrain: nextState.volumetricPlasticStrain,
249
+ yieldValueKpa: yieldValue,
250
+ yieldResidualRatio: Math.max(0, yieldValue) / yieldScale,
251
+ plasticMultiplier: 0,
252
+ equivalentPlasticStrain: nextState.equivalentPlasticStrain,
253
+ state: 'elastic',
254
+ },
205
255
  };
206
256
  }
207
- const targetQ = Math.max(0, params.rho * trace + intercept);
208
- const scale = q > 0 ? targetQ / q : 0;
209
- const correctedPrincipal = principalStress.map((value) => mean + (value - mean) * scale);
210
- const corrected = planeStressFromPrincipalCompression(correctedPrincipal, principal.angleRad);
211
- const correctedQ = deviatoricNorm(correctedPrincipal);
212
- const correctedYieldValue = correctedQ - params.rho * trace - intercept;
213
- const correctedScale = Math.max(correctedQ, Math.abs(params.rho * trace), Math.abs(intercept), 1);
214
257
  const moduli = elasticModuli(material);
215
258
  const denominator = 2 * moduli.shearModulusKpa +
216
259
  9 * moduli.bulkModulusKpa * params.rho * params.rhoBar +
217
- (material.hardeningModulusKpa ?? 0);
260
+ hardeningModulusKpa;
218
261
  const plasticMultiplier = Math.max(0, yieldValue / Math.max(denominator, 1e-12));
219
- return {
262
+ const updatedEquivalentPlasticStrain = input.previous.equivalentPlasticStrain + plasticMultiplier;
263
+ const updatedIntercept = params.compressionInterceptKpa +
264
+ hardeningModulusKpa * updatedEquivalentPlasticStrain;
265
+ const trialDeviator = principalDeviator(principalStress);
266
+ const flowDirection = q > 0 ? scalePrincipal(trialDeviator, 1 / q) : [0, 0, 0];
267
+ const plasticStrainIncrement = addPrincipal(scalePrincipal(flowDirection, plasticMultiplier), [-params.rhoBar * plasticMultiplier, -params.rhoBar * plasticMultiplier, -params.rhoBar * plasticMultiplier]);
268
+ const correctedDeviator = addPrincipal(trialDeviator, scalePrincipal(flowDirection, -2 * moduli.shearModulusKpa * plasticMultiplier));
269
+ const correctedTrace = trace + 9 * moduli.bulkModulusKpa * params.rhoBar * plasticMultiplier;
270
+ const correctedPrincipal = addPrincipal(correctedDeviator, [correctedTrace / 3, correctedTrace / 3, correctedTrace / 3]);
271
+ const corrected = planeStressFromPrincipalCompression(correctedPrincipal, principal.angleRad);
272
+ const correctedQ = deviatoricNorm(correctedPrincipal);
273
+ const correctedYieldValue = correctedQ - params.rho * correctedTrace - updatedIntercept;
274
+ const correctedScale = Math.max(correctedQ, Math.abs(params.rho * correctedTrace), Math.abs(updatedIntercept), 1);
275
+ const plasticStrainPrincipal = addPrincipal(input.previous.plasticStrainPrincipal, plasticStrainIncrement);
276
+ const volumetricPlasticStrain = input.previous.volumetricPlasticStrain + principalTrace(plasticStrainIncrement);
277
+ const nextState = {
220
278
  strain,
221
279
  stressKpa: corrected.stress,
222
- outOfPlaneStressKpa: corrected.sigmaZKpa,
223
- compressionPositivePrincipalStressKpa: correctedPrincipal,
224
- yieldValueKpa: correctedYieldValue,
225
- yieldResidualRatio: Math.abs(correctedYieldValue) / correctedScale,
226
- plasticMultiplier,
227
- equivalentPlasticStrain: plasticMultiplier,
228
- state: 'plastic',
280
+ sigmaZKpa: corrected.sigmaZKpa,
281
+ equivalentPlasticStrain: updatedEquivalentPlasticStrain,
282
+ plasticStrainPrincipal,
283
+ volumetricPlasticStrain,
284
+ };
285
+ return {
286
+ nextState,
287
+ result: {
288
+ strain,
289
+ stressKpa: corrected.stress,
290
+ outOfPlaneStressKpa: corrected.sigmaZKpa,
291
+ compressionPositivePrincipalStressKpa: correctedPrincipal,
292
+ strainIncrement,
293
+ previousEquivalentPlasticStrain: input.previous.equivalentPlasticStrain,
294
+ equivalentPlasticStrainIncrement: plasticMultiplier,
295
+ plasticStrainPrincipal,
296
+ volumetricPlasticStrain,
297
+ yieldValueKpa: correctedYieldValue,
298
+ yieldResidualRatio: Math.abs(correctedYieldValue) / correctedScale,
299
+ plasticMultiplier,
300
+ equivalentPlasticStrain: updatedEquivalentPlasticStrain,
301
+ state: 'plastic',
302
+ },
229
303
  };
230
304
  }
231
305
  function shapeDerivativesNatural(xi, eta) {
@@ -1019,7 +1093,7 @@ export function runPlaneStrainBiotConsolidation(model) {
1019
1093
  pressureOvershootKpa: round(pressureOvershootKpa, 8),
1020
1094
  };
1021
1095
  const maxVerticalSettlementM = Math.max(0, -Math.min(...model.nodes.map((_, index) => displacement[dofIndex(index, 'uy')])));
1022
- const converged = residualNormRatio <= policy.forceBalanceTolerance &&
1096
+ const acceptedByPolicy = residualNormRatio <= policy.forceBalanceTolerance &&
1023
1097
  massBalanceErrorRatio <= policy.porePressureMassBalanceTolerance;
1024
1098
  timeSteps.push({
1025
1099
  step: stepIndex + 1,
@@ -1035,7 +1109,8 @@ export function runPlaneStrainBiotConsolidation(model) {
1035
1109
  minPorePressureKpa: round(minPorePressureKpa, 8),
1036
1110
  maxPorePressureKpa: round(maxPorePressureKpa, 8),
1037
1111
  maxVerticalSettlementM: round(maxVerticalSettlementM, 12),
1038
- converged,
1112
+ acceptedByPolicy,
1113
+ converged: acceptedByPolicy,
1039
1114
  });
1040
1115
  previousDisplacement = [...displacement];
1041
1116
  previousPorePressure = [...porePressure];
@@ -1051,6 +1126,69 @@ export function runPlaneStrainBiotConsolidation(model) {
1051
1126
  lastMinPorePressureKpa = minPorePressureKpa;
1052
1127
  lastMaxPorePressureKpa = maxPorePressureKpa;
1053
1128
  }
1129
+ let monotonicAverageFreePressureDissipation = true;
1130
+ let monotonicMaxPressureEnvelope = true;
1131
+ let previousAverageFreePressure = initialAverageFreePorePressureKpa;
1132
+ let previousMaxPressure = pressureUpperBoundKpa;
1133
+ let maxPressureOvershootKpa = 0;
1134
+ let maxResidualNormRatio = 0;
1135
+ let maxMassBalanceErrorRatio = 0;
1136
+ const pressureMonotonicToleranceKpa = 1e-8;
1137
+ for (const step of timeSteps) {
1138
+ maxResidualNormRatio = Math.max(maxResidualNormRatio, step.residualNormRatio);
1139
+ maxMassBalanceErrorRatio = Math.max(maxMassBalanceErrorRatio, step.massBalanceErrorRatio);
1140
+ maxPressureOvershootKpa = Math.max(maxPressureOvershootKpa, step.pressureDiagnostics.pressureOvershootKpa);
1141
+ if (step.pressureDiagnostics.averageFreePorePressureKpa >
1142
+ previousAverageFreePressure + pressureMonotonicToleranceKpa) {
1143
+ monotonicAverageFreePressureDissipation = false;
1144
+ }
1145
+ if (step.maxPorePressureKpa > previousMaxPressure + pressureMonotonicToleranceKpa) {
1146
+ monotonicMaxPressureEnvelope = false;
1147
+ }
1148
+ previousAverageFreePressure = step.pressureDiagnostics.averageFreePorePressureKpa;
1149
+ previousMaxPressure = step.maxPorePressureKpa;
1150
+ }
1151
+ const acceptedStepCount = timeSteps.filter((step) => step.acceptedByPolicy).length;
1152
+ const monotonicAverageFreePressureDissipationRequired = Array.from(prescribedPressures.values()).every((value) => value <= pressureMonotonicToleranceKpa) &&
1153
+ fluxes.every((value) => Math.abs(value) <= 1e-15);
1154
+ const dissipationCheckMode = monotonicAverageFreePressureDissipationRequired
1155
+ ? 'drained-dissipation'
1156
+ : 'prescribed-gradient-relaxation';
1157
+ const transientBlockerCodes = [
1158
+ ...(acceptedStepCount < policy.minAcceptedSteps
1159
+ ? ['accepted-step-count-less-than-policy']
1160
+ : []),
1161
+ ...(maxResidualNormRatio > policy.forceBalanceTolerance
1162
+ ? ['force-residual-tolerance-exceeded']
1163
+ : []),
1164
+ ...(maxMassBalanceErrorRatio > policy.porePressureMassBalanceTolerance
1165
+ ? ['pore-pressure-mass-balance-tolerance-exceeded']
1166
+ : []),
1167
+ ...(maxPressureOvershootKpa > 1e-6
1168
+ ? ['pressure-overshoot-nonzero']
1169
+ : []),
1170
+ ...(monotonicAverageFreePressureDissipationRequired && !monotonicAverageFreePressureDissipation
1171
+ ? ['average-free-pore-pressure-dissipation-not-monotonic']
1172
+ : []),
1173
+ ...(!monotonicMaxPressureEnvelope
1174
+ ? ['max-pore-pressure-envelope-not-monotonic']
1175
+ : []),
1176
+ ];
1177
+ const transientAcceptance = {
1178
+ schemaVersion: 'fem-plane-strain-biot-transient-acceptance.v1',
1179
+ accepted: transientBlockerCodes.length === 0,
1180
+ dissipationCheckMode,
1181
+ acceptedStepCount,
1182
+ requiredStepCount: policy.minAcceptedSteps,
1183
+ maxResidualNormRatio: round(maxResidualNormRatio, 12),
1184
+ maxMassBalanceErrorRatio: round(maxMassBalanceErrorRatio, 12),
1185
+ maxPressureOvershootKpa: round(maxPressureOvershootKpa, 8),
1186
+ monotonicAverageFreePressureDissipation,
1187
+ monotonicAverageFreePressureDissipationRequired,
1188
+ monotonicMaxPressureEnvelope,
1189
+ finalPorePressureDissipationRatio: round(lastPressureDiagnostics.porePressureDissipationRatio, 12),
1190
+ blockerCodes: transientBlockerCodes,
1191
+ };
1054
1192
  let maxBiotCouplingKpa = 0;
1055
1193
  const elementOutputs = elementGaussCache.map((entry) => {
1056
1194
  const material = materialById.get(entry.element.materialId);
@@ -1146,10 +1284,11 @@ export function runPlaneStrainBiotConsolidation(model) {
1146
1284
  freePorePressureResidualL1M3PerS: Number(lastFreePorePressureResidualL1M3PerS.toExponential(12)),
1147
1285
  pressureAudit: lastPressureAudit,
1148
1286
  pressureDiagnostics: lastPressureDiagnostics,
1287
+ transientAcceptance,
1149
1288
  massBalanceErrorRatio: round(lastMassBalanceErrorRatio, 12),
1150
1289
  minPorePressureKpa: round(lastMinPorePressureKpa, 8),
1151
1290
  maxPorePressureKpa: round(lastMaxPorePressureKpa, 8),
1152
- converged: timeSteps.every((step) => step.converged),
1291
+ converged: transientAcceptance.accepted,
1153
1292
  productionReady: false,
1154
1293
  policy,
1155
1294
  limitations: [
@@ -1489,27 +1628,30 @@ function assemblePlaneStrainSystem(model, options = {}) {
1489
1628
  };
1490
1629
  }
1491
1630
  function evaluatePlaneStrainDruckerPragerState(input) {
1492
- const { system, displacement, loadFactor } = input;
1631
+ const { system, displacement, loadFactor, committedStates } = input;
1493
1632
  const internal = new Array(system.dofCount).fill(0);
1494
1633
  let maxYieldResidualRatio = 0;
1495
1634
  let maxEquivalentPlasticStrain = 0;
1635
+ let maxEquivalentPlasticStrainIncrement = 0;
1496
1636
  let plasticGaussPointCount = 0;
1497
- const elements = system.elementGaussCache.map((entry) => {
1637
+ const trialStates = [];
1638
+ const elements = system.elementGaussCache.map((entry, elementIndex) => {
1498
1639
  const material = system.materialById.get(entry.element.materialId);
1499
- const d = planeStrainD(material);
1500
1640
  const elementDisplacement = entry.globalDofs.map((index) => displacement[index]);
1501
1641
  const gaussPoints = entry.gauss.map((point, index) => {
1502
1642
  const strain = point.b.map((row) => row.reduce((sum, value, col) => sum + value * elementDisplacement[col], 0));
1503
- const elasticStress = d.map((row) => row.reduce((sum, value, col) => sum + value * strain[col], 0));
1504
- const projected = projectDruckerPragerStress({
1643
+ const integrated = integrateDruckerPragerStress({
1505
1644
  material,
1506
1645
  policy: system.policy,
1507
1646
  strain,
1508
- stress: elasticStress,
1509
- sigmaZKpa: planeStrainSigmaZ(material, strain),
1647
+ previous: committedStates[elementIndex]?.[index] ?? initialDruckerPragerState(),
1510
1648
  });
1649
+ const projected = integrated.result;
1650
+ trialStates[elementIndex] = trialStates[elementIndex] ?? [];
1651
+ trialStates[elementIndex][index] = integrated.nextState;
1511
1652
  maxYieldResidualRatio = Math.max(maxYieldResidualRatio, projected.yieldResidualRatio);
1512
1653
  maxEquivalentPlasticStrain = Math.max(maxEquivalentPlasticStrain, projected.equivalentPlasticStrain);
1654
+ maxEquivalentPlasticStrainIncrement = Math.max(maxEquivalentPlasticStrainIncrement, projected.equivalentPlasticStrainIncrement);
1513
1655
  if (projected.state === 'plastic')
1514
1656
  plasticGaussPointCount += 1;
1515
1657
  for (let localDof = 0; localDof < 8; localDof += 1) {
@@ -1538,6 +1680,19 @@ function evaluatePlaneStrainDruckerPragerState(input) {
1538
1680
  round(projected.compressionPositivePrincipalStressKpa[1], 8),
1539
1681
  round(projected.compressionPositivePrincipalStressKpa[2], 8),
1540
1682
  ],
1683
+ strainIncrement: [
1684
+ round(projected.strainIncrement[0], 12),
1685
+ round(projected.strainIncrement[1], 12),
1686
+ round(projected.strainIncrement[2], 12),
1687
+ ],
1688
+ previousEquivalentPlasticStrain: round(projected.previousEquivalentPlasticStrain, 12),
1689
+ equivalentPlasticStrainIncrement: round(projected.equivalentPlasticStrainIncrement, 12),
1690
+ plasticStrainPrincipal: [
1691
+ round(projected.plasticStrainPrincipal[0], 12),
1692
+ round(projected.plasticStrainPrincipal[1], 12),
1693
+ round(projected.plasticStrainPrincipal[2], 12),
1694
+ ],
1695
+ volumetricPlasticStrain: round(projected.volumetricPlasticStrain, 12),
1541
1696
  yieldValueKpa: round(projected.yieldValueKpa, 10),
1542
1697
  yieldResidualRatio: round(projected.yieldResidualRatio, 12),
1543
1698
  plasticMultiplier: round(projected.plasticMultiplier, 12),
@@ -1578,13 +1733,33 @@ function evaluatePlaneStrainDruckerPragerState(input) {
1578
1733
  reactionBalanceRatio,
1579
1734
  maxYieldResidualRatio,
1580
1735
  maxEquivalentPlasticStrain,
1736
+ maxEquivalentPlasticStrainIncrement,
1581
1737
  plasticGaussPointCount,
1738
+ trialStates,
1582
1739
  elements,
1583
1740
  };
1584
1741
  }
1585
- function normalizeLoadStepFractions(loadStepFractions) {
1586
- const fractions = loadStepFractions && loadStepFractions.length > 0
1587
- ? [...loadStepFractions]
1742
+ function normalizeLoadStepFractions(options) {
1743
+ if (options.loadStepFractions != null && options.loadHistoryFactors != null) {
1744
+ throw new Error('Specify either loadStepFractions or loadHistoryFactors, not both.');
1745
+ }
1746
+ if (options.loadHistoryFactors != null) {
1747
+ if (!Array.isArray(options.loadHistoryFactors) || options.loadHistoryFactors.length === 0) {
1748
+ throw new Error('loadHistoryFactors must contain at least one load factor when provided.');
1749
+ }
1750
+ const history = [...options.loadHistoryFactors];
1751
+ for (const [index, factor] of history.entries()) {
1752
+ if (!Number.isFinite(factor) || factor < 0 || factor > 1) {
1753
+ throw new Error(`loadHistoryFactors.${index} must be finite and between 0 and 1.`);
1754
+ }
1755
+ }
1756
+ if (history[history.length - 1] !== 1) {
1757
+ throw new Error('loadHistoryFactors must end at 1.');
1758
+ }
1759
+ return history;
1760
+ }
1761
+ const fractions = options.loadStepFractions && options.loadStepFractions.length > 0
1762
+ ? [...options.loadStepFractions]
1588
1763
  : [0.25, 0.5, 0.75, 1];
1589
1764
  let previous = 0;
1590
1765
  for (const [index, fraction] of fractions.entries()) {
@@ -1602,6 +1777,9 @@ function isDruckerPragerStepConverged(evaluation, policy) {
1602
1777
  return evaluation.residualNormRatio <= policy.forceBalanceTolerance &&
1603
1778
  evaluation.maxYieldResidualRatio <= policy.residualTolerance;
1604
1779
  }
1780
+ function createInitialDruckerPragerStateGrid(system) {
1781
+ return system.elementGaussCache.map((entry) => entry.gauss.map(() => initialDruckerPragerState()));
1782
+ }
1605
1783
  function druckerPragerResidualHistoryEntry(iteration, evaluation, policy) {
1606
1784
  return {
1607
1785
  iteration,
@@ -1707,7 +1885,10 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1707
1885
  const system = assemblePlaneStrainSystem(model, {
1708
1886
  storage: linearSolver === 'sparse-csr-cg' ? 'triplets-only' : 'dense-and-triplets',
1709
1887
  });
1710
- const loadStepFractions = normalizeLoadStepFractions(options.loadStepFractions);
1888
+ const loadStepFractions = normalizeLoadStepFractions({
1889
+ loadStepFractions: options.loadStepFractions,
1890
+ loadHistoryFactors: options.loadHistoryFactors,
1891
+ });
1711
1892
  const linearSolverTolerance = options.linearSolverTolerance ?? Math.min(1e-10, system.policy.forceBalanceTolerance / 10);
1712
1893
  if (!Number.isFinite(linearSolverTolerance) || linearSolverTolerance <= 0) {
1713
1894
  throw new Error('linearSolverTolerance must be a finite positive number.');
@@ -1724,12 +1905,18 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1724
1905
  })
1725
1906
  : undefined;
1726
1907
  const displacement = new Array(system.dofCount).fill(0);
1908
+ let committedStates = createInitialDruckerPragerStateGrid(system);
1727
1909
  let finalEvaluation;
1728
1910
  const loadSteps = [];
1729
1911
  for (const [stepIndex, loadFactor] of loadStepFractions.entries()) {
1730
1912
  for (const [index, value] of system.prescribed)
1731
1913
  displacement[index] = value * loadFactor;
1732
- let evaluation = evaluatePlaneStrainDruckerPragerState({ system, displacement, loadFactor });
1914
+ let evaluation = evaluatePlaneStrainDruckerPragerState({
1915
+ system,
1916
+ displacement,
1917
+ loadFactor,
1918
+ committedStates,
1919
+ });
1733
1920
  let iterations = 0;
1734
1921
  let converged = isDruckerPragerStepConverged(evaluation, system.policy);
1735
1922
  const residualHistory = [
@@ -1769,11 +1956,18 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1769
1956
  }
1770
1957
  for (const [index, value] of system.prescribed)
1771
1958
  displacement[index] = value * loadFactor;
1772
- evaluation = evaluatePlaneStrainDruckerPragerState({ system, displacement, loadFactor });
1959
+ evaluation = evaluatePlaneStrainDruckerPragerState({
1960
+ system,
1961
+ displacement,
1962
+ loadFactor,
1963
+ committedStates,
1964
+ });
1773
1965
  converged = isDruckerPragerStepConverged(evaluation, system.policy);
1774
1966
  residualHistory.push(druckerPragerResidualHistoryEntry(iterations, evaluation, system.policy));
1775
1967
  }
1776
1968
  finalEvaluation = evaluation;
1969
+ if (converged)
1970
+ committedStates = evaluation.trialStates;
1777
1971
  const terminationReason = druckerPragerTerminationReason(evaluation, system.policy, converged, iterations, linearSolverFailure);
1778
1972
  const lastLinearAudit = linearSolverAudits.at(-1);
1779
1973
  loadSteps.push({
@@ -1785,6 +1979,7 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1785
1979
  reactionBalanceRatio: round(evaluation.reactionBalanceRatio, 12),
1786
1980
  maxYieldResidualRatio: round(evaluation.maxYieldResidualRatio, 12),
1787
1981
  maxEquivalentPlasticStrain: round(evaluation.maxEquivalentPlasticStrain, 12),
1982
+ maxEquivalentPlasticStrainIncrement: round(evaluation.maxEquivalentPlasticStrainIncrement, 12),
1788
1983
  plasticGaussPointCount: evaluation.plasticGaussPointCount,
1789
1984
  linearSolver,
1790
1985
  linearIterations: linearSolverAudits.reduce((sum, audit) => sum + audit.iterations, 0),
@@ -1819,13 +2014,15 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1819
2014
  linearSolver,
1820
2015
  nonlinearAlgorithm: 'modified-newton',
1821
2016
  globalTangent: 'elastic',
1822
- materialIntegration: 'total-strain-drucker-prager-projection',
2017
+ materialIntegration: 'incremental-committed-drucker-prager-return-mapping',
2018
+ stateStorage: 'committed-gauss-point-history',
1823
2019
  loadSteps,
1824
2020
  maxFreeResidualKn: round(finalEvaluation.maxFreeResidualKn, 12),
1825
2021
  residualNormRatio: round(finalEvaluation.residualNormRatio, 12),
1826
2022
  reactionBalanceRatio: round(finalEvaluation.reactionBalanceRatio, 12),
1827
2023
  maxYieldResidualRatio: round(finalEvaluation.maxYieldResidualRatio, 12),
1828
2024
  maxEquivalentPlasticStrain: round(finalEvaluation.maxEquivalentPlasticStrain, 12),
2025
+ maxEquivalentPlasticStrainIncrement: round(finalEvaluation.maxEquivalentPlasticStrainIncrement, 12),
1829
2026
  plasticGaussPointCount: finalEvaluation.plasticGaussPointCount,
1830
2027
  converged: status === 'converged',
1831
2028
  status,
@@ -1844,8 +2041,8 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1844
2041
  'Benchmark-scale modified-Newton plane-strain plasticity evidence kernel only.',
1845
2042
  ...(failedStep ? ['Nonconverged load-step result is reported fail-closed and must not be treated as an accepted engineering solve.'] : []),
1846
2043
  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.',
2044
+ ? 'Uses an experimental CSR Conjugate Gradient linear solve audit, elastic global tangent, and committed Gauss-point Drucker-Prager return mapping; no production consistent tangent, hardening calibration, staged activation, pore-pressure DOF, or route-backed result manifest is provided.'
2045
+ : 'Uses elastic global tangent with committed Gauss-point Drucker-Prager return mapping; no production consistent tangent, production sparse solver, hardening calibration, staged activation, pore-pressure DOF, or route-backed result manifest is provided.',
1849
2046
  'Use for deterministic evidence and regression tests only until independent published/commercial benchmark comparison and licensed production approval gates are complete.',
1850
2047
  ],
1851
2048
  };