@geotechcli/core 0.4.109 → 0.4.111

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.
Files changed (45) hide show
  1. package/dist/agents/brain.d.ts.map +1 -1
  2. package/dist/agents/brain.js +31 -2
  3. package/dist/agents/brain.js.map +1 -1
  4. package/dist/agents/fem-tools.js +5 -0
  5. package/dist/agents/fem-tools.js.map +1 -1
  6. package/dist/agents/safety.d.ts +1 -0
  7. package/dist/agents/safety.d.ts.map +1 -1
  8. package/dist/agents/safety.js +62 -0
  9. package/dist/agents/safety.js.map +1 -1
  10. package/dist/fem/demo.d.ts.map +1 -1
  11. package/dist/fem/demo.js +4 -0
  12. package/dist/fem/demo.js.map +1 -1
  13. package/dist/fem/engineering-evidence.d.ts +21 -2
  14. package/dist/fem/engineering-evidence.d.ts.map +1 -1
  15. package/dist/fem/engineering-evidence.js +354 -6
  16. package/dist/fem/engineering-evidence.js.map +1 -1
  17. package/dist/fem/index.d.ts +4 -2
  18. package/dist/fem/index.d.ts.map +1 -1
  19. package/dist/fem/index.js +2 -0
  20. package/dist/fem/index.js.map +1 -1
  21. package/dist/fem/nonlinear-plane-strain-solver.d.ts +10 -0
  22. package/dist/fem/nonlinear-plane-strain-solver.d.ts.map +1 -0
  23. package/dist/fem/nonlinear-plane-strain-solver.js +358 -0
  24. package/dist/fem/nonlinear-plane-strain-solver.js.map +1 -0
  25. package/dist/fem/plane-strain-assembly.d.ts +54 -0
  26. package/dist/fem/plane-strain-assembly.d.ts.map +1 -1
  27. package/dist/fem/plane-strain-assembly.js +277 -46
  28. package/dist/fem/plane-strain-assembly.js.map +1 -1
  29. package/dist/fem/production-readiness.js +5 -5
  30. package/dist/fem/production-readiness.js.map +1 -1
  31. package/dist/fem/support-design.d.ts +124 -0
  32. package/dist/fem/support-design.d.ts.map +1 -0
  33. package/dist/fem/support-design.js +380 -0
  34. package/dist/fem/support-design.js.map +1 -0
  35. package/dist/fem/types.d.ts +68 -3
  36. package/dist/fem/types.d.ts.map +1 -1
  37. package/dist/fem/validation.d.ts.map +1 -1
  38. package/dist/fem/validation.js +346 -13
  39. package/dist/fem/validation.js.map +1 -1
  40. package/dist/ingest/document-evidence-packet.d.ts +30 -30
  41. package/dist/ingest/job-worker.d.ts.map +1 -1
  42. package/dist/ingest/job-worker.js +4 -0
  43. package/dist/ingest/job-worker.js.map +1 -1
  44. package/dist/meta/metadata.json +1 -1
  45. package/package.json +1 -1
@@ -1093,7 +1093,7 @@ export function runPlaneStrainBiotConsolidation(model) {
1093
1093
  pressureOvershootKpa: round(pressureOvershootKpa, 8),
1094
1094
  };
1095
1095
  const maxVerticalSettlementM = Math.max(0, -Math.min(...model.nodes.map((_, index) => displacement[dofIndex(index, 'uy')])));
1096
- const converged = residualNormRatio <= policy.forceBalanceTolerance &&
1096
+ const acceptedByPolicy = residualNormRatio <= policy.forceBalanceTolerance &&
1097
1097
  massBalanceErrorRatio <= policy.porePressureMassBalanceTolerance;
1098
1098
  timeSteps.push({
1099
1099
  step: stepIndex + 1,
@@ -1109,7 +1109,8 @@ export function runPlaneStrainBiotConsolidation(model) {
1109
1109
  minPorePressureKpa: round(minPorePressureKpa, 8),
1110
1110
  maxPorePressureKpa: round(maxPorePressureKpa, 8),
1111
1111
  maxVerticalSettlementM: round(maxVerticalSettlementM, 12),
1112
- converged,
1112
+ acceptedByPolicy,
1113
+ converged: acceptedByPolicy,
1113
1114
  });
1114
1115
  previousDisplacement = [...displacement];
1115
1116
  previousPorePressure = [...porePressure];
@@ -1125,6 +1126,69 @@ export function runPlaneStrainBiotConsolidation(model) {
1125
1126
  lastMinPorePressureKpa = minPorePressureKpa;
1126
1127
  lastMaxPorePressureKpa = maxPorePressureKpa;
1127
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
+ };
1128
1192
  let maxBiotCouplingKpa = 0;
1129
1193
  const elementOutputs = elementGaussCache.map((entry) => {
1130
1194
  const material = materialById.get(entry.element.materialId);
@@ -1220,10 +1284,11 @@ export function runPlaneStrainBiotConsolidation(model) {
1220
1284
  freePorePressureResidualL1M3PerS: Number(lastFreePorePressureResidualL1M3PerS.toExponential(12)),
1221
1285
  pressureAudit: lastPressureAudit,
1222
1286
  pressureDiagnostics: lastPressureDiagnostics,
1287
+ transientAcceptance,
1223
1288
  massBalanceErrorRatio: round(lastMassBalanceErrorRatio, 12),
1224
1289
  minPorePressureKpa: round(lastMinPorePressureKpa, 8),
1225
1290
  maxPorePressureKpa: round(lastMaxPorePressureKpa, 8),
1226
- converged: timeSteps.every((step) => step.converged),
1291
+ converged: transientAcceptance.accepted,
1227
1292
  productionReady: false,
1228
1293
  policy,
1229
1294
  limitations: [
@@ -1708,6 +1773,42 @@ function normalizeLoadStepFractions(options) {
1708
1773
  }
1709
1774
  return fractions;
1710
1775
  }
1776
+ function normalizeAdaptiveDruckerPragerLoadStepping(value) {
1777
+ if (value == null || value === false) {
1778
+ return {
1779
+ enabled: false,
1780
+ strategy: 'explicit-only',
1781
+ minLoadFactorIncrement: 0,
1782
+ maxCutbacks: 0,
1783
+ };
1784
+ }
1785
+ const options = value === true ? {} : value;
1786
+ const enabled = options.enabled ?? true;
1787
+ if (!enabled) {
1788
+ return {
1789
+ enabled: false,
1790
+ strategy: 'explicit-only',
1791
+ minLoadFactorIncrement: 0,
1792
+ maxCutbacks: 0,
1793
+ };
1794
+ }
1795
+ const strategy = options.strategy ?? 'cutback-bisection';
1796
+ if (strategy !== 'cutback-bisection') {
1797
+ throw new Error('adaptiveLoadStepping.strategy must be cutback-bisection.');
1798
+ }
1799
+ const minLoadFactorIncrement = options.minLoadFactorIncrement ?? 1 / 64;
1800
+ if (!Number.isFinite(minLoadFactorIncrement) || minLoadFactorIncrement <= 0 || minLoadFactorIncrement >= 1) {
1801
+ throw new Error('adaptiveLoadStepping.minLoadFactorIncrement must be finite and between 0 and 1.');
1802
+ }
1803
+ const maxCutbacks = Math.floor(options.maxCutbacks ?? 24);
1804
+ assertPositiveInteger(maxCutbacks, 'adaptiveLoadStepping.maxCutbacks');
1805
+ return {
1806
+ enabled: true,
1807
+ strategy,
1808
+ minLoadFactorIncrement,
1809
+ maxCutbacks,
1810
+ };
1811
+ }
1711
1812
  function isDruckerPragerStepConverged(evaluation, policy) {
1712
1813
  return evaluation.residualNormRatio <= policy.forceBalanceTolerance &&
1713
1814
  evaluation.maxYieldResidualRatio <= policy.residualTolerance;
@@ -1715,6 +1816,19 @@ function isDruckerPragerStepConverged(evaluation, policy) {
1715
1816
  function createInitialDruckerPragerStateGrid(system) {
1716
1817
  return system.elementGaussCache.map((entry) => entry.gauss.map(() => initialDruckerPragerState()));
1717
1818
  }
1819
+ function druckerPragerStateSignature(states) {
1820
+ return states
1821
+ .flatMap((elementStates) => elementStates)
1822
+ .map((state) => [
1823
+ ...state.strain,
1824
+ ...state.stressKpa,
1825
+ state.sigmaZKpa,
1826
+ state.equivalentPlasticStrain,
1827
+ ...state.plasticStrainPrincipal,
1828
+ state.volumetricPlasticStrain,
1829
+ ].map((value) => round(value, 12)).join(','))
1830
+ .join('|');
1831
+ }
1718
1832
  function druckerPragerResidualHistoryEntry(iteration, evaluation, policy) {
1719
1833
  return {
1720
1834
  iteration,
@@ -1830,6 +1944,7 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1830
1944
  }
1831
1945
  const linearSolverMaxIterations = options.linearSolverMaxIterations ?? Math.max(100, system.freeDofs.length * 10);
1832
1946
  assertPositiveInteger(linearSolverMaxIterations, 'linearSolverMaxIterations');
1947
+ const adaptiveOptions = normalizeAdaptiveDruckerPragerLoadStepping(options.adaptiveLoadStepping);
1833
1948
  const reducedDenseK = linearSolver === 'dense-gaussian'
1834
1949
  ? system.freeDofs.map((row) => system.freeDofs.map((col) => system.stiffness[row][col]))
1835
1950
  : undefined;
@@ -1843,14 +1958,22 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1843
1958
  let committedStates = createInitialDruckerPragerStateGrid(system);
1844
1959
  let finalEvaluation;
1845
1960
  const loadSteps = [];
1846
- for (const [stepIndex, loadFactor] of loadStepFractions.entries()) {
1961
+ let currentLoadFactor = 0;
1962
+ let attemptedStepCount = 0;
1963
+ let cutbackCount = 0;
1964
+ let maxCutbackDepth = 0;
1965
+ const acceptedLoadFactors = [];
1966
+ const adaptiveAttemptAudits = [];
1967
+ const adaptiveBlockerCodes = [];
1968
+ function solveDruckerPragerStepAttempt(input) {
1969
+ const trialDisplacement = [...input.startingDisplacement];
1847
1970
  for (const [index, value] of system.prescribed)
1848
- displacement[index] = value * loadFactor;
1971
+ trialDisplacement[index] = value * input.loadFactor;
1849
1972
  let evaluation = evaluatePlaneStrainDruckerPragerState({
1850
1973
  system,
1851
- displacement,
1852
- loadFactor,
1853
- committedStates,
1974
+ displacement: trialDisplacement,
1975
+ loadFactor: input.loadFactor,
1976
+ committedStates: input.startingCommittedStates,
1854
1977
  });
1855
1978
  let iterations = 0;
1856
1979
  let converged = isDruckerPragerStepConverged(evaluation, system.policy);
@@ -1864,7 +1987,7 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1864
1987
  if (system.freeDofs.length === 0)
1865
1988
  break;
1866
1989
  const correctionRhs = system.freeDofs.map((index) => -evaluation.residual[index]);
1867
- const currentFreeDisplacement = system.freeDofs.map((index) => displacement[index]);
1990
+ const currentFreeDisplacement = system.freeDofs.map((index) => trialDisplacement[index]);
1868
1991
  const solved = linearSolver === 'sparse-csr-cg'
1869
1992
  ? solveSparseDruckerPragerCorrection({
1870
1993
  reducedK: reducedSparseK,
@@ -1887,51 +2010,155 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1887
2010
  }
1888
2011
  const correction = solved.correction;
1889
2012
  for (const [correctionIndex, dof] of system.freeDofs.entries()) {
1890
- displacement[dof] += correction[correctionIndex];
2013
+ trialDisplacement[dof] += correction[correctionIndex];
1891
2014
  }
1892
2015
  for (const [index, value] of system.prescribed)
1893
- displacement[index] = value * loadFactor;
2016
+ trialDisplacement[index] = value * input.loadFactor;
1894
2017
  evaluation = evaluatePlaneStrainDruckerPragerState({
1895
2018
  system,
1896
- displacement,
1897
- loadFactor,
1898
- committedStates,
2019
+ displacement: trialDisplacement,
2020
+ loadFactor: input.loadFactor,
2021
+ committedStates: input.startingCommittedStates,
1899
2022
  });
1900
2023
  converged = isDruckerPragerStepConverged(evaluation, system.policy);
1901
2024
  residualHistory.push(druckerPragerResidualHistoryEntry(iterations, evaluation, system.policy));
1902
2025
  }
1903
- finalEvaluation = evaluation;
1904
- if (converged)
1905
- committedStates = evaluation.trialStates;
1906
2026
  const terminationReason = druckerPragerTerminationReason(evaluation, system.policy, converged, iterations, linearSolverFailure);
1907
2027
  const lastLinearAudit = linearSolverAudits.at(-1);
1908
- loadSteps.push({
1909
- step: stepIndex + 1,
1910
- loadFactor: round(loadFactor, 8),
1911
- iterations,
1912
- maxFreeResidualKn: round(evaluation.maxFreeResidualKn, 12),
1913
- residualNormRatio: round(evaluation.residualNormRatio, 12),
1914
- reactionBalanceRatio: round(evaluation.reactionBalanceRatio, 12),
1915
- maxYieldResidualRatio: round(evaluation.maxYieldResidualRatio, 12),
1916
- maxEquivalentPlasticStrain: round(evaluation.maxEquivalentPlasticStrain, 12),
1917
- maxEquivalentPlasticStrainIncrement: round(evaluation.maxEquivalentPlasticStrainIncrement, 12),
1918
- plasticGaussPointCount: evaluation.plasticGaussPointCount,
1919
- linearSolver,
1920
- linearIterations: linearSolverAudits.reduce((sum, audit) => sum + audit.iterations, 0),
1921
- linearResidualNormRatio: round(lastLinearAudit?.residualNormRatio ?? 0, 12),
1922
- correctionNormRatio: round(lastLinearAudit?.correctionNormRatio ?? 0, 12),
1923
- linearSolverAudits,
2028
+ return {
2029
+ evaluation,
2030
+ displacement: trialDisplacement,
1924
2031
  converged,
1925
- terminationReason,
1926
- ...(linearSolverFailure?.failureReason ? { failureReason: linearSolverFailure.failureReason } : {}),
1927
- residualHistory,
2032
+ step: {
2033
+ step: input.step,
2034
+ loadFactor: round(input.loadFactor, 8),
2035
+ ...(Math.abs(input.requestedLoadFactor - input.loadFactor) > 1e-12
2036
+ ? { requestedLoadFactor: round(input.requestedLoadFactor, 8) }
2037
+ : {}),
2038
+ ...(input.cutbackDepth > 0 ? { cutbackDepth: input.cutbackDepth, adaptiveCutback: true } : {}),
2039
+ iterations,
2040
+ maxFreeResidualKn: round(evaluation.maxFreeResidualKn, 12),
2041
+ residualNormRatio: round(evaluation.residualNormRatio, 12),
2042
+ reactionBalanceRatio: round(evaluation.reactionBalanceRatio, 12),
2043
+ maxYieldResidualRatio: round(evaluation.maxYieldResidualRatio, 12),
2044
+ maxEquivalentPlasticStrain: round(evaluation.maxEquivalentPlasticStrain, 12),
2045
+ maxEquivalentPlasticStrainIncrement: round(evaluation.maxEquivalentPlasticStrainIncrement, 12),
2046
+ plasticGaussPointCount: evaluation.plasticGaussPointCount,
2047
+ linearSolver,
2048
+ linearIterations: linearSolverAudits.reduce((sum, audit) => sum + audit.iterations, 0),
2049
+ linearResidualNormRatio: round(lastLinearAudit?.residualNormRatio ?? 0, 12),
2050
+ correctionNormRatio: round(lastLinearAudit?.correctionNormRatio ?? 0, 12),
2051
+ linearSolverAudits,
2052
+ converged,
2053
+ terminationReason,
2054
+ ...(linearSolverFailure?.failureReason ? { failureReason: linearSolverFailure.failureReason } : {}),
2055
+ residualHistory,
2056
+ },
2057
+ };
2058
+ }
2059
+ function solveToLoadFactor(targetLoadFactor, requestedLoadFactor, cutbackDepth) {
2060
+ const startLoadFactor = currentLoadFactor;
2061
+ const committedStateSignatureBefore = druckerPragerStateSignature(committedStates);
2062
+ const attempt = solveDruckerPragerStepAttempt({
2063
+ step: loadSteps.length + 1,
2064
+ loadFactor: targetLoadFactor,
2065
+ requestedLoadFactor,
2066
+ cutbackDepth,
2067
+ startingDisplacement: displacement,
2068
+ startingCommittedStates: committedStates,
2069
+ });
2070
+ attemptedStepCount += 1;
2071
+ maxCutbackDepth = Math.max(maxCutbackDepth, cutbackDepth);
2072
+ if (attempt.converged) {
2073
+ adaptiveAttemptAudits.push({
2074
+ attempt: adaptiveAttemptAudits.length + 1,
2075
+ startLoadFactor: round(startLoadFactor, 8),
2076
+ targetLoadFactor: round(targetLoadFactor, 8),
2077
+ requestedLoadFactor: round(requestedLoadFactor, 8),
2078
+ cutbackDepth,
2079
+ accepted: true,
2080
+ rollbackApplied: false,
2081
+ terminationReason: attempt.step.terminationReason,
2082
+ committedStateSignatureBefore,
2083
+ committedStateSignatureAfter: druckerPragerStateSignature(attempt.evaluation.trialStates),
2084
+ });
2085
+ displacement.splice(0, displacement.length, ...attempt.displacement);
2086
+ committedStates = attempt.evaluation.trialStates;
2087
+ finalEvaluation = attempt.evaluation;
2088
+ currentLoadFactor = targetLoadFactor;
2089
+ acceptedLoadFactors.push(round(targetLoadFactor, 8));
2090
+ loadSteps.push({
2091
+ ...attempt.step,
2092
+ step: loadSteps.length + 1,
2093
+ });
2094
+ return true;
2095
+ }
2096
+ adaptiveAttemptAudits.push({
2097
+ attempt: adaptiveAttemptAudits.length + 1,
2098
+ startLoadFactor: round(startLoadFactor, 8),
2099
+ targetLoadFactor: round(targetLoadFactor, 8),
2100
+ requestedLoadFactor: round(requestedLoadFactor, 8),
2101
+ cutbackDepth,
2102
+ accepted: false,
2103
+ rollbackApplied: adaptiveOptions.enabled,
2104
+ terminationReason: attempt.step.terminationReason,
2105
+ committedStateSignatureBefore,
2106
+ committedStateSignatureAfter: committedStateSignatureBefore,
1928
2107
  });
2108
+ if (adaptiveOptions.enabled) {
2109
+ const increment = targetLoadFactor - startLoadFactor;
2110
+ const midpoint = startLoadFactor + increment / 2;
2111
+ if (cutbackCount >= adaptiveOptions.maxCutbacks) {
2112
+ adaptiveBlockerCodes.push('adaptive-load-step-max-cutbacks-exhausted');
2113
+ }
2114
+ else if (Math.abs(midpoint - startLoadFactor) < adaptiveOptions.minLoadFactorIncrement) {
2115
+ adaptiveBlockerCodes.push('adaptive-load-step-min-increment-reached');
2116
+ }
2117
+ else if (Math.abs(midpoint - targetLoadFactor) < 1e-12) {
2118
+ adaptiveBlockerCodes.push('adaptive-load-step-cutback-stalled');
2119
+ }
2120
+ else {
2121
+ cutbackCount += 1;
2122
+ const firstHalfAccepted = solveToLoadFactor(midpoint, requestedLoadFactor, cutbackDepth + 1);
2123
+ if (!firstHalfAccepted)
2124
+ return false;
2125
+ return solveToLoadFactor(targetLoadFactor, requestedLoadFactor, cutbackDepth + 1);
2126
+ }
2127
+ }
2128
+ displacement.splice(0, displacement.length, ...attempt.displacement);
2129
+ finalEvaluation = attempt.evaluation;
2130
+ loadSteps.push({
2131
+ ...attempt.step,
2132
+ step: loadSteps.length + 1,
2133
+ });
2134
+ return false;
2135
+ }
2136
+ for (const loadFactor of loadStepFractions) {
2137
+ const accepted = solveToLoadFactor(loadFactor, loadFactor, 0);
2138
+ if (!accepted && adaptiveOptions.enabled)
2139
+ break;
1929
2140
  }
1930
2141
  if (!finalEvaluation) {
1931
2142
  throw new Error('Plane-strain nonlinear load-step solver requires at least one load step.');
1932
2143
  }
2144
+ const acceptedFinalEvaluation = finalEvaluation;
1933
2145
  const failedStep = loadSteps.find((step) => !step.converged);
1934
2146
  const status = failedStep ? 'nonconverged' : 'converged';
2147
+ const adaptiveLoadStepping = {
2148
+ schemaVersion: 'fem-plane-strain-dp-adaptive-load-stepping.v1',
2149
+ enabled: adaptiveOptions.enabled,
2150
+ strategy: adaptiveOptions.strategy,
2151
+ requestedStepCount: loadStepFractions.length,
2152
+ attemptedStepCount,
2153
+ acceptedStepCount: loadSteps.filter((step) => step.converged).length,
2154
+ cutbackCount,
2155
+ maxCutbackDepth,
2156
+ minLoadFactorIncrement: round(adaptiveOptions.minLoadFactorIncrement, 12),
2157
+ requestedLoadFactors: loadStepFractions.map((factor) => round(factor, 8)),
2158
+ acceptedLoadFactors,
2159
+ attempts: adaptiveAttemptAudits,
2160
+ blockerCodes: [...new Set(adaptiveBlockerCodes)],
2161
+ };
1935
2162
  return {
1936
2163
  schemaVersion: 'fem-plane-strain-drucker-prager-result.v1',
1937
2164
  method: 'quad4-plane-strain-drucker-prager-modified-newton',
@@ -1939,10 +2166,10 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1939
2166
  ...node,
1940
2167
  uxM: round(displacement[dofIndex(index, 'ux')], 12),
1941
2168
  uyM: round(displacement[dofIndex(index, 'uy')], 12),
1942
- rxnXKn: round(finalEvaluation.residual[dofIndex(index, 'ux')], 8),
1943
- rxnYKn: round(finalEvaluation.residual[dofIndex(index, 'uy')], 8),
2169
+ rxnXKn: round(acceptedFinalEvaluation.residual[dofIndex(index, 'ux')], 8),
2170
+ rxnYKn: round(acceptedFinalEvaluation.residual[dofIndex(index, 'uy')], 8),
1944
2171
  })),
1945
- elements: finalEvaluation.elements,
2172
+ elements: acceptedFinalEvaluation.elements,
1946
2173
  dofCount: system.dofCount,
1947
2174
  freeDofCount: system.freeDofs.length,
1948
2175
  constrainedDofCount: system.prescribed.size,
@@ -1951,14 +2178,15 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1951
2178
  globalTangent: 'elastic',
1952
2179
  materialIntegration: 'incremental-committed-drucker-prager-return-mapping',
1953
2180
  stateStorage: 'committed-gauss-point-history',
2181
+ adaptiveLoadStepping,
1954
2182
  loadSteps,
1955
- maxFreeResidualKn: round(finalEvaluation.maxFreeResidualKn, 12),
1956
- residualNormRatio: round(finalEvaluation.residualNormRatio, 12),
1957
- reactionBalanceRatio: round(finalEvaluation.reactionBalanceRatio, 12),
1958
- maxYieldResidualRatio: round(finalEvaluation.maxYieldResidualRatio, 12),
1959
- maxEquivalentPlasticStrain: round(finalEvaluation.maxEquivalentPlasticStrain, 12),
1960
- maxEquivalentPlasticStrainIncrement: round(finalEvaluation.maxEquivalentPlasticStrainIncrement, 12),
1961
- plasticGaussPointCount: finalEvaluation.plasticGaussPointCount,
2183
+ maxFreeResidualKn: round(acceptedFinalEvaluation.maxFreeResidualKn, 12),
2184
+ residualNormRatio: round(acceptedFinalEvaluation.residualNormRatio, 12),
2185
+ reactionBalanceRatio: round(acceptedFinalEvaluation.reactionBalanceRatio, 12),
2186
+ maxYieldResidualRatio: round(acceptedFinalEvaluation.maxYieldResidualRatio, 12),
2187
+ maxEquivalentPlasticStrain: round(acceptedFinalEvaluation.maxEquivalentPlasticStrain, 12),
2188
+ maxEquivalentPlasticStrainIncrement: round(acceptedFinalEvaluation.maxEquivalentPlasticStrainIncrement, 12),
2189
+ plasticGaussPointCount: acceptedFinalEvaluation.plasticGaussPointCount,
1962
2190
  converged: status === 'converged',
1963
2191
  status,
1964
2192
  ...(failedStep ? {
@@ -1975,6 +2203,9 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
1975
2203
  limitations: [
1976
2204
  'Benchmark-scale modified-Newton plane-strain plasticity evidence kernel only.',
1977
2205
  ...(failedStep ? ['Nonconverged load-step result is reported fail-closed and must not be treated as an accepted engineering solve.'] : []),
2206
+ ...(adaptiveOptions.enabled
2207
+ ? ['Adaptive cutback-bisection load stepping can subdivide rejected nonlinear increments with rollback audit, but it is still not an arc-length or production consistent-tangent strategy.']
2208
+ : []),
1978
2209
  linearSolver === 'sparse-csr-cg'
1979
2210
  ? '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
2211
  : '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.',