@geotechcli/core 0.4.108 → 0.4.109

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) {
@@ -1489,27 +1563,30 @@ function assemblePlaneStrainSystem(model, options = {}) {
1489
1563
  };
1490
1564
  }
1491
1565
  function evaluatePlaneStrainDruckerPragerState(input) {
1492
- const { system, displacement, loadFactor } = input;
1566
+ const { system, displacement, loadFactor, committedStates } = input;
1493
1567
  const internal = new Array(system.dofCount).fill(0);
1494
1568
  let maxYieldResidualRatio = 0;
1495
1569
  let maxEquivalentPlasticStrain = 0;
1570
+ let maxEquivalentPlasticStrainIncrement = 0;
1496
1571
  let plasticGaussPointCount = 0;
1497
- const elements = system.elementGaussCache.map((entry) => {
1572
+ const trialStates = [];
1573
+ const elements = system.elementGaussCache.map((entry, elementIndex) => {
1498
1574
  const material = system.materialById.get(entry.element.materialId);
1499
- const d = planeStrainD(material);
1500
1575
  const elementDisplacement = entry.globalDofs.map((index) => displacement[index]);
1501
1576
  const gaussPoints = entry.gauss.map((point, index) => {
1502
1577
  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({
1578
+ const integrated = integrateDruckerPragerStress({
1505
1579
  material,
1506
1580
  policy: system.policy,
1507
1581
  strain,
1508
- stress: elasticStress,
1509
- sigmaZKpa: planeStrainSigmaZ(material, strain),
1582
+ previous: committedStates[elementIndex]?.[index] ?? initialDruckerPragerState(),
1510
1583
  });
1584
+ const projected = integrated.result;
1585
+ trialStates[elementIndex] = trialStates[elementIndex] ?? [];
1586
+ trialStates[elementIndex][index] = integrated.nextState;
1511
1587
  maxYieldResidualRatio = Math.max(maxYieldResidualRatio, projected.yieldResidualRatio);
1512
1588
  maxEquivalentPlasticStrain = Math.max(maxEquivalentPlasticStrain, projected.equivalentPlasticStrain);
1589
+ maxEquivalentPlasticStrainIncrement = Math.max(maxEquivalentPlasticStrainIncrement, projected.equivalentPlasticStrainIncrement);
1513
1590
  if (projected.state === 'plastic')
1514
1591
  plasticGaussPointCount += 1;
1515
1592
  for (let localDof = 0; localDof < 8; localDof += 1) {
@@ -1538,6 +1615,19 @@ function evaluatePlaneStrainDruckerPragerState(input) {
1538
1615
  round(projected.compressionPositivePrincipalStressKpa[1], 8),
1539
1616
  round(projected.compressionPositivePrincipalStressKpa[2], 8),
1540
1617
  ],
1618
+ strainIncrement: [
1619
+ round(projected.strainIncrement[0], 12),
1620
+ round(projected.strainIncrement[1], 12),
1621
+ round(projected.strainIncrement[2], 12),
1622
+ ],
1623
+ previousEquivalentPlasticStrain: round(projected.previousEquivalentPlasticStrain, 12),
1624
+ equivalentPlasticStrainIncrement: round(projected.equivalentPlasticStrainIncrement, 12),
1625
+ plasticStrainPrincipal: [
1626
+ round(projected.plasticStrainPrincipal[0], 12),
1627
+ round(projected.plasticStrainPrincipal[1], 12),
1628
+ round(projected.plasticStrainPrincipal[2], 12),
1629
+ ],
1630
+ volumetricPlasticStrain: round(projected.volumetricPlasticStrain, 12),
1541
1631
  yieldValueKpa: round(projected.yieldValueKpa, 10),
1542
1632
  yieldResidualRatio: round(projected.yieldResidualRatio, 12),
1543
1633
  plasticMultiplier: round(projected.plasticMultiplier, 12),
@@ -1578,13 +1668,33 @@ function evaluatePlaneStrainDruckerPragerState(input) {
1578
1668
  reactionBalanceRatio,
1579
1669
  maxYieldResidualRatio,
1580
1670
  maxEquivalentPlasticStrain,
1671
+ maxEquivalentPlasticStrainIncrement,
1581
1672
  plasticGaussPointCount,
1673
+ trialStates,
1582
1674
  elements,
1583
1675
  };
1584
1676
  }
1585
- function normalizeLoadStepFractions(loadStepFractions) {
1586
- const fractions = loadStepFractions && loadStepFractions.length > 0
1587
- ? [...loadStepFractions]
1677
+ function normalizeLoadStepFractions(options) {
1678
+ if (options.loadStepFractions != null && options.loadHistoryFactors != null) {
1679
+ throw new Error('Specify either loadStepFractions or loadHistoryFactors, not both.');
1680
+ }
1681
+ if (options.loadHistoryFactors != null) {
1682
+ if (!Array.isArray(options.loadHistoryFactors) || options.loadHistoryFactors.length === 0) {
1683
+ throw new Error('loadHistoryFactors must contain at least one load factor when provided.');
1684
+ }
1685
+ const history = [...options.loadHistoryFactors];
1686
+ for (const [index, factor] of history.entries()) {
1687
+ if (!Number.isFinite(factor) || factor < 0 || factor > 1) {
1688
+ throw new Error(`loadHistoryFactors.${index} must be finite and between 0 and 1.`);
1689
+ }
1690
+ }
1691
+ if (history[history.length - 1] !== 1) {
1692
+ throw new Error('loadHistoryFactors must end at 1.');
1693
+ }
1694
+ return history;
1695
+ }
1696
+ const fractions = options.loadStepFractions && options.loadStepFractions.length > 0
1697
+ ? [...options.loadStepFractions]
1588
1698
  : [0.25, 0.5, 0.75, 1];
1589
1699
  let previous = 0;
1590
1700
  for (const [index, fraction] of fractions.entries()) {
@@ -1602,6 +1712,9 @@ function isDruckerPragerStepConverged(evaluation, policy) {
1602
1712
  return evaluation.residualNormRatio <= policy.forceBalanceTolerance &&
1603
1713
  evaluation.maxYieldResidualRatio <= policy.residualTolerance;
1604
1714
  }
1715
+ function createInitialDruckerPragerStateGrid(system) {
1716
+ return system.elementGaussCache.map((entry) => entry.gauss.map(() => initialDruckerPragerState()));
1717
+ }
1605
1718
  function druckerPragerResidualHistoryEntry(iteration, evaluation, policy) {
1606
1719
  return {
1607
1720
  iteration,
@@ -1707,7 +1820,10 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1707
1820
  const system = assemblePlaneStrainSystem(model, {
1708
1821
  storage: linearSolver === 'sparse-csr-cg' ? 'triplets-only' : 'dense-and-triplets',
1709
1822
  });
1710
- const loadStepFractions = normalizeLoadStepFractions(options.loadStepFractions);
1823
+ const loadStepFractions = normalizeLoadStepFractions({
1824
+ loadStepFractions: options.loadStepFractions,
1825
+ loadHistoryFactors: options.loadHistoryFactors,
1826
+ });
1711
1827
  const linearSolverTolerance = options.linearSolverTolerance ?? Math.min(1e-10, system.policy.forceBalanceTolerance / 10);
1712
1828
  if (!Number.isFinite(linearSolverTolerance) || linearSolverTolerance <= 0) {
1713
1829
  throw new Error('linearSolverTolerance must be a finite positive number.');
@@ -1724,12 +1840,18 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1724
1840
  })
1725
1841
  : undefined;
1726
1842
  const displacement = new Array(system.dofCount).fill(0);
1843
+ let committedStates = createInitialDruckerPragerStateGrid(system);
1727
1844
  let finalEvaluation;
1728
1845
  const loadSteps = [];
1729
1846
  for (const [stepIndex, loadFactor] of loadStepFractions.entries()) {
1730
1847
  for (const [index, value] of system.prescribed)
1731
1848
  displacement[index] = value * loadFactor;
1732
- let evaluation = evaluatePlaneStrainDruckerPragerState({ system, displacement, loadFactor });
1849
+ let evaluation = evaluatePlaneStrainDruckerPragerState({
1850
+ system,
1851
+ displacement,
1852
+ loadFactor,
1853
+ committedStates,
1854
+ });
1733
1855
  let iterations = 0;
1734
1856
  let converged = isDruckerPragerStepConverged(evaluation, system.policy);
1735
1857
  const residualHistory = [
@@ -1769,11 +1891,18 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1769
1891
  }
1770
1892
  for (const [index, value] of system.prescribed)
1771
1893
  displacement[index] = value * loadFactor;
1772
- evaluation = evaluatePlaneStrainDruckerPragerState({ system, displacement, loadFactor });
1894
+ evaluation = evaluatePlaneStrainDruckerPragerState({
1895
+ system,
1896
+ displacement,
1897
+ loadFactor,
1898
+ committedStates,
1899
+ });
1773
1900
  converged = isDruckerPragerStepConverged(evaluation, system.policy);
1774
1901
  residualHistory.push(druckerPragerResidualHistoryEntry(iterations, evaluation, system.policy));
1775
1902
  }
1776
1903
  finalEvaluation = evaluation;
1904
+ if (converged)
1905
+ committedStates = evaluation.trialStates;
1777
1906
  const terminationReason = druckerPragerTerminationReason(evaluation, system.policy, converged, iterations, linearSolverFailure);
1778
1907
  const lastLinearAudit = linearSolverAudits.at(-1);
1779
1908
  loadSteps.push({
@@ -1785,6 +1914,7 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1785
1914
  reactionBalanceRatio: round(evaluation.reactionBalanceRatio, 12),
1786
1915
  maxYieldResidualRatio: round(evaluation.maxYieldResidualRatio, 12),
1787
1916
  maxEquivalentPlasticStrain: round(evaluation.maxEquivalentPlasticStrain, 12),
1917
+ maxEquivalentPlasticStrainIncrement: round(evaluation.maxEquivalentPlasticStrainIncrement, 12),
1788
1918
  plasticGaussPointCount: evaluation.plasticGaussPointCount,
1789
1919
  linearSolver,
1790
1920
  linearIterations: linearSolverAudits.reduce((sum, audit) => sum + audit.iterations, 0),
@@ -1819,13 +1949,15 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1819
1949
  linearSolver,
1820
1950
  nonlinearAlgorithm: 'modified-newton',
1821
1951
  globalTangent: 'elastic',
1822
- materialIntegration: 'total-strain-drucker-prager-projection',
1952
+ materialIntegration: 'incremental-committed-drucker-prager-return-mapping',
1953
+ stateStorage: 'committed-gauss-point-history',
1823
1954
  loadSteps,
1824
1955
  maxFreeResidualKn: round(finalEvaluation.maxFreeResidualKn, 12),
1825
1956
  residualNormRatio: round(finalEvaluation.residualNormRatio, 12),
1826
1957
  reactionBalanceRatio: round(finalEvaluation.reactionBalanceRatio, 12),
1827
1958
  maxYieldResidualRatio: round(finalEvaluation.maxYieldResidualRatio, 12),
1828
1959
  maxEquivalentPlasticStrain: round(finalEvaluation.maxEquivalentPlasticStrain, 12),
1960
+ maxEquivalentPlasticStrainIncrement: round(finalEvaluation.maxEquivalentPlasticStrainIncrement, 12),
1829
1961
  plasticGaussPointCount: finalEvaluation.plasticGaussPointCount,
1830
1962
  converged: status === 'converged',
1831
1963
  status,
@@ -1844,8 +1976,8 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1844
1976
  'Benchmark-scale modified-Newton plane-strain plasticity evidence kernel only.',
1845
1977
  ...(failedStep ? ['Nonconverged load-step result is reported fail-closed and must not be treated as an accepted engineering solve.'] : []),
1846
1978
  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.',
1979
+ ? '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.'
1980
+ : '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
1981
  'Use for deterministic evidence and regression tests only until independent published/commercial benchmark comparison and licensed production approval gates are complete.',
1850
1982
  ],
1851
1983
  };