@geotechcli/core 0.4.100 → 0.4.102
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.
- package/dist/agents/fem-tools.js +11 -0
- package/dist/agents/fem-tools.js.map +1 -1
- package/dist/fem/engineering-evidence.d.ts +1 -1
- package/dist/fem/engineering-evidence.d.ts.map +1 -1
- package/dist/fem/engineering-evidence.js +50 -2
- package/dist/fem/engineering-evidence.js.map +1 -1
- package/dist/fem/index.d.ts +1 -1
- package/dist/fem/index.d.ts.map +1 -1
- package/dist/fem/index.js +1 -1
- package/dist/fem/index.js.map +1 -1
- package/dist/fem/plane-strain-assembly.d.ts +87 -0
- package/dist/fem/plane-strain-assembly.d.ts.map +1 -1
- package/dist/fem/plane-strain-assembly.js +439 -0
- package/dist/fem/plane-strain-assembly.js.map +1 -1
- package/dist/fem/production-readiness.d.ts.map +1 -1
- package/dist/fem/production-readiness.js +4 -3
- package/dist/fem/production-readiness.js.map +1 -1
- package/dist/meta/metadata.json +1 -1
- package/package.json +1 -1
|
@@ -370,6 +370,15 @@ function hydraulicElementMatrices(input) {
|
|
|
370
370
|
}
|
|
371
371
|
return { conductivity, areaM2, gauss };
|
|
372
372
|
}
|
|
373
|
+
function validateBiotMaterial(material) {
|
|
374
|
+
const hydraulic = validateHydraulicMaterial(material);
|
|
375
|
+
const specificStorage1PerM = material.specificStorage1PerM;
|
|
376
|
+
if (specificStorage1PerM == null) {
|
|
377
|
+
throw new Error(`material ${material.id} specificStorage1PerM is required for plane-strain Biot consolidation.`);
|
|
378
|
+
}
|
|
379
|
+
assertFinitePositive(specificStorage1PerM, `material ${material.id} specificStorage1PerM`);
|
|
380
|
+
return { ...hydraulic, specificStorage1PerM };
|
|
381
|
+
}
|
|
373
382
|
export function buildPlaneStrainRectangularMesh(input) {
|
|
374
383
|
assertFinitePositive(input.widthM, 'widthM');
|
|
375
384
|
assertFinitePositive(input.heightM, 'heightM');
|
|
@@ -604,6 +613,436 @@ export function runPlaneStrainSteadySeepage(model) {
|
|
|
604
613
|
],
|
|
605
614
|
};
|
|
606
615
|
}
|
|
616
|
+
export function runPlaneStrainBiotConsolidation(model) {
|
|
617
|
+
if (model.schemaVersion !== 'fem-plane-strain-biot-consolidation-model.v1') {
|
|
618
|
+
throw new Error('Only fem-plane-strain-biot-consolidation-model.v1 is supported.');
|
|
619
|
+
}
|
|
620
|
+
assertArray(model.nodes, 'nodes');
|
|
621
|
+
assertArray(model.elements, 'elements');
|
|
622
|
+
assertArray(model.materials, 'materials');
|
|
623
|
+
assertArray(model.boundaryConditions, 'boundaryConditions');
|
|
624
|
+
assertArray(model.porePressureBoundaryConditions, 'porePressureBoundaryConditions');
|
|
625
|
+
assertArray(model.timeStepsSeconds, 'timeStepsSeconds');
|
|
626
|
+
if (model.nodes.length < 4)
|
|
627
|
+
throw new Error('Plane-strain Biot consolidation model requires at least four nodes.');
|
|
628
|
+
if (model.elements.length < 1)
|
|
629
|
+
throw new Error('Plane-strain Biot consolidation model requires at least one element.');
|
|
630
|
+
if (model.materials.length < 1)
|
|
631
|
+
throw new Error('Plane-strain Biot consolidation model requires at least one material.');
|
|
632
|
+
if (model.timeStepsSeconds.length < 1) {
|
|
633
|
+
throw new Error('Plane-strain Biot consolidation model requires at least one time step.');
|
|
634
|
+
}
|
|
635
|
+
if (model.defaultThicknessM != null)
|
|
636
|
+
assertFinitePositive(model.defaultThicknessM, 'defaultThicknessM');
|
|
637
|
+
if (model.nodalLoads != null)
|
|
638
|
+
assertArray(model.nodalLoads, 'nodalLoads');
|
|
639
|
+
if (model.nodalFluxes != null)
|
|
640
|
+
assertArray(model.nodalFluxes, 'nodalFluxes');
|
|
641
|
+
const gammaWaterKpaPerM = model.gammaWaterKpaPerM ?? 9.81;
|
|
642
|
+
assertFinitePositive(gammaWaterKpaPerM, 'gammaWaterKpaPerM');
|
|
643
|
+
const initialPorePressureKpa = model.initialPorePressureKpa ?? 0;
|
|
644
|
+
assertFiniteNonNegative(initialPorePressureKpa, 'initialPorePressureKpa');
|
|
645
|
+
assertUniqueIds(model.nodes, 'node');
|
|
646
|
+
assertUniqueIds(model.materials, 'material');
|
|
647
|
+
assertUniqueIds(model.elements, 'element');
|
|
648
|
+
const policy = model.policy ?? DEFAULT_FEM_CONVERGENCE_POLICY;
|
|
649
|
+
validateConvergencePolicy(policy);
|
|
650
|
+
const nodeIndexById = new Map(model.nodes.map((node, index) => [node.id, index]));
|
|
651
|
+
const materialById = new Map(model.materials.map((material) => [material.id, material]));
|
|
652
|
+
const displacementDofCount = model.nodes.length * 2;
|
|
653
|
+
const porePressureDofCount = model.nodes.length;
|
|
654
|
+
if (displacementDofCount + porePressureDofCount > MAX_DENSE_DOF_COUNT) {
|
|
655
|
+
throw new Error(`Plane-strain Biot consolidation dense assembly is capped at ${MAX_DENSE_DOF_COUNT} coupled DOFs for benchmark-scale evidence runs.`);
|
|
656
|
+
}
|
|
657
|
+
let previousTimeSeconds = 0;
|
|
658
|
+
for (const [index, timeSeconds] of model.timeStepsSeconds.entries()) {
|
|
659
|
+
assertFinitePositive(timeSeconds, `timeStepsSeconds.${index}`);
|
|
660
|
+
if (timeSeconds <= previousTimeSeconds) {
|
|
661
|
+
throw new Error(`timeStepsSeconds.${index} must be strictly increasing.`);
|
|
662
|
+
}
|
|
663
|
+
previousTimeSeconds = timeSeconds;
|
|
664
|
+
}
|
|
665
|
+
for (const node of model.nodes) {
|
|
666
|
+
assertFinite(node.xM, `node ${node.id} xM`);
|
|
667
|
+
assertFinite(node.yM, `node ${node.id} yM`);
|
|
668
|
+
}
|
|
669
|
+
for (const material of model.materials) {
|
|
670
|
+
planeStrainD(material);
|
|
671
|
+
validateBiotMaterial(material);
|
|
672
|
+
if (material.unitWeightKnM3 != null) {
|
|
673
|
+
assertFiniteNonNegative(material.unitWeightKnM3, `material ${material.id} unitWeightKnM3`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const stiffness = Array.from({ length: displacementDofCount }, () => new Array(displacementDofCount).fill(0));
|
|
677
|
+
const coupling = Array.from({ length: displacementDofCount }, () => new Array(porePressureDofCount).fill(0));
|
|
678
|
+
const pressureConductivity = Array.from({ length: porePressureDofCount }, () => new Array(porePressureDofCount).fill(0));
|
|
679
|
+
const pressureStorage = Array.from({ length: porePressureDofCount }, () => new Array(porePressureDofCount).fill(0));
|
|
680
|
+
const loads = new Array(displacementDofCount).fill(0);
|
|
681
|
+
const fluxes = new Array(porePressureDofCount).fill(0);
|
|
682
|
+
for (const load of model.nodalLoads ?? []) {
|
|
683
|
+
const nodeIndex = nodeIndexById.get(load.nodeId);
|
|
684
|
+
if (nodeIndex == null)
|
|
685
|
+
throw new Error(`Unknown nodal load node: ${load.nodeId}.`);
|
|
686
|
+
const fxKn = load.fxKn ?? 0;
|
|
687
|
+
const fyKn = load.fyKn ?? 0;
|
|
688
|
+
assertFinite(fxKn, `nodal load ${load.nodeId}.fxKn`);
|
|
689
|
+
assertFinite(fyKn, `nodal load ${load.nodeId}.fyKn`);
|
|
690
|
+
loads[dofIndex(nodeIndex, 'ux')] += fxKn;
|
|
691
|
+
loads[dofIndex(nodeIndex, 'uy')] += fyKn;
|
|
692
|
+
}
|
|
693
|
+
for (const flux of model.nodalFluxes ?? []) {
|
|
694
|
+
const nodeIndex = nodeIndexById.get(flux.nodeId);
|
|
695
|
+
if (nodeIndex == null)
|
|
696
|
+
throw new Error(`Unknown Biot nodal flux node: ${flux.nodeId}.`);
|
|
697
|
+
const flowM3PerS = flux.flowM3PerS ?? 0;
|
|
698
|
+
assertFinite(flowM3PerS, `nodal flux ${flux.nodeId}.flowM3PerS`);
|
|
699
|
+
fluxes[nodeIndex] += flowM3PerS;
|
|
700
|
+
}
|
|
701
|
+
const elementGaussCache = [];
|
|
702
|
+
for (const element of model.elements) {
|
|
703
|
+
if (!Array.isArray(element.nodeIds) || element.nodeIds.length !== 4) {
|
|
704
|
+
throw new Error(`Plane-strain Biot element ${element.id} must reference exactly four nodes.`);
|
|
705
|
+
}
|
|
706
|
+
if (new Set(element.nodeIds).size !== 4) {
|
|
707
|
+
throw new Error(`Plane-strain Biot element ${element.id} has duplicate node references.`);
|
|
708
|
+
}
|
|
709
|
+
assertNonEmptyId(element.materialId, `element ${element.id} material`);
|
|
710
|
+
const nodeIndices = element.nodeIds.map((id) => {
|
|
711
|
+
const index = nodeIndexById.get(id);
|
|
712
|
+
if (index == null)
|
|
713
|
+
throw new Error(`Unknown Biot element node: ${id}.`);
|
|
714
|
+
return index;
|
|
715
|
+
});
|
|
716
|
+
const material = materialById.get(element.materialId);
|
|
717
|
+
if (!material)
|
|
718
|
+
throw new Error(`Unknown Biot element material: ${element.materialId}.`);
|
|
719
|
+
const biotMaterial = validateBiotMaterial(material);
|
|
720
|
+
const thicknessM = element.thicknessM ?? model.defaultThicknessM ?? 1;
|
|
721
|
+
assertFinitePositive(thicknessM, `element ${element.id} thicknessM`);
|
|
722
|
+
const nodes = nodeIndices.map((index) => model.nodes[index]);
|
|
723
|
+
const mechanical = elementMatrices({ nodes, material, thicknessM });
|
|
724
|
+
const hydraulic = hydraulicElementMatrices({ nodes, material, thicknessM });
|
|
725
|
+
const globalDofs = nodeIndices.flatMap((index) => [dofIndex(index, 'ux'), dofIndex(index, 'uy')]);
|
|
726
|
+
for (let localRow = 0; localRow < 8; localRow += 1) {
|
|
727
|
+
for (let localCol = 0; localCol < 8; localCol += 1) {
|
|
728
|
+
stiffness[globalDofs[localRow]][globalDofs[localCol]] += mechanical.stiffness[localRow][localCol];
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
for (let localRow = 0; localRow < 4; localRow += 1) {
|
|
732
|
+
for (let localCol = 0; localCol < 4; localCol += 1) {
|
|
733
|
+
pressureConductivity[nodeIndices[localRow]][nodeIndices[localCol]] +=
|
|
734
|
+
hydraulic.conductivity[localRow][localCol] / gammaWaterKpaPerM;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
for (const [gaussIndex, point] of mechanical.gauss.entries()) {
|
|
738
|
+
const hydraulicPoint = hydraulic.gauss[gaussIndex];
|
|
739
|
+
for (let localDof = 0; localDof < 8; localDof += 1) {
|
|
740
|
+
const volumetricShapeDerivative = point.b[0][localDof] + point.b[1][localDof];
|
|
741
|
+
for (let localPressure = 0; localPressure < 4; localPressure += 1) {
|
|
742
|
+
coupling[globalDofs[localDof]][nodeIndices[localPressure]] +=
|
|
743
|
+
biotMaterial.biotCoefficient *
|
|
744
|
+
volumetricShapeDerivative *
|
|
745
|
+
hydraulicPoint.shape[localPressure] *
|
|
746
|
+
point.detJ *
|
|
747
|
+
thicknessM;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
for (let localRow = 0; localRow < 4; localRow += 1) {
|
|
751
|
+
for (let localCol = 0; localCol < 4; localCol += 1) {
|
|
752
|
+
pressureStorage[nodeIndices[localRow]][nodeIndices[localCol]] +=
|
|
753
|
+
(biotMaterial.specificStorage1PerM / gammaWaterKpaPerM) *
|
|
754
|
+
hydraulicPoint.shape[localRow] *
|
|
755
|
+
hydraulicPoint.shape[localCol] *
|
|
756
|
+
point.detJ *
|
|
757
|
+
thicknessM;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
elementGaussCache.push({
|
|
762
|
+
element,
|
|
763
|
+
globalDofs,
|
|
764
|
+
globalNodes: nodeIndices,
|
|
765
|
+
mechanicalGauss: mechanical.gauss,
|
|
766
|
+
hydraulicGauss: hydraulic.gauss,
|
|
767
|
+
areaM2: mechanical.areaM2,
|
|
768
|
+
thicknessM,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
const prescribedDisplacements = new Map();
|
|
772
|
+
for (const bc of model.boundaryConditions) {
|
|
773
|
+
const nodeIndex = nodeIndexById.get(bc.nodeId);
|
|
774
|
+
if (nodeIndex == null)
|
|
775
|
+
throw new Error(`Unknown boundary-condition node: ${bc.nodeId}.`);
|
|
776
|
+
if (bc.dof !== 'ux' && bc.dof !== 'uy') {
|
|
777
|
+
throw new Error(`Boundary condition ${bc.nodeId} dof must be ux or uy.`);
|
|
778
|
+
}
|
|
779
|
+
const index = dofIndex(nodeIndex, bc.dof);
|
|
780
|
+
const value = bc.valueM ?? 0;
|
|
781
|
+
assertFinite(value, `boundary condition ${bc.nodeId}.${bc.dof}`);
|
|
782
|
+
const existing = prescribedDisplacements.get(index);
|
|
783
|
+
if (existing != null && Math.abs(existing - value) > 1e-12) {
|
|
784
|
+
throw new Error(`Conflicting boundary condition for ${bc.nodeId}.${bc.dof}.`);
|
|
785
|
+
}
|
|
786
|
+
prescribedDisplacements.set(index, value);
|
|
787
|
+
}
|
|
788
|
+
if (prescribedDisplacements.size === 0) {
|
|
789
|
+
throw new Error('Plane-strain Biot model requires at least one displacement boundary condition.');
|
|
790
|
+
}
|
|
791
|
+
const constrainedNodeIds = new Set(model.boundaryConditions.map((bc) => bc.nodeId));
|
|
792
|
+
const hasUxConstraint = model.boundaryConditions.some((bc) => bc.dof === 'ux');
|
|
793
|
+
const hasUyConstraint = model.boundaryConditions.some((bc) => bc.dof === 'uy');
|
|
794
|
+
if (prescribedDisplacements.size < 3 || constrainedNodeIds.size < 2 || !hasUxConstraint || !hasUyConstraint) {
|
|
795
|
+
throw new Error('Plane-strain Biot model has insufficient displacement constraints to restrain rigid-body modes.');
|
|
796
|
+
}
|
|
797
|
+
const prescribedPressures = new Map();
|
|
798
|
+
for (const bc of model.porePressureBoundaryConditions) {
|
|
799
|
+
const nodeIndex = nodeIndexById.get(bc.nodeId);
|
|
800
|
+
if (nodeIndex == null)
|
|
801
|
+
throw new Error(`Unknown pore-pressure boundary node: ${bc.nodeId}.`);
|
|
802
|
+
assertFiniteNonNegative(bc.porePressureKpa, `pore-pressure boundary ${bc.nodeId}.porePressureKpa`);
|
|
803
|
+
const existing = prescribedPressures.get(nodeIndex);
|
|
804
|
+
if (existing != null && Math.abs(existing - bc.porePressureKpa) > 1e-12) {
|
|
805
|
+
throw new Error(`Conflicting pore-pressure boundary condition for ${bc.nodeId}.`);
|
|
806
|
+
}
|
|
807
|
+
prescribedPressures.set(nodeIndex, bc.porePressureKpa);
|
|
808
|
+
}
|
|
809
|
+
if (prescribedPressures.size === 0) {
|
|
810
|
+
throw new Error('Plane-strain Biot consolidation model requires at least one pore-pressure boundary condition.');
|
|
811
|
+
}
|
|
812
|
+
const freeDisplacementDofs = Array.from({ length: displacementDofCount }, (_, index) => index)
|
|
813
|
+
.filter((index) => !prescribedDisplacements.has(index));
|
|
814
|
+
const freePressureDofs = Array.from({ length: porePressureDofCount }, (_, index) => index)
|
|
815
|
+
.filter((index) => !prescribedPressures.has(index));
|
|
816
|
+
const coupledUnknownCount = freeDisplacementDofs.length + freePressureDofs.length;
|
|
817
|
+
const pressureUnknownOffset = freeDisplacementDofs.length;
|
|
818
|
+
const displacement = new Array(displacementDofCount).fill(0);
|
|
819
|
+
const porePressure = new Array(porePressureDofCount).fill(initialPorePressureKpa);
|
|
820
|
+
for (const [index, value] of prescribedDisplacements)
|
|
821
|
+
displacement[index] = value;
|
|
822
|
+
for (const [index, value] of prescribedPressures)
|
|
823
|
+
porePressure[index] = value;
|
|
824
|
+
let previousDisplacement = [...displacement];
|
|
825
|
+
let previousPorePressure = [...porePressure];
|
|
826
|
+
let lastMechanicalResidual = new Array(displacementDofCount).fill(0);
|
|
827
|
+
let lastPressureResidual = new Array(porePressureDofCount).fill(0);
|
|
828
|
+
let lastResidualNormRatio = 0;
|
|
829
|
+
let lastMassBalanceErrorRatio = 0;
|
|
830
|
+
let lastMaxFreeResidualKn = 0;
|
|
831
|
+
let lastMaxFreePorePressureResidualM3PerS = 0;
|
|
832
|
+
const timeSteps = [];
|
|
833
|
+
let previousStepTime = 0;
|
|
834
|
+
for (const [stepIndex, timeSeconds] of model.timeStepsSeconds.entries()) {
|
|
835
|
+
const deltaTimeSeconds = timeSeconds - previousStepTime;
|
|
836
|
+
previousStepTime = timeSeconds;
|
|
837
|
+
for (const [index, value] of prescribedDisplacements)
|
|
838
|
+
displacement[index] = value;
|
|
839
|
+
for (const [index, value] of prescribedPressures)
|
|
840
|
+
porePressure[index] = value;
|
|
841
|
+
if (coupledUnknownCount > 0) {
|
|
842
|
+
const matrix = Array.from({ length: coupledUnknownCount }, () => new Array(coupledUnknownCount).fill(0));
|
|
843
|
+
const rhs = new Array(coupledUnknownCount).fill(0);
|
|
844
|
+
for (const [rowIndex, globalDof] of freeDisplacementDofs.entries()) {
|
|
845
|
+
for (const [colIndex, colDof] of freeDisplacementDofs.entries()) {
|
|
846
|
+
matrix[rowIndex][colIndex] = stiffness[globalDof][colDof];
|
|
847
|
+
}
|
|
848
|
+
for (const [colIndex, pressureDof] of freePressureDofs.entries()) {
|
|
849
|
+
matrix[rowIndex][pressureUnknownOffset + colIndex] = -coupling[globalDof][pressureDof];
|
|
850
|
+
}
|
|
851
|
+
rhs[rowIndex] = loads[globalDof];
|
|
852
|
+
for (const [knownDof, value] of prescribedDisplacements) {
|
|
853
|
+
rhs[rowIndex] -= stiffness[globalDof][knownDof] * value;
|
|
854
|
+
}
|
|
855
|
+
for (const [knownPressure, value] of prescribedPressures) {
|
|
856
|
+
rhs[rowIndex] += coupling[globalDof][knownPressure] * value;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
for (const [rowPressureIndex, pressureDof] of freePressureDofs.entries()) {
|
|
860
|
+
const rowIndex = pressureUnknownOffset + rowPressureIndex;
|
|
861
|
+
for (const [colIndex, displacementDof] of freeDisplacementDofs.entries()) {
|
|
862
|
+
matrix[rowIndex][colIndex] = coupling[displacementDof][pressureDof] / deltaTimeSeconds;
|
|
863
|
+
}
|
|
864
|
+
for (const [colIndex, colPressureDof] of freePressureDofs.entries()) {
|
|
865
|
+
matrix[rowIndex][pressureUnknownOffset + colIndex] =
|
|
866
|
+
pressureStorage[pressureDof][colPressureDof] / deltaTimeSeconds +
|
|
867
|
+
pressureConductivity[pressureDof][colPressureDof];
|
|
868
|
+
}
|
|
869
|
+
rhs[rowIndex] = fluxes[pressureDof];
|
|
870
|
+
for (let displacementDof = 0; displacementDof < displacementDofCount; displacementDof += 1) {
|
|
871
|
+
rhs[rowIndex] += coupling[displacementDof][pressureDof] *
|
|
872
|
+
previousDisplacement[displacementDof] /
|
|
873
|
+
deltaTimeSeconds;
|
|
874
|
+
}
|
|
875
|
+
for (let colPressureDof = 0; colPressureDof < porePressureDofCount; colPressureDof += 1) {
|
|
876
|
+
rhs[rowIndex] += pressureStorage[pressureDof][colPressureDof] *
|
|
877
|
+
previousPorePressure[colPressureDof] /
|
|
878
|
+
deltaTimeSeconds;
|
|
879
|
+
}
|
|
880
|
+
for (const [knownDof, value] of prescribedDisplacements) {
|
|
881
|
+
rhs[rowIndex] -= coupling[knownDof][pressureDof] * value / deltaTimeSeconds;
|
|
882
|
+
}
|
|
883
|
+
for (const [knownPressure, value] of prescribedPressures) {
|
|
884
|
+
rhs[rowIndex] -= (pressureStorage[pressureDof][knownPressure] / deltaTimeSeconds +
|
|
885
|
+
pressureConductivity[pressureDof][knownPressure]) * value;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const solved = solveDenseLinearSystem(matrix, rhs);
|
|
889
|
+
for (const [index, dof] of freeDisplacementDofs.entries())
|
|
890
|
+
displacement[dof] = solved[index];
|
|
891
|
+
for (const [index, dof] of freePressureDofs.entries()) {
|
|
892
|
+
porePressure[dof] = Math.max(0, solved[pressureUnknownOffset + index]);
|
|
893
|
+
}
|
|
894
|
+
for (const [index, value] of prescribedDisplacements)
|
|
895
|
+
displacement[index] = value;
|
|
896
|
+
for (const [index, value] of prescribedPressures)
|
|
897
|
+
porePressure[index] = value;
|
|
898
|
+
}
|
|
899
|
+
const elasticInternal = matVec(stiffness, displacement);
|
|
900
|
+
const couplingLoad = matVec(coupling, porePressure);
|
|
901
|
+
const mechanicalInternal = elasticInternal.map((value, index) => value - couplingLoad[index]);
|
|
902
|
+
const mechanicalResidual = mechanicalInternal.map((value, index) => value - loads[index]);
|
|
903
|
+
const pressureResidual = new Array(porePressureDofCount).fill(0);
|
|
904
|
+
for (let row = 0; row < porePressureDofCount; row += 1) {
|
|
905
|
+
let value = -fluxes[row];
|
|
906
|
+
for (let displacementDof = 0; displacementDof < displacementDofCount; displacementDof += 1) {
|
|
907
|
+
value += coupling[displacementDof][row] *
|
|
908
|
+
(displacement[displacementDof] - previousDisplacement[displacementDof]) /
|
|
909
|
+
deltaTimeSeconds;
|
|
910
|
+
}
|
|
911
|
+
for (let pressureDof = 0; pressureDof < porePressureDofCount; pressureDof += 1) {
|
|
912
|
+
value += pressureStorage[row][pressureDof] *
|
|
913
|
+
(porePressure[pressureDof] - previousPorePressure[pressureDof]) /
|
|
914
|
+
deltaTimeSeconds;
|
|
915
|
+
value += pressureConductivity[row][pressureDof] * porePressure[pressureDof];
|
|
916
|
+
}
|
|
917
|
+
pressureResidual[row] = value;
|
|
918
|
+
}
|
|
919
|
+
const maxFreeResidualKn = freeDisplacementDofs.length > 0
|
|
920
|
+
? Math.max(...freeDisplacementDofs.map((index) => Math.abs(mechanicalResidual[index])))
|
|
921
|
+
: 0;
|
|
922
|
+
const loadNorm = Math.max(Math.hypot(...loads), Math.hypot(...mechanicalInternal), Math.hypot(...couplingLoad), Math.hypot(...Array.from(prescribedDisplacements.keys()).map((index) => mechanicalResidual[index])), 1);
|
|
923
|
+
const residualNormRatio = maxFreeResidualKn / loadNorm;
|
|
924
|
+
const maxFreePorePressureResidualM3PerS = freePressureDofs.length > 0
|
|
925
|
+
? Math.max(...freePressureDofs.map((index) => Math.abs(pressureResidual[index])))
|
|
926
|
+
: 0;
|
|
927
|
+
const freePressureResidualSum = freePressureDofs
|
|
928
|
+
.reduce((sum, index) => sum + Math.abs(pressureResidual[index]), 0);
|
|
929
|
+
const pressureScale = Math.max(Math.hypot(...fluxes), Math.hypot(...pressureResidual), Math.hypot(...Array.from(prescribedPressures.keys()).map((index) => pressureResidual[index])), 1e-12);
|
|
930
|
+
const massBalanceErrorRatio = freePressureResidualSum / pressureScale;
|
|
931
|
+
const maxPorePressureKpa = Math.max(...porePressure);
|
|
932
|
+
const maxVerticalSettlementM = Math.max(0, -Math.min(...model.nodes.map((_, index) => displacement[dofIndex(index, 'uy')])));
|
|
933
|
+
const converged = residualNormRatio <= policy.forceBalanceTolerance &&
|
|
934
|
+
massBalanceErrorRatio <= policy.porePressureMassBalanceTolerance;
|
|
935
|
+
timeSteps.push({
|
|
936
|
+
step: stepIndex + 1,
|
|
937
|
+
timeSeconds: round(timeSeconds, 8),
|
|
938
|
+
deltaTimeSeconds: round(deltaTimeSeconds, 8),
|
|
939
|
+
maxFreeResidualKn: round(maxFreeResidualKn, 12),
|
|
940
|
+
residualNormRatio: round(residualNormRatio, 12),
|
|
941
|
+
maxFreePorePressureResidualM3PerS: Number(maxFreePorePressureResidualM3PerS.toExponential(12)),
|
|
942
|
+
massBalanceErrorRatio: round(massBalanceErrorRatio, 12),
|
|
943
|
+
maxPorePressureKpa: round(maxPorePressureKpa, 8),
|
|
944
|
+
maxVerticalSettlementM: round(maxVerticalSettlementM, 12),
|
|
945
|
+
converged,
|
|
946
|
+
});
|
|
947
|
+
previousDisplacement = [...displacement];
|
|
948
|
+
previousPorePressure = [...porePressure];
|
|
949
|
+
lastMechanicalResidual = mechanicalResidual;
|
|
950
|
+
lastPressureResidual = pressureResidual;
|
|
951
|
+
lastResidualNormRatio = residualNormRatio;
|
|
952
|
+
lastMassBalanceErrorRatio = massBalanceErrorRatio;
|
|
953
|
+
lastMaxFreeResidualKn = maxFreeResidualKn;
|
|
954
|
+
lastMaxFreePorePressureResidualM3PerS = maxFreePorePressureResidualM3PerS;
|
|
955
|
+
}
|
|
956
|
+
let maxBiotCouplingKpa = 0;
|
|
957
|
+
const elementOutputs = elementGaussCache.map((entry) => {
|
|
958
|
+
const material = materialById.get(entry.element.materialId);
|
|
959
|
+
const d = planeStrainD(material);
|
|
960
|
+
const elementDisplacement = entry.globalDofs.map((index) => displacement[index]);
|
|
961
|
+
const localPressures = entry.globalNodes.map((index) => porePressure[index]);
|
|
962
|
+
const gaussPoints = entry.mechanicalGauss.map((point, index) => {
|
|
963
|
+
const hydraulicPoint = entry.hydraulicGauss[index];
|
|
964
|
+
const strain = point.b.map((row) => row.reduce((sum, value, col) => sum + value * elementDisplacement[col], 0));
|
|
965
|
+
const effectiveStress = d.map((row) => row.reduce((sum, value, col) => sum + value * strain[col], 0));
|
|
966
|
+
const porePressureKpa = Math.max(0, hydraulicPoint.shape.reduce((sum, shape, node) => sum + shape * localPressures[node], 0));
|
|
967
|
+
const biotStressReductionKpa = hydraulicPoint.biotCoefficient * porePressureKpa;
|
|
968
|
+
const totalStress = [
|
|
969
|
+
effectiveStress[0] - biotStressReductionKpa,
|
|
970
|
+
effectiveStress[1] - biotStressReductionKpa,
|
|
971
|
+
effectiveStress[2],
|
|
972
|
+
];
|
|
973
|
+
const gradientX = hydraulicPoint.dNdx.reduce((sum, dNdx, node) => sum + dNdx * localPressures[node], 0);
|
|
974
|
+
const gradientY = hydraulicPoint.dNdy.reduce((sum, dNdy, node) => sum + dNdy * localPressures[node], 0);
|
|
975
|
+
maxBiotCouplingKpa = Math.max(maxBiotCouplingKpa, biotStressReductionKpa);
|
|
976
|
+
return {
|
|
977
|
+
elementId: entry.element.id,
|
|
978
|
+
gaussPoint: index + 1,
|
|
979
|
+
xi: round(point.xi, 10),
|
|
980
|
+
eta: round(point.eta, 10),
|
|
981
|
+
detJ: round(point.detJ, 10),
|
|
982
|
+
strain: [round(strain[0], 12), round(strain[1], 12), round(strain[2], 12)],
|
|
983
|
+
effectiveStressKpa: [
|
|
984
|
+
round(effectiveStress[0], 8),
|
|
985
|
+
round(effectiveStress[1], 8),
|
|
986
|
+
round(effectiveStress[2], 8),
|
|
987
|
+
],
|
|
988
|
+
totalStressKpa: [
|
|
989
|
+
round(totalStress[0], 8),
|
|
990
|
+
round(totalStress[1], 8),
|
|
991
|
+
round(totalStress[2], 8),
|
|
992
|
+
],
|
|
993
|
+
porePressureKpa: round(porePressureKpa, 8),
|
|
994
|
+
biotCoefficient: round(hydraulicPoint.biotCoefficient, 8),
|
|
995
|
+
biotStressReductionKpa: round(biotStressReductionKpa, 8),
|
|
996
|
+
hydraulicGradientKpaPerM: [round(gradientX, 8), round(gradientY, 8)],
|
|
997
|
+
darcyFluxMPerS: [
|
|
998
|
+
Number((-(hydraulicPoint.kxMPerS / gammaWaterKpaPerM) * gradientX).toExponential(12)),
|
|
999
|
+
Number((-(hydraulicPoint.kyMPerS / gammaWaterKpaPerM) * gradientY).toExponential(12)),
|
|
1000
|
+
],
|
|
1001
|
+
};
|
|
1002
|
+
});
|
|
1003
|
+
return {
|
|
1004
|
+
id: entry.element.id,
|
|
1005
|
+
areaM2: round(entry.areaM2, 10),
|
|
1006
|
+
thicknessM: round(entry.thicknessM, 10),
|
|
1007
|
+
gaussPoints,
|
|
1008
|
+
};
|
|
1009
|
+
});
|
|
1010
|
+
return {
|
|
1011
|
+
schemaVersion: 'fem-plane-strain-biot-consolidation-result.v1',
|
|
1012
|
+
method: 'quad4-plane-strain-biot-u-p-backward-euler-evidence',
|
|
1013
|
+
nodes: model.nodes.map((node, index) => ({
|
|
1014
|
+
...node,
|
|
1015
|
+
uxM: round(displacement[dofIndex(index, 'ux')], 12),
|
|
1016
|
+
uyM: round(displacement[dofIndex(index, 'uy')], 12),
|
|
1017
|
+
porePressureKpa: round(porePressure[index], 8),
|
|
1018
|
+
rxnXKn: round(lastMechanicalResidual[dofIndex(index, 'ux')], 8),
|
|
1019
|
+
rxnYKn: round(lastMechanicalResidual[dofIndex(index, 'uy')], 8),
|
|
1020
|
+
porePressureResidualM3PerS: Number(lastPressureResidual[index].toExponential(12)),
|
|
1021
|
+
})),
|
|
1022
|
+
elements: elementOutputs,
|
|
1023
|
+
timeSteps,
|
|
1024
|
+
displacementDofCount,
|
|
1025
|
+
freeDisplacementDofCount: freeDisplacementDofs.length,
|
|
1026
|
+
constrainedDisplacementDofCount: prescribedDisplacements.size,
|
|
1027
|
+
porePressureDofCount,
|
|
1028
|
+
freePorePressureDofCount: freePressureDofs.length,
|
|
1029
|
+
constrainedPorePressureDofCount: prescribedPressures.size,
|
|
1030
|
+
coupledUnknownCount,
|
|
1031
|
+
maxBiotCouplingKpa: round(maxBiotCouplingKpa, 8),
|
|
1032
|
+
maxFreeResidualKn: round(lastMaxFreeResidualKn, 12),
|
|
1033
|
+
residualNormRatio: round(lastResidualNormRatio, 12),
|
|
1034
|
+
maxFreePorePressureResidualM3PerS: Number(lastMaxFreePorePressureResidualM3PerS.toExponential(12)),
|
|
1035
|
+
massBalanceErrorRatio: round(lastMassBalanceErrorRatio, 12),
|
|
1036
|
+
converged: timeSteps.every((step) => step.converged),
|
|
1037
|
+
productionReady: false,
|
|
1038
|
+
policy,
|
|
1039
|
+
limitations: [
|
|
1040
|
+
'Benchmark-scale saturated linear-elastic Quad4 Biot u-p evidence kernel only.',
|
|
1041
|
+
'Uses dense backward-Euler displacement/pore-pressure coupling with a coupled DOF cap; it is not a production sparse solver or route-backed result manifest.',
|
|
1042
|
+
'Pore pressure is treated as excess pressure in kPa for deterministic evidence; groundwater elevation routing, unsaturated flow, uplift/piping design, nonlinear plasticity coupling, staged activation, and cross-solver validation are not provided.',
|
|
1043
|
+
],
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
607
1046
|
export function runPlaneStrainQuad4Assembly(model) {
|
|
608
1047
|
if (model.schemaVersion !== 'fem-plane-strain-model.v1') {
|
|
609
1048
|
throw new Error('Only fem-plane-strain-model.v1 is supported.');
|