@geotechcli/core 0.4.112 → 0.4.113

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.
@@ -410,6 +410,13 @@ function elementMatrices(input) {
410
410
  }
411
411
  return { stiffness: k, areaM2, gauss };
412
412
  }
413
+ function resolveBiotCoefficient(material) {
414
+ const biotCoefficient = material.biotCoefficient ?? 1;
415
+ if (!Number.isFinite(biotCoefficient) || biotCoefficient < 0 || biotCoefficient > 1) {
416
+ throw new Error(`material ${material.id} biotCoefficient must be finite and between 0 and 1.`);
417
+ }
418
+ return biotCoefficient;
419
+ }
413
420
  function validateHydraulicMaterial(material) {
414
421
  const kxMPerS = material.hydraulicConductivityXMPerS;
415
422
  if (kxMPerS == null) {
@@ -418,10 +425,7 @@ function validateHydraulicMaterial(material) {
418
425
  assertFinitePositive(kxMPerS, `material ${material.id} hydraulicConductivityXMPerS`);
419
426
  const kyMPerS = material.hydraulicConductivityYMPerS ?? kxMPerS;
420
427
  assertFinitePositive(kyMPerS, `material ${material.id} hydraulicConductivityYMPerS`);
421
- const biotCoefficient = material.biotCoefficient ?? 1;
422
- if (!Number.isFinite(biotCoefficient) || biotCoefficient < 0 || biotCoefficient > 1) {
423
- throw new Error(`material ${material.id} biotCoefficient must be finite and between 0 and 1.`);
424
- }
428
+ const biotCoefficient = resolveBiotCoefficient(material);
425
429
  return { kxMPerS, kyMPerS, biotCoefficient };
426
430
  }
427
431
  function hydraulicElementMatrices(input) {
@@ -1506,6 +1510,9 @@ function assemblePlaneStrainSystem(model, options = {}) {
1506
1510
  assertFinitePositive(model.defaultThicknessM, 'defaultThicknessM');
1507
1511
  if (model.nodalLoads != null)
1508
1512
  assertArray(model.nodalLoads, 'nodalLoads');
1513
+ if (model.prescribedPorePressureIncrements != null) {
1514
+ assertArray(model.prescribedPorePressureIncrements, 'prescribedPorePressureIncrements');
1515
+ }
1509
1516
  assertUniqueIds(model.nodes, 'node');
1510
1517
  assertUniqueIds(model.materials, 'material');
1511
1518
  assertUniqueIds(model.elements, 'element');
@@ -1526,6 +1533,11 @@ function assemblePlaneStrainSystem(model, options = {}) {
1526
1533
  : undefined;
1527
1534
  const stiffnessTriplets = [];
1528
1535
  const loads = new Array(dofCount).fill(0);
1536
+ const pressureEquivalentLoads = new Array(dofCount).fill(0);
1537
+ const porePressureIncrements = new Array(model.nodes.length).fill(0);
1538
+ let prescribedPorePressureIncrementActive = false;
1539
+ let maxAbsInputPorePressureIncrementKpa = 0;
1540
+ let maxAbsEffectiveStressReductionKpa = 0;
1529
1541
  for (const node of model.nodes) {
1530
1542
  assertFinite(node.xM, `node ${node.id} xM`);
1531
1543
  assertFinite(node.yM, `node ${node.id} yM`);
@@ -1548,6 +1560,22 @@ function assemblePlaneStrainSystem(model, options = {}) {
1548
1560
  loads[dofIndex(nodeIndex, 'ux')] += fxKn;
1549
1561
  loads[dofIndex(nodeIndex, 'uy')] += fyKn;
1550
1562
  }
1563
+ const prescribedPorePressureIncrements = new Map();
1564
+ for (const increment of model.prescribedPorePressureIncrements ?? []) {
1565
+ const nodeIndex = nodeIndexById.get(increment.nodeId);
1566
+ if (nodeIndex == null)
1567
+ throw new Error(`Unknown prescribed pore-pressure increment node: ${increment.nodeId}.`);
1568
+ assertFinite(increment.porePressureIncrementKpa, `prescribed pore-pressure increment ${increment.nodeId}.porePressureIncrementKpa`);
1569
+ const existing = prescribedPorePressureIncrements.get(nodeIndex);
1570
+ if (existing != null && Math.abs(existing - increment.porePressureIncrementKpa) > 1e-12) {
1571
+ throw new Error(`Conflicting prescribed pore-pressure increment for ${increment.nodeId}.`);
1572
+ }
1573
+ prescribedPorePressureIncrements.set(nodeIndex, increment.porePressureIncrementKpa);
1574
+ porePressureIncrements[nodeIndex] = increment.porePressureIncrementKpa;
1575
+ maxAbsInputPorePressureIncrementKpa = Math.max(maxAbsInputPorePressureIncrementKpa, Math.abs(increment.porePressureIncrementKpa));
1576
+ if (Math.abs(increment.porePressureIncrementKpa) > 0)
1577
+ prescribedPorePressureIncrementActive = true;
1578
+ }
1551
1579
  const elementGaussCache = [];
1552
1580
  for (const element of model.elements) {
1553
1581
  if (!Array.isArray(element.nodeIds) || element.nodeIds.length !== 4) {
@@ -1566,6 +1594,7 @@ function assemblePlaneStrainSystem(model, options = {}) {
1566
1594
  const material = materialById.get(element.materialId);
1567
1595
  if (!material)
1568
1596
  throw new Error(`Unknown element material: ${element.materialId}.`);
1597
+ const biotCoefficient = resolveBiotCoefficient(material);
1569
1598
  const thicknessM = element.thicknessM ?? model.defaultThicknessM ?? 1;
1570
1599
  assertFinitePositive(thicknessM, `element ${element.id} thicknessM`);
1571
1600
  const nodes = nodeIndices.map((index) => model.nodes[index]);
@@ -1581,10 +1610,29 @@ function assemblePlaneStrainSystem(model, options = {}) {
1581
1610
  stiffness[row][col] += value;
1582
1611
  }
1583
1612
  }
1613
+ const pressureGauss = elementData.gauss.map((point) => {
1614
+ const shape = shapeFunctions(point.xi, point.eta);
1615
+ const porePressureIncrementKpa = shape.reduce((sum, value, localNodeIndex) => sum + value * porePressureIncrements[nodeIndices[localNodeIndex]], 0);
1616
+ const biotStressReductionKpa = biotCoefficient * porePressureIncrementKpa;
1617
+ maxAbsEffectiveStressReductionKpa = Math.max(maxAbsEffectiveStressReductionKpa, Math.abs(biotStressReductionKpa));
1618
+ if (prescribedPorePressureIncrementActive && biotCoefficient !== 0) {
1619
+ for (let localDof = 0; localDof < 8; localDof += 1) {
1620
+ const volumetricShapeDerivative = point.b[0][localDof] + point.b[1][localDof];
1621
+ pressureEquivalentLoads[globalDofs[localDof]] +=
1622
+ biotStressReductionKpa * volumetricShapeDerivative * point.detJ * thicknessM;
1623
+ }
1624
+ }
1625
+ return {
1626
+ porePressureIncrementKpa,
1627
+ biotCoefficient,
1628
+ biotStressReductionKpa,
1629
+ };
1630
+ });
1584
1631
  elementGaussCache.push({
1585
1632
  element,
1586
1633
  globalDofs,
1587
1634
  gauss: elementData.gauss,
1635
+ pressureGauss,
1588
1636
  areaM2: elementData.areaM2,
1589
1637
  thicknessM,
1590
1638
  });
@@ -1621,6 +1669,10 @@ function assemblePlaneStrainSystem(model, options = {}) {
1621
1669
  stiffness,
1622
1670
  stiffnessTriplets,
1623
1671
  loads,
1672
+ pressureEquivalentLoads,
1673
+ prescribedPorePressureIncrementActive,
1674
+ maxAbsInputPorePressureIncrementKpa,
1675
+ maxAbsEffectiveStressReductionKpa,
1624
1676
  prescribed,
1625
1677
  freeDofs,
1626
1678
  dofCount,
@@ -1658,6 +1710,14 @@ function evaluatePlaneStrainDruckerPragerState(input) {
1658
1710
  const force = point.b.reduce((sum, bRow, component) => sum + bRow[localDof] * projected.stressKpa[component], 0) * point.detJ * entry.thicknessM;
1659
1711
  internal[entry.globalDofs[localDof]] += force;
1660
1712
  }
1713
+ const pressurePoint = entry.pressureGauss[index];
1714
+ const porePressureIncrementKpa = pressurePoint.porePressureIncrementKpa * loadFactor;
1715
+ const biotStressReductionKpa = pressurePoint.biotStressReductionKpa * loadFactor;
1716
+ const totalStressKpa = [
1717
+ projected.stressKpa[0] - biotStressReductionKpa,
1718
+ projected.stressKpa[1] - biotStressReductionKpa,
1719
+ projected.stressKpa[2],
1720
+ ];
1661
1721
  return {
1662
1722
  elementId: entry.element.id,
1663
1723
  gaussPoint: index + 1,
@@ -1680,6 +1740,16 @@ function evaluatePlaneStrainDruckerPragerState(input) {
1680
1740
  round(projected.compressionPositivePrincipalStressKpa[1], 8),
1681
1741
  round(projected.compressionPositivePrincipalStressKpa[2], 8),
1682
1742
  ],
1743
+ ...(system.prescribedPorePressureIncrementActive ? {
1744
+ porePressureIncrementKpa: round(porePressureIncrementKpa, 8),
1745
+ biotCoefficient: round(pressurePoint.biotCoefficient, 8),
1746
+ biotStressReductionKpa: round(biotStressReductionKpa, 8),
1747
+ totalStressKpa: [
1748
+ round(totalStressKpa[0], 8),
1749
+ round(totalStressKpa[1], 8),
1750
+ round(totalStressKpa[2], 8),
1751
+ ],
1752
+ } : {}),
1683
1753
  strainIncrement: [
1684
1754
  round(projected.strainIncrement[0], 12),
1685
1755
  round(projected.strainIncrement[1], 12),
@@ -1708,14 +1778,20 @@ function evaluatePlaneStrainDruckerPragerState(input) {
1708
1778
  };
1709
1779
  });
1710
1780
  const loadsAtStep = system.loads.map((load) => load * loadFactor);
1711
- const residual = internal.map((value, index) => value - loadsAtStep[index]);
1781
+ const pressureEquivalentLoadsAtStep = system.pressureEquivalentLoads.map((load) => load * loadFactor);
1782
+ const totalAppliedLoadsAtStep = loadsAtStep.map((load, index) => load + pressureEquivalentLoadsAtStep[index]);
1783
+ const residual = internal.map((value, index) => value - totalAppliedLoadsAtStep[index]);
1712
1784
  const maxFreeResidualKn = system.freeDofs.length > 0
1713
1785
  ? Math.max(...system.freeDofs.map((index) => Math.abs(residual[index])))
1714
1786
  : 0;
1715
- const loadNorm = Math.max(Math.hypot(...loadsAtStep), Math.hypot(...internal), Math.hypot(...Array.from(system.prescribed.keys()).map((index) => residual[index])), 1);
1787
+ const loadNorm = Math.max(Math.hypot(...loadsAtStep), Math.hypot(...pressureEquivalentLoadsAtStep), Math.hypot(...totalAppliedLoadsAtStep), Math.hypot(...internal), Math.hypot(...Array.from(system.prescribed.keys()).map((index) => residual[index])), 1);
1716
1788
  const residualNormRatio = maxFreeResidualKn / loadNorm;
1717
- const externalLoadSumX = loadsAtStep.filter((_, index) => index % 2 === 0).reduce((sum, value) => sum + value, 0);
1718
- const externalLoadSumY = loadsAtStep.filter((_, index) => index % 2 === 1).reduce((sum, value) => sum + value, 0);
1789
+ const externalLoadSumX = totalAppliedLoadsAtStep
1790
+ .filter((_, index) => index % 2 === 0)
1791
+ .reduce((sum, value) => sum + value, 0);
1792
+ const externalLoadSumY = totalAppliedLoadsAtStep
1793
+ .filter((_, index) => index % 2 === 1)
1794
+ .reduce((sum, value) => sum + value, 0);
1719
1795
  const reactionSumX = Array.from(system.prescribed.keys())
1720
1796
  .filter((index) => index % 2 === 0)
1721
1797
  .reduce((sum, index) => sum + residual[index], 0);
@@ -2159,6 +2235,26 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
2159
2235
  attempts: adaptiveAttemptAudits,
2160
2236
  blockerCodes: [...new Set(adaptiveBlockerCodes)],
2161
2237
  };
2238
+ const finalAppliedLoadFactor = loadSteps.at(-1)?.loadFactor ?? round(currentLoadFactor, 8);
2239
+ const hydroMechanicalCoupling = system.prescribedPorePressureIncrementActive
2240
+ ? {
2241
+ schemaVersion: 'fem-plane-strain-dp-prescribed-pressure-coupling.v1',
2242
+ mode: 'one-way-prescribed-pore-pressure-increment',
2243
+ porePressureDofCount: 0,
2244
+ pressureRamp: 'load-factor-scaled-increment',
2245
+ appliedLoadFactor: round(finalAppliedLoadFactor, 8),
2246
+ maxAbsInputPorePressureIncrementKpa: round(system.maxAbsInputPorePressureIncrementKpa, 8),
2247
+ maxAbsAppliedPorePressureIncrementKpa: round(system.maxAbsInputPorePressureIncrementKpa * finalAppliedLoadFactor, 8),
2248
+ maxAbsAppliedEffectiveStressReductionKpa: round(system.maxAbsEffectiveStressReductionKpa * finalAppliedLoadFactor, 8),
2249
+ stressConvention: 'stressKpa is effective skeleton stress; totalStressKpa applies Biot pore-pressure reduction to xx/yy only',
2250
+ totalStressRelation: 'sigma_total_xx_yy = sigma_effective_xx_yy - alpha_B * delta_p; shear unchanged',
2251
+ limitations: [
2252
+ 'One-way prescribed pore-pressure increment only; no pore-pressure DOFs, pressure equation, seepage solve, or Biot-plastic consistent tangent is assembled.',
2253
+ 'Pressure increment is ramped by the same load factor history as mechanical loads and prescribed displacements.',
2254
+ 'Use for deterministic effective-stress evidence only until coupled nonlinear hydro-mechanical benchmarks and reviewer approval gates are complete.',
2255
+ ],
2256
+ }
2257
+ : undefined;
2162
2258
  return {
2163
2259
  schemaVersion: 'fem-plane-strain-drucker-prager-result.v1',
2164
2260
  method: 'quad4-plane-strain-drucker-prager-modified-newton',
@@ -2178,6 +2274,7 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
2178
2274
  globalTangent: 'elastic',
2179
2275
  materialIntegration: 'incremental-committed-drucker-prager-return-mapping',
2180
2276
  stateStorage: 'committed-gauss-point-history',
2277
+ ...(hydroMechanicalCoupling ? { hydroMechanicalCoupling } : {}),
2181
2278
  adaptiveLoadStepping,
2182
2279
  loadSteps,
2183
2280
  maxFreeResidualKn: round(acceptedFinalEvaluation.maxFreeResidualKn, 12),
@@ -2206,6 +2303,9 @@ export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
2206
2303
  ...(adaptiveOptions.enabled
2207
2304
  ? ['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
2305
  : []),
2306
+ ...(hydroMechanicalCoupling
2307
+ ? ['Includes a one-way prescribed pore-pressure increment in the effective-stress residual; this is not a coupled Biot-plastic production solve and introduces no pore-pressure DOFs.']
2308
+ : []),
2209
2309
  linearSolver === 'sparse-csr-cg'
2210
2310
  ? '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.'
2211
2311
  : '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.',