@geotechcli/core 0.4.123 → 0.4.124

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.
@@ -22,6 +22,7 @@ const FEM_MAX_PREVIEW_MESH_NODES = FEM_WEBGL_UINT16_INDEX_LIMIT + 1;
22
22
  const FEM_MIN_BIOT_TRANSIENT_STEPS = 3;
23
23
  const FEM_MAX_BIOT_TIME_STEP_GROWTH_RATIO = 8;
24
24
  const FEM_PLANE_STRAIN_DP_ADAPTIVE_BACKEND_ID = 'builtin-plane-strain-dp-adaptive-v0';
25
+ const FEM_PLANE_STRAIN_DP_BIOT_REPLAY_BACKEND_ID = 'builtin-plane-strain-dp-biot-replay-v0';
25
26
  const FEM_PLANE_STRAIN_DP_ANALYSIS_TYPE = 'static_2d_plane_strain_drucker_prager';
26
27
  function expectedMeshCounts(mesh) {
27
28
  if (mesh.elementType === 'quad4_plane_strain') {
@@ -719,7 +720,9 @@ export function validateFemAnalysisCase(caseFile) {
719
720
  findings.push(finding('review', 'stages.final-depth-review', 'Last excavation stage does not exactly match the final depth; staging requires review.'));
720
721
  }
721
722
  findings.push(isPlaneStrainDruckerPragerAnalysis
722
- ? finding('review', 'excavation.plane-strain-dp-preview', 'Excavation preview uses experimental plane-strain Drucker-Prager plasticity and still excludes retaining wall design, basal heave, seepage, consolidation, and production design acceptance.')
723
+ ? biot
724
+ ? finding('review', 'excavation.plane-strain-dp-biot-replay-preview', 'Excavation preview uses experimental plane-strain Drucker-Prager plasticity with sequential Biot pressure replay; it still excludes retaining wall design, basal heave, monolithic hydro-mechanical coupling, and production design acceptance.')
725
+ : finding('review', 'excavation.plane-strain-dp-preview', 'Excavation preview uses experimental plane-strain Drucker-Prager plasticity and still excludes retaining wall design, basal heave, seepage, consolidation, and production design acceptance.')
723
726
  : finding('review', 'excavation.design-excluded', 'Excavation preview excludes retaining wall design, basal heave, seepage, consolidation, and nonlinear soil response.'));
724
727
  }
725
728
  }
@@ -799,7 +802,9 @@ export function validateFemAnalysisCase(caseFile) {
799
802
  findings.push(finding('review', 'consolidation.1d-preview', 'Staged consolidation preview uses a 1D Terzaghi column and Mohr-Coulomb material-point review gate; it is not a full 2D/3D coupled FEM consolidation solver.'));
800
803
  }
801
804
  }
802
- if (caseFile.objective === 'seepage_groundwater_coupling') {
805
+ const validatesBiotPressureSourceGeometry = caseFile.objective === 'seepage_groundwater_coupling' ||
806
+ (caseFile.objective === 'excavation_deformation' && isPlaneStrainDruckerPragerAnalysis && biot != null);
807
+ if (validatesBiotPressureSourceGeometry) {
803
808
  if (!biot) {
804
809
  findings.push(finding('blocker', 'geometry.biot-missing', 'Seepage/groundwater coupling cases require plane-strain Biot geometry.'));
805
810
  }
@@ -865,7 +870,9 @@ export function validateFemAnalysisCase(caseFile) {
865
870
  pushFiniteNumberFinding(findings, boundary.porePressureKpa, `${prefix}.pore-pressure`, 'Prescribed pore pressure', { nonNegative: true });
866
871
  }
867
872
  }
868
- findings.push(finding('review', 'biot.up-preview-only', 'Seepage/groundwater coupling uses an experimental deterministic 2D Biot u-p preview and is not production design evidence.'));
873
+ findings.push(caseFile.objective === 'seepage_groundwater_coupling'
874
+ ? finding('review', 'biot.up-preview-only', 'Seepage/groundwater coupling uses an experimental deterministic 2D Biot u-p preview and is not production design evidence.')
875
+ : finding('review', 'biot.pressure-replay-source-preview-only', 'Plane-strain Drucker-Prager excavation uses Biot geometry only as an experimental sequential pressure-replay source; this is not monolithic hydro-mechanical production evidence.'));
869
876
  }
870
877
  }
871
878
  if (caseFile.materials.length === 0) {
@@ -923,8 +930,10 @@ export function validateFemAnalysisCase(caseFile) {
923
930
  }
924
931
  pushFiniteNumberFinding(findings, material.cohesionKpa, `${prefix}.plane-strain-dp-cohesion`, 'Plane-strain Drucker-Prager cohesion', { nonNegative: true });
925
932
  }
926
- if (caseFile.objective === 'seepage_groundwater_coupling') {
927
- if (material.model !== 'linear_elastic') {
933
+ const requiresBiotMaterial = caseFile.objective === 'seepage_groundwater_coupling' ||
934
+ (isPlaneStrainDruckerPragerAnalysis && biot != null);
935
+ if (requiresBiotMaterial) {
936
+ if (caseFile.objective === 'seepage_groundwater_coupling' && material.model !== 'linear_elastic') {
928
937
  findings.push(finding('blocker', `${prefix}.biot-model-required`, 'Biot u-p preview requires a linear_elastic material with hydraulic coupling parameters.'));
929
938
  }
930
939
  const hasHydraulicX = material.hydraulicConductivityXMPerS != null || material.hydraulicConductivityMPerS != null;
@@ -1526,6 +1535,249 @@ function validatePlaneStrainDpAdaptiveAcceptance(findings, manifest, solverToler
1526
1535
  findings.push(finding('blocker', 'result.envelope.dp.yield-residual-too-large', 'DP yield residual exceeds the material return-map tolerance.'));
1527
1536
  }
1528
1537
  }
1538
+ function validatePressureAuditEnvelope(findings, manifest, context) {
1539
+ const { envelope } = manifest;
1540
+ const pressureAudit = manifest.pressureAudit;
1541
+ const codePrefix = context === 'biot' ? 'result.pressure-audit' : 'result.dp-biot-replay.pressure-audit';
1542
+ if (!pressureAudit) {
1543
+ findings.push(finding('blocker', `${codePrefix}.missing`, context === 'biot'
1544
+ ? 'Biot result manifests must include a pressure audit.'
1545
+ : 'DP Biot pressure-replay manifests must include the upstream Biot pressure audit.'));
1546
+ return;
1547
+ }
1548
+ const pressureAuditEntries = [
1549
+ ['freePorePressureResidualL1M3PerS', 'free-pore-pressure-residual-l1', 'Free pore-pressure residual L1 norm'],
1550
+ ['prescribedPorePressureResidualL1M3PerS', 'prescribed-pore-pressure-residual-l1', 'Prescribed pore-pressure residual L1 norm'],
1551
+ ['netPrescribedPressureBoundaryFlowM3PerS', 'net-prescribed-pressure-boundary-flow', 'Net prescribed pressure boundary flow'],
1552
+ ['appliedNodalFluxSumM3PerS', 'applied-nodal-flux-sum', 'Applied nodal flux sum'],
1553
+ ['storageRateSumM3PerS', 'storage-rate-sum', 'Storage-rate sum'],
1554
+ ['couplingRateSumM3PerS', 'coupling-rate-sum', 'Coupling-rate sum'],
1555
+ ['darcyFlowRateSumM3PerS', 'darcy-flow-rate-sum', 'Darcy flow-rate sum'],
1556
+ ];
1557
+ for (const [key, code, label] of pressureAuditEntries) {
1558
+ pushFiniteNumberFinding(findings, pressureAudit[key], `${codePrefix}.${code}`, label);
1559
+ }
1560
+ pushApproximateMatchFinding(findings, pressureAudit.freePorePressureResidualL1M3PerS, envelope.freePorePressureResidualL1M3PerS, `${codePrefix}.free-residual-envelope-mismatch`, 'Pressure-audit free residual', 1e-12);
1561
+ pushApproximateMatchFinding(findings, pressureAudit.prescribedPorePressureResidualL1M3PerS, envelope.prescribedPorePressureResidualL1M3PerS, `${codePrefix}.prescribed-residual-envelope-mismatch`, 'Pressure-audit prescribed residual', 1e-12);
1562
+ }
1563
+ function validateBiotTransientAcceptanceEnvelope(findings, manifest, options) {
1564
+ const { envelope } = manifest;
1565
+ const transientAcceptance = manifest.biotTransientAcceptance;
1566
+ const codePrefix = options.context === 'biot'
1567
+ ? 'result.biot-transient-acceptance'
1568
+ : 'result.dp-biot-replay.biot-transient-acceptance';
1569
+ if (!transientAcceptance) {
1570
+ findings.push(finding('blocker', `${codePrefix}.missing`, options.context === 'biot'
1571
+ ? 'Biot result manifests must include transient acceptance metadata.'
1572
+ : 'DP Biot pressure-replay manifests must include upstream Biot transient acceptance metadata.'));
1573
+ return;
1574
+ }
1575
+ if (transientAcceptance.schemaVersion !== 'fem-plane-strain-biot-transient-acceptance.v1') {
1576
+ findings.push(finding('blocker', `${codePrefix}.schema.unsupported`, 'Unsupported Biot transient acceptance schema.'));
1577
+ }
1578
+ if (transientAcceptance.accepted !== true) {
1579
+ findings.push(finding('blocker', `${codePrefix}.not-accepted`, 'Biot transient acceptance must be accepted before publishing a preview manifest.'));
1580
+ }
1581
+ if (transientAcceptance.dissipationCheckMode !== 'drained-dissipation' &&
1582
+ transientAcceptance.dissipationCheckMode !== 'prescribed-gradient-relaxation' &&
1583
+ transientAcceptance.dissipationCheckMode !== 'load-generated-consolidation') {
1584
+ findings.push(finding('blocker', `${codePrefix}.mode.invalid`, 'Biot transient acceptance mode is invalid.'));
1585
+ }
1586
+ const pressureEnvelopeMode = transientAcceptance.pressureEnvelopeMode ??
1587
+ (transientAcceptance.dissipationCheckMode === 'load-generated-consolidation'
1588
+ ? 'load-generated-positive-pressure'
1589
+ : 'initial-prescribed-bound');
1590
+ if (pressureEnvelopeMode !== 'initial-prescribed-bound' &&
1591
+ pressureEnvelopeMode !== 'load-generated-positive-pressure') {
1592
+ findings.push(finding('blocker', `${codePrefix}.pressure-envelope-mode.invalid`, 'Biot transient pressure envelope mode is invalid.'));
1593
+ }
1594
+ const acceptedStepCountOk = pushFiniteNumberFinding(findings, transientAcceptance.acceptedStepCount, `${codePrefix}.accepted-step-count`, 'Biot transient accepted step count', { positive: true });
1595
+ const requiredStepCountOk = pushFiniteNumberFinding(findings, transientAcceptance.requiredStepCount, `${codePrefix}.required-step-count`, 'Biot transient required step count', { positive: true });
1596
+ const maxResidualOk = pushFiniteNumberFinding(findings, transientAcceptance.maxResidualNormRatio, `${codePrefix}.max-residual-ratio`, 'Biot transient maximum residual ratio', { nonNegative: true });
1597
+ const maxMassBalanceOk = pushFiniteNumberFinding(findings, transientAcceptance.maxMassBalanceErrorRatio, `${codePrefix}.max-mass-balance-ratio`, 'Biot transient maximum mass-balance ratio', { nonNegative: true });
1598
+ pushFiniteNumberFinding(findings, transientAcceptance.maxPressureOvershootKpa, `${codePrefix}.max-pressure-overshoot`, 'Biot transient maximum pressure overshoot', { nonNegative: true });
1599
+ const finalDissipationOk = pushFiniteNumberFinding(findings, transientAcceptance.finalPorePressureDissipationRatio, `${codePrefix}.final-dissipation-ratio`, 'Biot transient final pore-pressure dissipation ratio', { nonNegative: true });
1600
+ if (acceptedStepCountOk && isFiniteNumber(envelope.timeStepCount) && transientAcceptance.acceptedStepCount !== envelope.timeStepCount) {
1601
+ findings.push(finding('blocker', `${codePrefix}.accepted-step-count-mismatch`, 'Biot accepted step count must match the envelope time-step count.'));
1602
+ }
1603
+ if (acceptedStepCountOk &&
1604
+ options.expectedAcceptedStepCount != null &&
1605
+ transientAcceptance.acceptedStepCount !== options.expectedAcceptedStepCount) {
1606
+ findings.push(finding('blocker', `${codePrefix}.accepted-step-count-audit-mismatch`, 'Biot accepted step count must match the pressure-replay audit source step count.'));
1607
+ }
1608
+ if (requiredStepCountOk && transientAcceptance.requiredStepCount < FEM_MIN_BIOT_TRANSIENT_STEPS) {
1609
+ findings.push(finding('blocker', `${codePrefix}.required-step-count-too-small`, 'Biot required transient step count is below the preview policy.'));
1610
+ }
1611
+ if (acceptedStepCountOk && requiredStepCountOk && transientAcceptance.acceptedStepCount < transientAcceptance.requiredStepCount) {
1612
+ findings.push(finding('blocker', `${codePrefix}.accepted-step-count-too-small`, 'Biot accepted step count is below the required transient step count.'));
1613
+ }
1614
+ if (maxResidualOk && transientAcceptance.maxResidualNormRatio > 1e-3) {
1615
+ findings.push(finding('blocker', `${codePrefix}.max-residual-too-large`, 'Biot transient maximum residual ratio exceeds the preview tolerance.'));
1616
+ }
1617
+ if (maxMassBalanceOk && transientAcceptance.maxMassBalanceErrorRatio > 1e-3) {
1618
+ findings.push(finding('blocker', `${codePrefix}.max-mass-balance-too-large`, 'Biot transient maximum mass-balance ratio exceeds the preview tolerance.'));
1619
+ }
1620
+ if (finalDissipationOk && isFiniteNumber(envelope.porePressureDissipationRatio)) {
1621
+ pushApproximateMatchFinding(findings, transientAcceptance.finalPorePressureDissipationRatio, envelope.porePressureDissipationRatio, `${codePrefix}.final-dissipation-envelope-mismatch`, 'Biot transient final dissipation ratio', 1e-12);
1622
+ }
1623
+ if (options.requireDrainedMonotonicChecks &&
1624
+ transientAcceptance.dissipationCheckMode === 'drained-dissipation' &&
1625
+ transientAcceptance.monotonicAverageFreePressureDissipationRequired !== true) {
1626
+ findings.push(finding('blocker', `${codePrefix}.drained-monotonic-required`, 'Drained-dissipation Biot acceptance must require monotonic average free pore-pressure dissipation.'));
1627
+ }
1628
+ if (transientAcceptance.monotonicAverageFreePressureDissipationRequired &&
1629
+ transientAcceptance.monotonicAverageFreePressureDissipation !== true) {
1630
+ findings.push(finding('blocker', `${codePrefix}.average-free-pressure-not-monotonic`, 'Required average free pore-pressure dissipation was not monotonic.'));
1631
+ }
1632
+ if (options.requireDrainedMonotonicChecks &&
1633
+ pressureEnvelopeMode === 'initial-prescribed-bound' &&
1634
+ transientAcceptance.monotonicMaxPressureEnvelope !== true) {
1635
+ findings.push(finding('blocker', `${codePrefix}.max-pressure-not-monotonic`, 'Biot maximum pore-pressure envelope must be monotonic non-increasing.'));
1636
+ }
1637
+ if (!Array.isArray(transientAcceptance.blockerCodes)) {
1638
+ findings.push(finding('blocker', `${codePrefix}.blocker-codes.invalid`, 'Biot transient acceptance blocker codes must be an array.'));
1639
+ }
1640
+ else if (transientAcceptance.accepted && transientAcceptance.blockerCodes.length > 0) {
1641
+ findings.push(finding('blocker', `${codePrefix}.blocker-codes-not-empty`, 'Accepted Biot transient metadata must not include blocker codes.'));
1642
+ }
1643
+ }
1644
+ function validatePlaneStrainDpBiotPressureReplayAcceptance(findings, manifest) {
1645
+ const { envelope, analysisCase } = manifest;
1646
+ const biot = analysisCase.geometry.biot;
1647
+ if (!biot) {
1648
+ findings.push(finding('blocker', 'result.dp-biot-replay.geometry-biot-missing', 'DP Biot pressure-replay manifests must embed the reviewed Biot pressure-source geometry.'));
1649
+ return;
1650
+ }
1651
+ const audit = manifest.pressureReplayAudit;
1652
+ if (!isRecord(audit)) {
1653
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.missing', 'DP Biot pressure-replay manifests must include pressureReplayAudit metadata.'));
1654
+ return;
1655
+ }
1656
+ if (audit.schemaVersion !== 'fem-plane-strain-dp-biot-pressure-replay-audit.v1') {
1657
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.schema.unsupported', 'Unsupported DP Biot pressure-replay audit schema.'));
1658
+ }
1659
+ if (audit.mode !== 'sequential-one-way-biot-pressure-replay') {
1660
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.mode.invalid', 'DP Biot pressure replay must declare sequential one-way replay mode.'));
1661
+ }
1662
+ if (audit.pressureFrameSource !== 'final-biot-step') {
1663
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.pressure-frame-source.invalid', 'DP Biot pressure replay must use the final Biot step as the pressure-frame source.'));
1664
+ }
1665
+ if (!isNonEmptyString(audit.sourceMethod)) {
1666
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.source-method.missing', 'DP Biot pressure replay must record the upstream Biot source method.'));
1667
+ }
1668
+ if (audit.sourceTransientAccepted !== true) {
1669
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.source-transient-not-accepted', 'DP Biot pressure replay requires an accepted upstream transient source.'));
1670
+ }
1671
+ if (!Array.isArray(audit.sourceTransientBlockerCodes)) {
1672
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.source-blockers.invalid', 'DP Biot pressure replay source blocker codes must be an array.'));
1673
+ }
1674
+ else if (audit.sourceTransientBlockerCodes.length > 0) {
1675
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.source-blockers-not-empty', 'Accepted DP Biot pressure replay source must not include blocker codes.'));
1676
+ }
1677
+ const sourceStepsOk = pushFiniteNumberFinding(findings, audit.sourceAcceptedStepCount, 'result.dp-biot-replay.audit.source-accepted-step-count', 'Pressure-replay source accepted step count', { positive: true });
1678
+ const sourcePressureDofsOk = pushFiniteNumberFinding(findings, audit.sourcePorePressureDofCount, 'result.dp-biot-replay.audit.source-pore-pressure-dof-count', 'Pressure-replay source pore-pressure DOF count', { positive: true });
1679
+ const replayNodeCountOk = pushFiniteNumberFinding(findings, audit.replayNodeCount, 'result.dp-biot-replay.audit.replay-node-count', 'Pressure-replay node count', { positive: true });
1680
+ const sourceMassBalanceOk = pushFiniteNumberFinding(findings, audit.sourceMassBalanceErrorRatio, 'result.dp-biot-replay.audit.source-mass-balance-ratio', 'Pressure-replay source mass-balance ratio', { nonNegative: true });
1681
+ const sourceResidualOk = pushFiniteNumberFinding(findings, audit.sourceResidualNormRatio, 'result.dp-biot-replay.audit.source-residual-ratio', 'Pressure-replay source residual ratio', { nonNegative: true });
1682
+ pushFiniteNumberFinding(findings, audit.pressureScale, 'result.dp-biot-replay.audit.pressure-scale', 'Pressure-replay scale', { nonNegative: true });
1683
+ pushFiniteNumberFinding(findings, audit.maxInputPorePressureKpa, 'result.dp-biot-replay.audit.max-input-pore-pressure', 'Pressure-replay maximum input pore pressure', { nonNegative: true });
1684
+ const auditCouplingOk = pushFiniteNumberFinding(findings, audit.maxAppliedEffectiveStressReductionKpa, 'result.dp-biot-replay.audit.max-applied-effective-stress-reduction', 'Pressure-replay maximum applied effective-stress reduction', { nonNegative: true });
1685
+ if (!Array.isArray(audit.limitations) || audit.limitations.length === 0) {
1686
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.limitations.missing', 'DP Biot pressure-replay audit must include explicit limitations.'));
1687
+ }
1688
+ else {
1689
+ const limitationText = audit.limitations.join(' ').toLowerCase();
1690
+ if (!limitationText.includes('sequential one-way') || !limitationText.includes('no pore-pressure dofs')) {
1691
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.limitations.incomplete', 'DP Biot pressure-replay audit limitations must disclose sequential one-way replay and no pore-pressure DOFs.'));
1692
+ }
1693
+ }
1694
+ for (const [ok, value, code, label] of [
1695
+ [sourceStepsOk, audit.sourceAcceptedStepCount, 'source-accepted-step-count', 'source accepted step count'],
1696
+ [sourcePressureDofsOk, audit.sourcePorePressureDofCount, 'source-pore-pressure-dof-count', 'source pore-pressure DOF count'],
1697
+ [replayNodeCountOk, audit.replayNodeCount, 'replay-node-count', 'replay node count'],
1698
+ ]) {
1699
+ if (ok && !Number.isInteger(value)) {
1700
+ findings.push(finding('blocker', `result.dp-biot-replay.audit.${code}.integer`, `Pressure-replay ${label} must be an integer.`));
1701
+ }
1702
+ }
1703
+ if (replayNodeCountOk && audit.replayNodeCount !== manifest.mesh.nodes) {
1704
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.replay-node-count-mismatch', 'Pressure-replay node count must match the manifest mesh node count.'));
1705
+ }
1706
+ if (sourcePressureDofsOk && audit.sourcePorePressureDofCount !== manifest.mesh.nodes) {
1707
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.source-pore-pressure-dof-count-mismatch', 'Pressure-replay source pore-pressure DOF count must match the pressure-source mesh node count.'));
1708
+ }
1709
+ if (sourceStepsOk && audit.sourceAcceptedStepCount !== biot.timeStepsSeconds.length) {
1710
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.source-step-count-mismatch', 'Pressure-replay source accepted step count must match Biot time steps.'));
1711
+ }
1712
+ if (sourceMassBalanceOk && audit.sourceMassBalanceErrorRatio > 1e-3) {
1713
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.source-mass-balance-too-large', 'Pressure-replay source mass-balance ratio exceeds the preview tolerance.'));
1714
+ }
1715
+ if (sourceResidualOk && audit.sourceResidualNormRatio > 1e-3) {
1716
+ findings.push(finding('blocker', 'result.dp-biot-replay.audit.source-residual-too-large', 'Pressure-replay source residual ratio exceeds the preview tolerance.'));
1717
+ }
1718
+ const minPorePressureOk = pushFiniteNumberFinding(findings, envelope.minPorePressureKpa, 'result.envelope.dp-biot-replay.min-pore-pressure', 'Pressure-replay envelope minimum pore pressure', { nonNegative: true });
1719
+ const maxPorePressureOk = pushFiniteNumberFinding(findings, envelope.maxPorePressureKpa, 'result.envelope.dp-biot-replay.max-pore-pressure', 'Pressure-replay envelope maximum pore pressure', { nonNegative: true });
1720
+ const maxExcessOk = pushFiniteNumberFinding(findings, envelope.maxExcessPorePressureKpa, 'result.envelope.dp-biot-replay.max-excess-pore-pressure', 'Pressure-replay envelope maximum excess pore pressure', { nonNegative: true });
1721
+ const maxCouplingOk = pushFiniteNumberFinding(findings, envelope.maxBiotCouplingKpa, 'result.envelope.dp-biot-replay.max-biot-coupling', 'Pressure-replay envelope maximum Biot coupling', { nonNegative: true });
1722
+ const massBalanceOk = pushFiniteNumberFinding(findings, envelope.porePressureMassBalanceErrorRatio, 'result.envelope.dp-biot-replay.mass-balance-ratio', 'Pressure-replay envelope pore-pressure mass-balance ratio', { nonNegative: true });
1723
+ pushFiniteNumberFinding(findings, envelope.maxFreePorePressureResidualM3PerS, 'result.envelope.dp-biot-replay.max-free-pore-pressure-residual', 'Pressure-replay maximum free pore-pressure residual', { nonNegative: true });
1724
+ pushFiniteNumberFinding(findings, envelope.freePorePressureResidualL1M3PerS, 'result.envelope.dp-biot-replay.free-pore-pressure-residual-l1', 'Pressure-replay free pore-pressure residual L1 norm', { nonNegative: true });
1725
+ pushFiniteNumberFinding(findings, envelope.prescribedPorePressureResidualL1M3PerS, 'result.envelope.dp-biot-replay.prescribed-pore-pressure-residual-l1', 'Pressure-replay prescribed pore-pressure residual L1 norm', { nonNegative: true });
1726
+ const averagePorePressureOk = pushFiniteNumberFinding(findings, envelope.averagePorePressureKpa, 'result.envelope.dp-biot-replay.average-pore-pressure', 'Pressure-replay average pore pressure', { nonNegative: true });
1727
+ const averageFreePorePressureOk = pushFiniteNumberFinding(findings, envelope.averageFreePorePressureKpa, 'result.envelope.dp-biot-replay.average-free-pore-pressure', 'Pressure-replay average free pore pressure', { nonNegative: true });
1728
+ const dissipationRatioOk = pushFiniteNumberFinding(findings, envelope.porePressureDissipationRatio, 'result.envelope.dp-biot-replay.pore-pressure-dissipation-ratio', 'Pressure-replay pore-pressure dissipation ratio', { nonNegative: true });
1729
+ pushFiniteNumberFinding(findings, envelope.maxPorePressureChangeRateKpaPerS, 'result.envelope.dp-biot-replay.max-pore-pressure-change-rate', 'Pressure-replay maximum pore-pressure change rate', { nonNegative: true });
1730
+ const timeStepCountOk = pushFiniteNumberFinding(findings, envelope.timeStepCount, 'result.envelope.dp-biot-replay.time-step-count', 'Pressure-replay envelope time-step count', { positive: true });
1731
+ const coupledUnknownsOk = pushFiniteNumberFinding(findings, envelope.coupledUnknownCount, 'result.envelope.dp-biot-replay.coupled-unknown-count', 'Pressure-replay coupled unknown count', { positive: true });
1732
+ const displacementDofsOk = pushFiniteNumberFinding(findings, envelope.displacementDofCount, 'result.envelope.dp-biot-replay.displacement-dof-count', 'Pressure-replay displacement DOF count', { positive: true });
1733
+ const pressureDofsOk = pushFiniteNumberFinding(findings, envelope.porePressureDofCount, 'result.envelope.dp-biot-replay.pore-pressure-dof-count', 'Pressure-replay nonlinear pore-pressure DOF count', { nonNegative: true });
1734
+ if (minPorePressureOk && maxPorePressureOk && envelope.minPorePressureKpa > envelope.maxPorePressureKpa) {
1735
+ findings.push(finding('blocker', 'result.envelope.dp-biot-replay.pore-pressure-range-invalid', 'Pressure-replay minimum pore pressure cannot exceed maximum pore pressure.'));
1736
+ }
1737
+ if (maxExcessOk && maxPorePressureOk) {
1738
+ pushApproximateMatchFinding(findings, envelope.maxExcessPorePressureKpa, envelope.maxPorePressureKpa, 'result.envelope.dp-biot-replay.max-excess-pore-pressure-mismatch', 'Pressure-replay maximum excess pore pressure', 1e-6);
1739
+ }
1740
+ if (averagePorePressureOk && minPorePressureOk && maxPorePressureOk && (envelope.averagePorePressureKpa < envelope.minPorePressureKpa - 1e-6 ||
1741
+ envelope.averagePorePressureKpa > envelope.maxPorePressureKpa + 1e-6)) {
1742
+ findings.push(finding('blocker', 'result.envelope.dp-biot-replay.average-pore-pressure-range-invalid', 'Pressure-replay average pore pressure must stay within the reported range.'));
1743
+ }
1744
+ if (averageFreePorePressureOk && maxPorePressureOk && envelope.averageFreePorePressureKpa > envelope.maxPorePressureKpa + 1e-6) {
1745
+ findings.push(finding('blocker', 'result.envelope.dp-biot-replay.average-free-pore-pressure-range-invalid', 'Pressure-replay average free pore pressure cannot exceed the maximum pore pressure.'));
1746
+ }
1747
+ if (dissipationRatioOk && envelope.porePressureDissipationRatio > 1) {
1748
+ findings.push(finding('blocker', 'result.envelope.dp-biot-replay.dissipation-ratio-invalid', 'Pressure-replay pore-pressure dissipation ratio must be between 0 and 1.'));
1749
+ }
1750
+ if (timeStepCountOk && (!Number.isInteger(envelope.timeStepCount) || envelope.timeStepCount !== biot.timeStepsSeconds.length)) {
1751
+ findings.push(finding('blocker', 'result.envelope.dp-biot-replay.time-step-count-mismatch', 'Pressure-replay envelope time-step count must match Biot time steps.'));
1752
+ }
1753
+ if (massBalanceOk && envelope.porePressureMassBalanceErrorRatio > 1e-3) {
1754
+ findings.push(finding('blocker', 'result.envelope.dp-biot-replay.mass-balance-too-large', 'Pressure-replay mass-balance ratio exceeds the preview tolerance.'));
1755
+ }
1756
+ for (const [ok, value, code, label] of [
1757
+ [coupledUnknownsOk, envelope.coupledUnknownCount, 'coupled-unknown-count', 'coupled unknown count'],
1758
+ [displacementDofsOk, envelope.displacementDofCount, 'displacement-dof-count', 'displacement DOF count'],
1759
+ [pressureDofsOk, envelope.porePressureDofCount, 'pore-pressure-dof-count', 'pore-pressure DOF count'],
1760
+ ]) {
1761
+ if (ok && !Number.isInteger(value)) {
1762
+ findings.push(finding('blocker', `result.envelope.dp-biot-replay.${code}.integer`, `Pressure-replay ${label} must be an integer.`));
1763
+ }
1764
+ }
1765
+ if (pressureDofsOk && envelope.porePressureDofCount !== 0) {
1766
+ findings.push(finding('blocker', 'result.envelope.dp-biot-replay.pore-pressure-dof-count-not-zero', 'DP Biot pressure replay must report zero pore-pressure DOFs in the nonlinear mechanical iterations.'));
1767
+ }
1768
+ if (coupledUnknownsOk && displacementDofsOk && envelope.coupledUnknownCount !== envelope.displacementDofCount) {
1769
+ findings.push(finding('blocker', 'result.envelope.dp-biot-replay.coupled-unknown-count-mismatch', 'DP Biot pressure replay coupled unknown count must equal displacement DOFs only.'));
1770
+ }
1771
+ if (maxCouplingOk && auditCouplingOk) {
1772
+ pushApproximateMatchFinding(findings, envelope.maxBiotCouplingKpa, audit.maxAppliedEffectiveStressReductionKpa, 'result.envelope.dp-biot-replay.max-coupling-audit-mismatch', 'Pressure-replay maximum Biot coupling', 1e-8);
1773
+ }
1774
+ validatePressureAuditEnvelope(findings, manifest, 'dp-biot-replay');
1775
+ validateBiotTransientAcceptanceEnvelope(findings, manifest, {
1776
+ context: 'dp-biot-replay',
1777
+ requireDrainedMonotonicChecks: false,
1778
+ expectedAcceptedStepCount: sourceStepsOk ? audit.sourceAcceptedStepCount : undefined,
1779
+ });
1780
+ }
1529
1781
  function validateResultEnvelopeSemantics(findings, manifest) {
1530
1782
  const { envelope, analysisCase } = manifest;
1531
1783
  const maxSettlementOk = pushFiniteNumberFinding(findings, envelope.maxSettlementMm, 'result.envelope.max-settlement', 'Envelope max settlement', { nonNegative: true });
@@ -1538,7 +1790,7 @@ function validateResultEnvelopeSemantics(findings, manifest) {
1538
1790
  }
1539
1791
  const expectedBackendByObjective = new Map([
1540
1792
  ['foundation_settlement', ['builtin-elastic3d-demo']],
1541
- ['excavation_deformation', ['builtin-staged-excavation-demo', FEM_PLANE_STRAIN_DP_ADAPTIVE_BACKEND_ID]],
1793
+ ['excavation_deformation', ['builtin-staged-excavation-demo', FEM_PLANE_STRAIN_DP_ADAPTIVE_BACKEND_ID, FEM_PLANE_STRAIN_DP_BIOT_REPLAY_BACKEND_ID]],
1542
1794
  ['tunnel_volume_loss_settlement', ['builtin-tunnel-volume-loss-demo']],
1543
1795
  ['staged_settlement_consolidation', ['builtin-staged-consolidation-1d', 'builtin-nonlinear-column-v0']],
1544
1796
  ['seepage_groundwater_coupling', ['builtin-biot-up-plane-strain-v0']],
@@ -1547,7 +1799,10 @@ function validateResultEnvelopeSemantics(findings, manifest) {
1547
1799
  if (expectedBackends && !expectedBackends.includes(manifest.backend.id)) {
1548
1800
  findings.push(finding('blocker', 'result.backend.objective-mismatch', 'Result backend must match the embedded FEM objective.'));
1549
1801
  }
1550
- if (analysisCase.objective !== 'seepage_groundwater_coupling') {
1802
+ const isPlaneStrainDpAdaptiveManifest = manifest.backend.id === FEM_PLANE_STRAIN_DP_ADAPTIVE_BACKEND_ID;
1803
+ const isPlaneStrainDpBiotReplayManifest = manifest.backend.id === FEM_PLANE_STRAIN_DP_BIOT_REPLAY_BACKEND_ID;
1804
+ const isPlaneStrainDpManifest = isPlaneStrainDpAdaptiveManifest || isPlaneStrainDpBiotReplayManifest;
1805
+ if (analysisCase.objective !== 'seepage_groundwater_coupling' && !isPlaneStrainDpBiotReplayManifest) {
1551
1806
  if (manifest.pressureAudit != null) {
1552
1807
  findings.push(finding('blocker', 'result.pressure-audit.unexpected', 'Pressure audit is only valid for Biot u-p seepage result manifests.'));
1553
1808
  }
@@ -1555,11 +1810,13 @@ function validateResultEnvelopeSemantics(findings, manifest) {
1555
1810
  findings.push(finding('blocker', 'result.biot-transient-acceptance.unexpected', 'Biot transient acceptance metadata is only valid for Biot u-p seepage result manifests.'));
1556
1811
  }
1557
1812
  }
1558
- const isPlaneStrainDpAdaptiveManifest = manifest.backend.id === FEM_PLANE_STRAIN_DP_ADAPTIVE_BACKEND_ID;
1559
- if (!isPlaneStrainDpAdaptiveManifest && manifest.adaptiveLoadStepping != null) {
1813
+ if (!isPlaneStrainDpManifest && manifest.adaptiveLoadStepping != null) {
1560
1814
  findings.push(finding('blocker', 'result.dp-adaptive.unexpected', 'Drucker-Prager adaptive metadata is only valid for plane-strain DP adaptive result manifests.'));
1561
1815
  }
1562
- if (isPlaneStrainDpAdaptiveManifest) {
1816
+ if (!isPlaneStrainDpBiotReplayManifest && manifest.pressureReplayAudit != null) {
1817
+ findings.push(finding('blocker', 'result.dp-biot-replay.unexpected', 'Biot pressure-replay audit metadata is only valid for the plane-strain DP Biot replay backend.'));
1818
+ }
1819
+ if (isPlaneStrainDpManifest) {
1563
1820
  if (analysisCase.analysisType !== FEM_PLANE_STRAIN_DP_ANALYSIS_TYPE) {
1564
1821
  findings.push(finding('blocker', 'result.dp-adaptive.analysis-type-mismatch', 'Plane-strain DP adaptive manifests require static_2d_plane_strain_drucker_prager analysis cases.'));
1565
1822
  }
@@ -1623,6 +1880,9 @@ function validateResultEnvelopeSemantics(findings, manifest) {
1623
1880
  const solverTolerances = validateNonlinearSolverConvergenceReport(findings, manifest, expectedDpLoadSteps);
1624
1881
  validatePlaneStrainDpAdaptiveAcceptance(findings, manifest, solverTolerances);
1625
1882
  }
1883
+ if (isPlaneStrainDpBiotReplayManifest) {
1884
+ validatePlaneStrainDpBiotPressureReplayAcceptance(findings, manifest);
1885
+ }
1626
1886
  return;
1627
1887
  }
1628
1888
  if (analysisCase.objective === 'tunnel_volume_loss_settlement') {
@@ -1897,6 +2157,7 @@ export function validateFemResultManifest(manifest) {
1897
2157
  'builtin-nonlinear-column-v0',
1898
2158
  'builtin-biot-up-plane-strain-v0',
1899
2159
  FEM_PLANE_STRAIN_DP_ADAPTIVE_BACKEND_ID,
2160
+ FEM_PLANE_STRAIN_DP_BIOT_REPLAY_BACKEND_ID,
1900
2161
  ]);
1901
2162
  if (!validBackendIds.has(manifest.backend.id)) {
1902
2163
  findings.push(finding('blocker', 'result.backend.id-invalid', `Unsupported FEM result backend: ${String(manifest.backend.id)}.`));