@geotechcli/core 0.4.104 → 0.4.106
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/fem/demo.d.ts +2 -0
- package/dist/fem/demo.d.ts.map +1 -1
- package/dist/fem/demo.js +410 -0
- package/dist/fem/demo.js.map +1 -1
- package/dist/fem/engineering-evidence.d.ts.map +1 -1
- package/dist/fem/engineering-evidence.js +48 -1
- 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.js +1 -1
- package/dist/fem/plane-strain-assembly.js.map +1 -1
- package/dist/fem/production-readiness.js +6 -6
- package/dist/fem/production-readiness.js.map +1 -1
- package/dist/fem/routing.d.ts +15 -0
- package/dist/fem/routing.d.ts.map +1 -1
- package/dist/fem/routing.js +147 -14
- package/dist/fem/routing.js.map +1 -1
- package/dist/fem/types.d.ts +49 -6
- package/dist/fem/types.d.ts.map +1 -1
- package/dist/fem/validation.d.ts.map +1 -1
- package/dist/fem/validation.js +272 -20
- package/dist/fem/validation.js.map +1 -1
- package/dist/ingest/job-store.js +1 -1
- package/dist/ingest/job-store.js.map +1 -1
- package/dist/meta/metadata.json +1 -1
- package/dist/verifier/findings.js +6 -7
- package/dist/verifier/findings.js.map +1 -1
- package/package.json +1 -1
package/dist/fem/validation.js
CHANGED
|
@@ -19,6 +19,18 @@ function isNonEmptyString(value) {
|
|
|
19
19
|
}
|
|
20
20
|
const FEM_WEBGL_UINT16_INDEX_LIMIT = 65_535;
|
|
21
21
|
const FEM_MAX_PREVIEW_MESH_NODES = FEM_WEBGL_UINT16_INDEX_LIMIT + 1;
|
|
22
|
+
function expectedMeshCounts(mesh) {
|
|
23
|
+
if (mesh.elementType === 'quad4_plane_strain') {
|
|
24
|
+
return {
|
|
25
|
+
nodes: (mesh.divisionsX + 1) * (mesh.divisionsY + 1),
|
|
26
|
+
elements: mesh.divisionsX * mesh.divisionsY,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
nodes: (mesh.divisionsX + 1) * (mesh.divisionsY + 1) * (mesh.divisionsZ + 1),
|
|
31
|
+
elements: mesh.divisionsX * mesh.divisionsY * mesh.divisionsZ,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
22
34
|
function isFiniteNumber(value) {
|
|
23
35
|
return typeof value === 'number' && Number.isFinite(value);
|
|
24
36
|
}
|
|
@@ -53,7 +65,7 @@ function isFemAnalysisCaseShape(value) {
|
|
|
53
65
|
const groundwater = value.groundwater;
|
|
54
66
|
return (isRecord(geometry) &&
|
|
55
67
|
isRecord(geometry.domain) &&
|
|
56
|
-
(isRecord(geometry.raft) || isRecord(geometry.excavation) || isRecord(geometry.tunnel) || isRecord(geometry.consolidation)) &&
|
|
68
|
+
(isRecord(geometry.raft) || isRecord(geometry.excavation) || isRecord(geometry.tunnel) || isRecord(geometry.consolidation) || isRecord(geometry.biot)) &&
|
|
57
69
|
isRecord(mesh) &&
|
|
58
70
|
isRecord(groundwater) &&
|
|
59
71
|
isRecord(value.units) &&
|
|
@@ -173,7 +185,7 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
173
185
|
const validFieldLocations = new Set(['surface_nodes', 'outline_nodes', 'envelope']);
|
|
174
186
|
const validFieldQuantities = new Set(['displacement', 'reaction', 'load', 'stage_count', 'pore_pressure', 'degree_of_consolidation', 'strength_ratio']);
|
|
175
187
|
const validFieldComponents = new Set(['x', 'y', 'z', 'magnitude']);
|
|
176
|
-
const validDatasetSources = new Set(['visualization.disp', 'visualization.frame', 'envelope']);
|
|
188
|
+
const validDatasetSources = new Set(['visualization.disp', 'visualization.frame', 'visualization.scalar-frame', 'envelope']);
|
|
177
189
|
const fieldIds = new Set();
|
|
178
190
|
const stepIds = new Set();
|
|
179
191
|
const stepIndexes = new Set();
|
|
@@ -209,6 +221,17 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
209
221
|
['max_mobilized_strength_ratio', manifest.envelope.maxMobilizedStrengthRatio],
|
|
210
222
|
['drainage_path', manifest.envelope.drainagePathM],
|
|
211
223
|
['consolidation_duration', manifest.envelope.consolidationDurationYears],
|
|
224
|
+
['time_step_count', manifest.envelope.timeStepCount],
|
|
225
|
+
['min_pore_pressure', manifest.envelope.minPorePressureKpa],
|
|
226
|
+
['max_pore_pressure', manifest.envelope.maxPorePressureKpa],
|
|
227
|
+
['max_biot_coupling', manifest.envelope.maxBiotCouplingKpa],
|
|
228
|
+
['pore_pressure_mass_balance_error_ratio', manifest.envelope.porePressureMassBalanceErrorRatio],
|
|
229
|
+
['max_free_pore_pressure_residual', manifest.envelope.maxFreePorePressureResidualM3PerS],
|
|
230
|
+
['free_pore_pressure_residual_l1', manifest.envelope.freePorePressureResidualL1M3PerS],
|
|
231
|
+
['prescribed_pore_pressure_residual_l1', manifest.envelope.prescribedPorePressureResidualL1M3PerS],
|
|
232
|
+
['coupled_unknown_count', manifest.envelope.coupledUnknownCount],
|
|
233
|
+
['displacement_dof_count', manifest.envelope.displacementDofCount],
|
|
234
|
+
['pore_pressure_dof_count', manifest.envelope.porePressureDofCount],
|
|
212
235
|
['tunnel_diameter', manifest.envelope.tunnelDiameterM],
|
|
213
236
|
['tunnel_axis_depth', manifest.envelope.tunnelAxisDepthM],
|
|
214
237
|
['volume_loss', manifest.envelope.volumeLossPercent],
|
|
@@ -235,8 +258,11 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
235
258
|
if (fieldInfo.component != null && !validFieldComponents.has(String(fieldInfo.component))) {
|
|
236
259
|
findings.push(finding('blocker', `result.fields.${index}.component.invalid`, `Unsupported result field component: ${String(fieldInfo.component)}.`));
|
|
237
260
|
}
|
|
238
|
-
if (fieldInfo.location !== 'envelope' && fieldInfo.quantity !== 'displacement') {
|
|
239
|
-
findings.push(finding('blocker', `result.fields.${index}.node-quantity-invalid`, 'Surface and outline node result fields must describe displacement quantities.'));
|
|
261
|
+
if (fieldInfo.location !== 'envelope' && fieldInfo.quantity !== 'displacement' && fieldInfo.quantity !== 'pore_pressure') {
|
|
262
|
+
findings.push(finding('blocker', `result.fields.${index}.node-quantity-invalid`, 'Surface and outline node result fields must describe displacement or pore-pressure quantities.'));
|
|
263
|
+
}
|
|
264
|
+
if (fieldInfo.location === 'outline_nodes' && fieldInfo.quantity === 'pore_pressure') {
|
|
265
|
+
findings.push(finding('blocker', `result.fields.${index}.outline-pore-pressure-invalid`, 'Pore-pressure result fields must be surface-node scalar fields.'));
|
|
240
266
|
}
|
|
241
267
|
if (fieldInfo.location !== 'envelope' && fieldInfo.quantity === 'displacement' && fieldInfo.component == null) {
|
|
242
268
|
findings.push(finding('blocker', `result.fields.${index}.component.missing`, 'Node displacement result fields must include a displacement component.'));
|
|
@@ -271,6 +297,7 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
271
297
|
}
|
|
272
298
|
}
|
|
273
299
|
}
|
|
300
|
+
let previousTimeSeconds;
|
|
274
301
|
if (Array.isArray(steps)) {
|
|
275
302
|
for (const [index, step] of steps.entries()) {
|
|
276
303
|
const id = pushUniqueStringFinding(findings, stepIds, step.id, `result.steps.${index}.id`, 'Result step id');
|
|
@@ -294,6 +321,26 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
294
321
|
if (step.depthM != null && (!Number.isFinite(step.depthM) || step.depthM < 0)) {
|
|
295
322
|
findings.push(finding('blocker', `result.steps.${index}.depth-invalid`, 'Result step depth must be finite and non-negative when present.'));
|
|
296
323
|
}
|
|
324
|
+
if (step.timeSeconds != null && (!Number.isFinite(step.timeSeconds) || step.timeSeconds < 0)) {
|
|
325
|
+
findings.push(finding('blocker', `result.steps.${index}.time-invalid`, 'Result step timeSeconds must be finite and non-negative when present.'));
|
|
326
|
+
}
|
|
327
|
+
if (step.deltaTimeSeconds != null && (!Number.isFinite(step.deltaTimeSeconds) || step.deltaTimeSeconds <= 0)) {
|
|
328
|
+
findings.push(finding('blocker', `result.steps.${index}.delta-time-invalid`, 'Result step deltaTimeSeconds must be finite and positive when present.'));
|
|
329
|
+
}
|
|
330
|
+
if (manifest.analysisCase.objective === 'seepage_groundwater_coupling') {
|
|
331
|
+
if (!isFiniteNumber(step.timeSeconds) || step.timeSeconds <= 0) {
|
|
332
|
+
findings.push(finding('blocker', `result.steps.${index}.time-required`, 'Biot consolidation result steps must carry a positive timeSeconds value.'));
|
|
333
|
+
}
|
|
334
|
+
if (!isFiniteNumber(step.deltaTimeSeconds) || step.deltaTimeSeconds <= 0) {
|
|
335
|
+
findings.push(finding('blocker', `result.steps.${index}.delta-time-required`, 'Biot consolidation result steps must carry a positive deltaTimeSeconds value.'));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (isFiniteNumber(step.timeSeconds)) {
|
|
339
|
+
if (previousTimeSeconds != null && step.timeSeconds <= previousTimeSeconds) {
|
|
340
|
+
findings.push(finding('blocker', `result.steps.${index}.time-order-invalid`, 'Result step timeSeconds values must increase monotonically.'));
|
|
341
|
+
}
|
|
342
|
+
previousTimeSeconds = step.timeSeconds;
|
|
343
|
+
}
|
|
297
344
|
if (manifest.analysisCase.objective === 'excavation_deformation') {
|
|
298
345
|
if (!step.analysisStageId) {
|
|
299
346
|
findings.push(finding('blocker', `result.steps.${index}.stage-required`, 'Excavation result steps must reference an analysis stage.'));
|
|
@@ -367,15 +414,31 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
367
414
|
findings.push(finding('blocker', `result.datasets.${index}.source.invalid`, `Unsupported result dataset source: ${String(dataset.source)}.`));
|
|
368
415
|
}
|
|
369
416
|
const location = fieldLocations.get(dataset.fieldId);
|
|
370
|
-
|
|
417
|
+
const fieldInfo = fieldInfoById.get(dataset.fieldId);
|
|
418
|
+
const isVisualizationDataset = dataset.source === 'visualization.disp' ||
|
|
419
|
+
dataset.source === 'visualization.frame' ||
|
|
420
|
+
dataset.source === 'visualization.scalar-frame';
|
|
421
|
+
if (isVisualizationDataset && dataset.stepId == null) {
|
|
371
422
|
findings.push(finding('blocker', `result.datasets.${index}.step-required`, 'Visualization datasets must reference a result step.'));
|
|
372
423
|
}
|
|
373
|
-
if (
|
|
424
|
+
if (isVisualizationDataset && location === 'envelope') {
|
|
374
425
|
findings.push(finding('blocker', `result.datasets.${index}.visualization-field-invalid`, 'Visualization datasets must reference surface or outline node result fields, not envelope fields.'));
|
|
375
426
|
}
|
|
376
427
|
if (dataset.source === 'envelope' && location !== 'envelope') {
|
|
377
428
|
findings.push(finding('blocker', `result.datasets.${index}.envelope-field-invalid`, 'Envelope datasets must reference an envelope result field.'));
|
|
378
429
|
}
|
|
430
|
+
if ((dataset.source === 'visualization.disp' || dataset.source === 'visualization.frame') && dataset.stride !== 3) {
|
|
431
|
+
findings.push(finding('blocker', `result.datasets.${index}.visualization-stride-invalid`, 'Displacement visualization datasets must use stride 3.'));
|
|
432
|
+
}
|
|
433
|
+
if ((dataset.source === 'visualization.disp' || dataset.source === 'visualization.frame') && fieldInfo?.quantity !== 'displacement') {
|
|
434
|
+
findings.push(finding('blocker', `result.datasets.${index}.visualization-quantity-invalid`, 'Displacement visualization datasets must reference displacement result fields.'));
|
|
435
|
+
}
|
|
436
|
+
if (dataset.source === 'visualization.scalar-frame' && dataset.stride !== 1) {
|
|
437
|
+
findings.push(finding('blocker', `result.datasets.${index}.scalar-stride-invalid`, 'Scalar-frame visualization datasets must use stride 1.'));
|
|
438
|
+
}
|
|
439
|
+
if (dataset.source === 'visualization.scalar-frame' && fieldInfo?.quantity !== 'pore_pressure') {
|
|
440
|
+
findings.push(finding('blocker', `result.datasets.${index}.scalar-quantity-invalid`, 'Scalar-frame visualization datasets must reference pore-pressure result fields.'));
|
|
441
|
+
}
|
|
379
442
|
if (dataset.source === 'visualization.disp' && !arraysApproximatelyEqual(dataset.values, manifest.visualization.disp, 1e-12)) {
|
|
380
443
|
findings.push(finding('blocker', `result.datasets.${index}.disp-values-mismatch`, 'Visualization displacement dataset values must match the manifest visualization displacement array.'));
|
|
381
444
|
}
|
|
@@ -388,6 +451,18 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
388
451
|
findings.push(finding('blocker', `result.datasets.${index}.frame-values-mismatch`, 'Visualization frame dataset values must match the referenced frame displacement array.'));
|
|
389
452
|
}
|
|
390
453
|
}
|
|
454
|
+
if (dataset.source === 'visualization.scalar-frame' && dataset.stepId != null) {
|
|
455
|
+
const frame = frameByKey.get(`${dataset.fieldId}:${dataset.stepId}`);
|
|
456
|
+
if (!frame) {
|
|
457
|
+
findings.push(finding('blocker', `result.datasets.${index}.scalar-frame-missing`, 'Scalar-frame dataset has no matching frame payload.'));
|
|
458
|
+
}
|
|
459
|
+
else if (!Array.isArray(frame.scalarValues)) {
|
|
460
|
+
findings.push(finding('blocker', `result.datasets.${index}.scalar-frame-values-missing`, 'Scalar-frame dataset must reference a frame with scalarValues.'));
|
|
461
|
+
}
|
|
462
|
+
else if (!arraysApproximatelyEqual(dataset.values, frame.scalarValues, 1e-12)) {
|
|
463
|
+
findings.push(finding('blocker', `result.datasets.${index}.scalar-frame-values-mismatch`, 'Scalar-frame dataset values must match the referenced frame scalarValues array.'));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
391
466
|
const valueCount = dataset.values.length / dataset.stride;
|
|
392
467
|
if (location === 'surface_nodes' && valueCount !== nodeCount) {
|
|
393
468
|
findings.push(finding('blocker', `result.datasets.${index}.surface-count-mismatch`, 'Surface-node dataset values must match visualization base node count.'));
|
|
@@ -480,7 +555,7 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
480
555
|
finding('blocker', 'schema.shape-invalid', 'FEM analysis case is missing required geometry, mesh, groundwater, or array fields.'),
|
|
481
556
|
]);
|
|
482
557
|
}
|
|
483
|
-
const { domain, raft, excavation, tunnel, consolidation } = caseFile.geometry;
|
|
558
|
+
const { domain, raft, excavation, tunnel, consolidation, biot } = caseFile.geometry;
|
|
484
559
|
if (caseFile.schemaVersion !== 'fem-analysis-case.v0') {
|
|
485
560
|
findings.push(finding('blocker', 'schema.unsupported', 'Only fem-analysis-case.v0 is supported.'));
|
|
486
561
|
}
|
|
@@ -499,7 +574,7 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
499
574
|
if (!caseFile.experimental) {
|
|
500
575
|
findings.push(finding('blocker', 'mode.experimental-required', 'FEM cases must be explicitly marked experimental.'));
|
|
501
576
|
}
|
|
502
|
-
if (!['foundation_settlement', 'excavation_deformation', 'tunnel_volume_loss_settlement', 'staged_settlement_consolidation'].includes(caseFile.objective)) {
|
|
577
|
+
if (!['foundation_settlement', 'excavation_deformation', 'tunnel_volume_loss_settlement', 'staged_settlement_consolidation', 'seepage_groundwater_coupling'].includes(caseFile.objective)) {
|
|
503
578
|
findings.push(finding('blocker', 'objective.unsupported', `Unsupported FEM objective: ${caseFile.objective}.`));
|
|
504
579
|
}
|
|
505
580
|
if (caseFile.objective === 'foundation_settlement' && caseFile.analysisType !== 'static_3d_small_strain') {
|
|
@@ -514,6 +589,9 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
514
589
|
if (caseFile.objective === 'staged_settlement_consolidation' && caseFile.analysisType !== 'time_dependent_1d_consolidation') {
|
|
515
590
|
findings.push(finding('blocker', 'analysis.unsupported', `Unsupported analysis type: ${caseFile.analysisType}.`));
|
|
516
591
|
}
|
|
592
|
+
if (caseFile.objective === 'seepage_groundwater_coupling' && caseFile.analysisType !== 'time_dependent_2d_biot_consolidation') {
|
|
593
|
+
findings.push(finding('blocker', 'analysis.unsupported', `Unsupported analysis type: ${caseFile.analysisType}.`));
|
|
594
|
+
}
|
|
517
595
|
if (caseFile.geometry.domain.type !== 'box') {
|
|
518
596
|
findings.push(finding('blocker', 'geometry.domain.type-invalid', `Unsupported domain type: ${String(caseFile.geometry.domain.type)}.`));
|
|
519
597
|
}
|
|
@@ -691,6 +769,64 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
691
769
|
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.'));
|
|
692
770
|
}
|
|
693
771
|
}
|
|
772
|
+
if (caseFile.objective === 'seepage_groundwater_coupling') {
|
|
773
|
+
if (!biot) {
|
|
774
|
+
findings.push(finding('blocker', 'geometry.biot-missing', 'Seepage/groundwater coupling cases require plane-strain Biot geometry.'));
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
if (biot.type !== 'plane_strain_biot_column') {
|
|
778
|
+
findings.push(finding('blocker', 'geometry.biot.type-invalid', `Unsupported Biot geometry type: ${String(biot.type)}.`));
|
|
779
|
+
}
|
|
780
|
+
pushPositiveNumberFindings(findings, [
|
|
781
|
+
[biot.widthM, 'geometry.biot.width', 'Biot column width'],
|
|
782
|
+
[biot.heightM, 'geometry.biot.height', 'Biot column height'],
|
|
783
|
+
[biot.thicknessM, 'geometry.biot.thickness', 'Biot column thickness'],
|
|
784
|
+
]);
|
|
785
|
+
pushFiniteNumberFinding(findings, biot.initialPorePressureKpa, 'geometry.biot.initial-pore-pressure', 'Initial pore pressure', { nonNegative: true });
|
|
786
|
+
if (!isApproxEqual(domain.lengthM, biot.widthM, 1e-9)) {
|
|
787
|
+
findings.push(finding('blocker', 'geometry.biot.domain-width-mismatch', 'Biot column width must match the box-domain length.'));
|
|
788
|
+
}
|
|
789
|
+
if (!isApproxEqual(domain.depthM, biot.heightM, 1e-9)) {
|
|
790
|
+
findings.push(finding('blocker', 'geometry.biot.domain-height-mismatch', 'Biot column height must match the box-domain depth.'));
|
|
791
|
+
}
|
|
792
|
+
if (!isApproxEqual(domain.widthM, biot.thicknessM, 1e-9)) {
|
|
793
|
+
findings.push(finding('blocker', 'geometry.biot.domain-thickness-mismatch', 'Biot column thickness must match the box-domain width.'));
|
|
794
|
+
}
|
|
795
|
+
if (!Array.isArray(biot.timeStepsSeconds) || biot.timeStepsSeconds.length === 0) {
|
|
796
|
+
findings.push(finding('blocker', 'geometry.biot.time-steps-missing', 'Biot consolidation cases require at least one positive time step.'));
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
let previousStepSeconds = 0;
|
|
800
|
+
for (const [index, timeSeconds] of biot.timeStepsSeconds.entries()) {
|
|
801
|
+
if (!Number.isFinite(timeSeconds) || timeSeconds <= previousStepSeconds) {
|
|
802
|
+
findings.push(finding('blocker', `geometry.biot.time-steps.${index}.invalid`, 'Biot time steps must be finite, positive, and strictly increasing.'));
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
previousStepSeconds = timeSeconds;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (!Array.isArray(biot.porePressureBoundaries) || biot.porePressureBoundaries.length === 0) {
|
|
809
|
+
findings.push(finding('blocker', 'geometry.biot.pressure-boundary-missing', 'Biot consolidation cases require at least one prescribed pore-pressure boundary.'));
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
const boundaryIds = new Set();
|
|
813
|
+
const validPressureBoundaries = new Set(['top', 'bottom', 'left', 'right']);
|
|
814
|
+
for (const [index, boundary] of biot.porePressureBoundaries.entries()) {
|
|
815
|
+
const prefix = `geometry.biot.pressure-boundaries.${index}`;
|
|
816
|
+
if (!isRecord(boundary)) {
|
|
817
|
+
findings.push(finding('blocker', `${prefix}.shape-invalid`, 'Pore-pressure boundary must be an object.'));
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
pushUniqueStringFinding(findings, boundaryIds, boundary.id, `${prefix}.id`, 'Pore-pressure boundary id');
|
|
821
|
+
if (!validPressureBoundaries.has(String(boundary.boundary))) {
|
|
822
|
+
findings.push(finding('blocker', `${prefix}.boundary-invalid`, `Unsupported pore-pressure boundary: ${String(boundary.boundary)}.`));
|
|
823
|
+
}
|
|
824
|
+
pushFiniteNumberFinding(findings, boundary.porePressureKpa, `${prefix}.pore-pressure`, 'Prescribed pore pressure', { nonNegative: true });
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
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.'));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
694
830
|
if (caseFile.materials.length === 0) {
|
|
695
831
|
findings.push(finding('blocker', 'material.missing', 'At least one material is required.'));
|
|
696
832
|
}
|
|
@@ -733,6 +869,28 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
733
869
|
pushFiniteNumberFinding(findings, material.hydraulicConductivityMPerS, `${prefix}.hydraulic-conductivity`, 'Hydraulic conductivity', { positive: true });
|
|
734
870
|
}
|
|
735
871
|
}
|
|
872
|
+
if (caseFile.objective === 'seepage_groundwater_coupling') {
|
|
873
|
+
if (material.model !== 'linear_elastic') {
|
|
874
|
+
findings.push(finding('blocker', `${prefix}.biot-model-required`, 'Biot u-p preview requires a linear_elastic material with hydraulic coupling parameters.'));
|
|
875
|
+
}
|
|
876
|
+
const hasHydraulicX = material.hydraulicConductivityXMPerS != null || material.hydraulicConductivityMPerS != null;
|
|
877
|
+
if (!hasHydraulicX) {
|
|
878
|
+
findings.push(finding('blocker', `${prefix}.hydraulic-conductivity-x-missing`, 'Biot u-p preview requires hydraulic conductivity in the x direction or an isotropic hydraulic conductivity.'));
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
pushFiniteNumberFinding(findings, material.hydraulicConductivityXMPerS ?? material.hydraulicConductivityMPerS, `${prefix}.hydraulic-conductivity-x`, 'Hydraulic conductivity X', { positive: true });
|
|
882
|
+
}
|
|
883
|
+
if (material.hydraulicConductivityYMPerS != null) {
|
|
884
|
+
pushFiniteNumberFinding(findings, material.hydraulicConductivityYMPerS, `${prefix}.hydraulic-conductivity-y`, 'Hydraulic conductivity Y', { positive: true });
|
|
885
|
+
}
|
|
886
|
+
if (material.hydraulicConductivityMPerS != null) {
|
|
887
|
+
pushFiniteNumberFinding(findings, material.hydraulicConductivityMPerS, `${prefix}.hydraulic-conductivity`, 'Isotropic hydraulic conductivity', { positive: true });
|
|
888
|
+
}
|
|
889
|
+
if (!isFiniteNumber(material.biotCoefficient) || material.biotCoefficient < 0 || material.biotCoefficient > 1) {
|
|
890
|
+
findings.push(finding('blocker', `${prefix}.biot-coefficient-invalid`, 'Biot coefficient must be finite and between 0 and 1.'));
|
|
891
|
+
}
|
|
892
|
+
pushFiniteNumberFinding(findings, material.specificStorage1PerM, `${prefix}.specific-storage`, 'Specific storage', { positive: true });
|
|
893
|
+
}
|
|
736
894
|
validateEvidenceRefs(findings, material.evidenceRefs, prefix);
|
|
737
895
|
validateAssumptions(findings, material.assumptions, prefix);
|
|
738
896
|
}
|
|
@@ -740,6 +898,9 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
740
898
|
if (caseFile.objective === 'tunnel_volume_loss_settlement' && caseFile.loads.length > 0) {
|
|
741
899
|
findings.push(finding('blocker', 'load.unsupported-for-objective', 'Tunnel volume-loss settlement previews use explicit volume loss and must not include pressure loads.'));
|
|
742
900
|
}
|
|
901
|
+
if (caseFile.objective === 'seepage_groundwater_coupling' && caseFile.loads.length > 0) {
|
|
902
|
+
findings.push(finding('blocker', 'load.unsupported-for-objective', 'Biot u-p seepage previews currently use prescribed pore-pressure boundaries and must not include pressure loads.'));
|
|
903
|
+
}
|
|
743
904
|
if (caseFile.loads.length === 0) {
|
|
744
905
|
if (caseFile.objective === 'foundation_settlement') {
|
|
745
906
|
findings.push(finding('blocker', 'load.missing', 'A raft pressure load is required.'));
|
|
@@ -775,6 +936,9 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
775
936
|
if (caseFile.objective === 'staged_settlement_consolidation' && load.target !== 'ground_surface') {
|
|
776
937
|
findings.push(finding('blocker', `${prefix}.target-invalid`, 'Staged consolidation loads must target ground_surface.'));
|
|
777
938
|
}
|
|
939
|
+
if (caseFile.objective === 'seepage_groundwater_coupling') {
|
|
940
|
+
findings.push(finding('blocker', `${prefix}.target-invalid`, 'Biot u-p seepage previews do not accept pressure loads.'));
|
|
941
|
+
}
|
|
778
942
|
if (caseFile.objective === 'staged_settlement_consolidation' && consolidation) {
|
|
779
943
|
if (caseFile.loads.length !== consolidation.stages.length) {
|
|
780
944
|
findings.push(finding('blocker', 'load.stage-count-mismatch', 'Staged consolidation load count must match the consolidation stage count.'));
|
|
@@ -789,19 +953,30 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
789
953
|
}
|
|
790
954
|
}
|
|
791
955
|
const { divisionsX, divisionsY, divisionsZ } = caseFile.mesh;
|
|
792
|
-
if (caseFile.mesh.elementType !== 'hex8') {
|
|
956
|
+
if (caseFile.mesh.elementType !== 'hex8' && caseFile.mesh.elementType !== 'quad4_plane_strain') {
|
|
793
957
|
findings.push(finding('blocker', 'mesh.element-type-invalid', `Unsupported mesh element type: ${String(caseFile.mesh.elementType)}.`));
|
|
794
958
|
}
|
|
959
|
+
if (caseFile.objective === 'seepage_groundwater_coupling' && caseFile.mesh.elementType !== 'quad4_plane_strain') {
|
|
960
|
+
findings.push(finding('blocker', 'mesh.element-type-biot-required', 'Biot u-p seepage previews require quad4_plane_strain mesh elements.'));
|
|
961
|
+
}
|
|
962
|
+
if (caseFile.objective !== 'seepage_groundwater_coupling' && caseFile.mesh.elementType !== 'hex8') {
|
|
963
|
+
findings.push(finding('blocker', 'mesh.element-type-hex8-required', 'Non-Biot FEM preview cases require hex8 mesh elements.'));
|
|
964
|
+
}
|
|
795
965
|
if (!Number.isInteger(divisionsX) ||
|
|
796
966
|
!Number.isInteger(divisionsY) ||
|
|
797
967
|
!Number.isInteger(divisionsZ)) {
|
|
798
968
|
findings.push(finding('blocker', 'mesh.divisions-integer', 'Mesh divisions must be finite integers.'));
|
|
799
969
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
970
|
+
else {
|
|
971
|
+
if (caseFile.mesh.elementType === 'quad4_plane_strain') {
|
|
972
|
+
if (divisionsX < 1 || divisionsY < 1 || divisionsZ !== 1) {
|
|
973
|
+
findings.push(finding('blocker', 'mesh.quad4-plane-strain-invalid', 'Quad4 plane-strain meshes require divisionsX and divisionsY of at least 1 and divisionsZ exactly 1.'));
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
else if (divisionsX < 2 || divisionsY < 2 || divisionsZ < 1) {
|
|
977
|
+
findings.push(finding('blocker', 'mesh.too-coarse', 'Mesh divisions must be at least 2 x 2 x 1.'));
|
|
978
|
+
}
|
|
979
|
+
const nodeCount = expectedMeshCounts(caseFile.mesh).nodes;
|
|
805
980
|
if (!Number.isFinite(nodeCount) || nodeCount > FEM_MAX_PREVIEW_MESH_NODES) {
|
|
806
981
|
findings.push(finding('blocker', 'mesh.preview-node-limit', `Experimental FEM/WebGL previews are capped at ${FEM_MAX_PREVIEW_MESH_NODES} nodes.`));
|
|
807
982
|
}
|
|
@@ -960,6 +1135,7 @@ function validateResultEnvelopeSemantics(findings, manifest) {
|
|
|
960
1135
|
['excavation_deformation', ['builtin-staged-excavation-demo']],
|
|
961
1136
|
['tunnel_volume_loss_settlement', ['builtin-tunnel-volume-loss-demo']],
|
|
962
1137
|
['staged_settlement_consolidation', ['builtin-staged-consolidation-1d', 'builtin-nonlinear-column-v0']],
|
|
1138
|
+
['seepage_groundwater_coupling', ['builtin-biot-up-plane-strain-v0']],
|
|
963
1139
|
]);
|
|
964
1140
|
const expectedBackends = expectedBackendByObjective.get(analysisCase.objective);
|
|
965
1141
|
if (expectedBackends && !expectedBackends.includes(manifest.backend.id)) {
|
|
@@ -1096,6 +1272,80 @@ function validateResultEnvelopeSemantics(findings, manifest) {
|
|
|
1096
1272
|
}
|
|
1097
1273
|
}
|
|
1098
1274
|
}
|
|
1275
|
+
if (analysisCase.objective === 'seepage_groundwater_coupling') {
|
|
1276
|
+
const biot = analysisCase.geometry.biot;
|
|
1277
|
+
if (!biot)
|
|
1278
|
+
return;
|
|
1279
|
+
const boundaryPressureValues = biot.porePressureBoundaries.map((boundary) => boundary.porePressureKpa);
|
|
1280
|
+
const expectedMaxPressureKpa = Math.max(biot.initialPorePressureKpa, ...boundaryPressureValues);
|
|
1281
|
+
const minPorePressureOk = pushFiniteNumberFinding(findings, envelope.minPorePressureKpa, 'result.envelope.min-pore-pressure', 'Envelope minimum pore pressure', { nonNegative: true });
|
|
1282
|
+
const maxPorePressureOk = pushFiniteNumberFinding(findings, envelope.maxPorePressureKpa, 'result.envelope.max-pore-pressure', 'Envelope maximum pore pressure', { nonNegative: true });
|
|
1283
|
+
const maxExcessOk = pushFiniteNumberFinding(findings, envelope.maxExcessPorePressureKpa, 'result.envelope.max-excess-pore-pressure', 'Envelope maximum excess pore pressure', { nonNegative: true });
|
|
1284
|
+
pushFiniteNumberFinding(findings, envelope.maxBiotCouplingKpa, 'result.envelope.max-biot-coupling', 'Envelope maximum Biot coupling', { nonNegative: true });
|
|
1285
|
+
const massBalanceOk = pushFiniteNumberFinding(findings, envelope.porePressureMassBalanceErrorRatio, 'result.envelope.pore-pressure-mass-balance-error-ratio', 'Envelope pore-pressure mass-balance error ratio', { nonNegative: true });
|
|
1286
|
+
pushFiniteNumberFinding(findings, envelope.maxFreePorePressureResidualM3PerS, 'result.envelope.max-free-pore-pressure-residual', 'Envelope maximum free pore-pressure residual', { nonNegative: true });
|
|
1287
|
+
pushFiniteNumberFinding(findings, envelope.freePorePressureResidualL1M3PerS, 'result.envelope.free-pore-pressure-residual-l1', 'Envelope free pore-pressure residual L1 norm', { nonNegative: true });
|
|
1288
|
+
pushFiniteNumberFinding(findings, envelope.prescribedPorePressureResidualL1M3PerS, 'result.envelope.prescribed-pore-pressure-residual-l1', 'Envelope prescribed pore-pressure residual L1 norm', { nonNegative: true });
|
|
1289
|
+
const timeStepCountOk = pushFiniteNumberFinding(findings, envelope.timeStepCount, 'result.envelope.time-step-count', 'Envelope time-step count', { positive: true });
|
|
1290
|
+
const coupledUnknownsOk = pushFiniteNumberFinding(findings, envelope.coupledUnknownCount, 'result.envelope.coupled-unknown-count', 'Envelope coupled unknown count', { positive: true });
|
|
1291
|
+
const displacementDofsOk = pushFiniteNumberFinding(findings, envelope.displacementDofCount, 'result.envelope.displacement-dof-count', 'Envelope displacement DOF count', { positive: true });
|
|
1292
|
+
const porePressureDofsOk = pushFiniteNumberFinding(findings, envelope.porePressureDofCount, 'result.envelope.pore-pressure-dof-count', 'Envelope pore-pressure DOF count', { positive: true });
|
|
1293
|
+
if (minPorePressureOk && maxPorePressureOk && envelope.minPorePressureKpa > envelope.maxPorePressureKpa) {
|
|
1294
|
+
findings.push(finding('blocker', 'result.envelope.pore-pressure-range-invalid', 'Envelope minimum pore pressure cannot exceed maximum pore pressure.'));
|
|
1295
|
+
}
|
|
1296
|
+
if (maxPorePressureOk && envelope.maxPorePressureKpa > expectedMaxPressureKpa + 1e-6) {
|
|
1297
|
+
findings.push(finding('blocker', 'result.envelope.pore-pressure-upper-bound-invalid', 'Envelope maximum pore pressure cannot exceed the initial or prescribed boundary pressure envelope.'));
|
|
1298
|
+
}
|
|
1299
|
+
if (maxExcessOk && maxPorePressureOk) {
|
|
1300
|
+
pushApproximateMatchFinding(findings, envelope.maxExcessPorePressureKpa, envelope.maxPorePressureKpa, 'result.envelope.max-excess-pore-pressure-mismatch', 'Maximum excess pore pressure', 1e-6);
|
|
1301
|
+
}
|
|
1302
|
+
if (timeStepCountOk && (!Number.isInteger(envelope.timeStepCount) || envelope.timeStepCount !== biot.timeStepsSeconds.length)) {
|
|
1303
|
+
findings.push(finding('blocker', 'result.envelope.time-step-count-mismatch', 'Biot envelope time-step count must match the embedded time-step schedule.'));
|
|
1304
|
+
}
|
|
1305
|
+
if (coupledUnknownsOk && !Number.isInteger(envelope.coupledUnknownCount)) {
|
|
1306
|
+
findings.push(finding('blocker', 'result.envelope.coupled-unknown-count-integer', 'Coupled unknown count must be an integer.'));
|
|
1307
|
+
}
|
|
1308
|
+
if (displacementDofsOk && !Number.isInteger(envelope.displacementDofCount)) {
|
|
1309
|
+
findings.push(finding('blocker', 'result.envelope.displacement-dof-count-integer', 'Displacement DOF count must be an integer.'));
|
|
1310
|
+
}
|
|
1311
|
+
if (porePressureDofsOk && !Number.isInteger(envelope.porePressureDofCount)) {
|
|
1312
|
+
findings.push(finding('blocker', 'result.envelope.pore-pressure-dof-count-integer', 'Pore-pressure DOF count must be an integer.'));
|
|
1313
|
+
}
|
|
1314
|
+
if (coupledUnknownsOk && displacementDofsOk && porePressureDofsOk && envelope.coupledUnknownCount > envelope.displacementDofCount + envelope.porePressureDofCount) {
|
|
1315
|
+
findings.push(finding('blocker', 'result.envelope.coupled-unknown-count-mismatch', 'Coupled unknown count cannot exceed displacement plus pore-pressure DOF counts.'));
|
|
1316
|
+
}
|
|
1317
|
+
if (massBalanceOk && envelope.porePressureMassBalanceErrorRatio > 1e-3) {
|
|
1318
|
+
findings.push(finding('blocker', 'result.envelope.pore-pressure-mass-balance-too-large', 'Biot pore-pressure mass-balance error ratio exceeds the preview tolerance.'));
|
|
1319
|
+
}
|
|
1320
|
+
pushApproximateMatchFinding(findings, envelope.totalLoadKn, 0, 'result.envelope.biot-total-load-invalid', 'Biot preview total load', 0.001);
|
|
1321
|
+
pushApproximateMatchFinding(findings, envelope.reactionKn, 0, 'result.envelope.biot-reaction-invalid', 'Biot preview reaction', 0.001);
|
|
1322
|
+
if (maxSettlementOk && isFiniteNumber(envelope.finalSettlementMm)) {
|
|
1323
|
+
pushApproximateMatchFinding(findings, envelope.maxSettlementMm, envelope.finalSettlementMm, 'result.envelope.biot-max-settlement-mismatch', 'Biot maximum settlement', 0.001);
|
|
1324
|
+
}
|
|
1325
|
+
const pressureAudit = manifest.pressureAudit;
|
|
1326
|
+
if (!pressureAudit) {
|
|
1327
|
+
findings.push(finding('blocker', 'result.pressure-audit.missing', 'Biot result manifests must include a pressure audit.'));
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
const pressureAuditEntries = [
|
|
1331
|
+
['freePorePressureResidualL1M3PerS', 'free-pore-pressure-residual-l1', 'Free pore-pressure residual L1 norm'],
|
|
1332
|
+
['prescribedPorePressureResidualL1M3PerS', 'prescribed-pore-pressure-residual-l1', 'Prescribed pore-pressure residual L1 norm'],
|
|
1333
|
+
['netPrescribedPressureBoundaryFlowM3PerS', 'net-prescribed-pressure-boundary-flow', 'Net prescribed pressure boundary flow'],
|
|
1334
|
+
['appliedNodalFluxSumM3PerS', 'applied-nodal-flux-sum', 'Applied nodal flux sum'],
|
|
1335
|
+
['storageRateSumM3PerS', 'storage-rate-sum', 'Storage-rate sum'],
|
|
1336
|
+
['couplingRateSumM3PerS', 'coupling-rate-sum', 'Coupling-rate sum'],
|
|
1337
|
+
['darcyFlowRateSumM3PerS', 'darcy-flow-rate-sum', 'Darcy flow-rate sum'],
|
|
1338
|
+
];
|
|
1339
|
+
for (const [key, code, label] of pressureAuditEntries) {
|
|
1340
|
+
pushFiniteNumberFinding(findings, pressureAudit[key], `result.pressure-audit.${code}`, label);
|
|
1341
|
+
}
|
|
1342
|
+
pushApproximateMatchFinding(findings, pressureAudit.freePorePressureResidualL1M3PerS, envelope.freePorePressureResidualL1M3PerS, 'result.pressure-audit.free-residual-envelope-mismatch', 'Pressure-audit free residual', 1e-12);
|
|
1343
|
+
pushApproximateMatchFinding(findings, pressureAudit.prescribedPorePressureResidualL1M3PerS, envelope.prescribedPorePressureResidualL1M3PerS, 'result.pressure-audit.prescribed-residual-envelope-mismatch', 'Pressure-audit prescribed residual', 1e-12);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
else if (manifest.pressureAudit != null) {
|
|
1347
|
+
findings.push(finding('blocker', 'result.pressure-audit.unexpected', 'Pressure audit is only valid for Biot u-p seepage result manifests.'));
|
|
1348
|
+
}
|
|
1099
1349
|
}
|
|
1100
1350
|
export function validateFemResultManifest(manifest) {
|
|
1101
1351
|
const findings = [];
|
|
@@ -1130,6 +1380,7 @@ export function validateFemResultManifest(manifest) {
|
|
|
1130
1380
|
'builtin-tunnel-volume-loss-demo',
|
|
1131
1381
|
'builtin-staged-consolidation-1d',
|
|
1132
1382
|
'builtin-nonlinear-column-v0',
|
|
1383
|
+
'builtin-biot-up-plane-strain-v0',
|
|
1133
1384
|
]);
|
|
1134
1385
|
if (!validBackendIds.has(manifest.backend.id)) {
|
|
1135
1386
|
findings.push(finding('blocker', 'result.backend.id-invalid', `Unsupported FEM result backend: ${String(manifest.backend.id)}.`));
|
|
@@ -1170,12 +1421,7 @@ export function validateFemResultManifest(manifest) {
|
|
|
1170
1421
|
}
|
|
1171
1422
|
pushIndexArrayFindings(findings, 'tri', visualization.tri, nodeCount, 3);
|
|
1172
1423
|
pushIndexArrayFindings(findings, 'edge', visualization.edge, nodeCount, 2);
|
|
1173
|
-
const expectedMeshNodes = (manifest.analysisCase.mesh
|
|
1174
|
-
(manifest.analysisCase.mesh.divisionsY + 1) *
|
|
1175
|
-
(manifest.analysisCase.mesh.divisionsZ + 1);
|
|
1176
|
-
const expectedMeshElements = manifest.analysisCase.mesh.divisionsX *
|
|
1177
|
-
manifest.analysisCase.mesh.divisionsY *
|
|
1178
|
-
manifest.analysisCase.mesh.divisionsZ;
|
|
1424
|
+
const { nodes: expectedMeshNodes, elements: expectedMeshElements } = expectedMeshCounts(manifest.analysisCase.mesh);
|
|
1179
1425
|
const meshDivisions = manifest.mesh.divisions;
|
|
1180
1426
|
if (!Number.isInteger(manifest.mesh.nodes) || manifest.mesh.nodes !== expectedMeshNodes) {
|
|
1181
1427
|
findings.push(finding('blocker', 'result.mesh.nodes-mismatch', 'Result mesh node count must match the embedded analysis case mesh divisions.'));
|
|
@@ -1232,6 +1478,12 @@ export function validateFemResultManifest(manifest) {
|
|
|
1232
1478
|
if (frame.color.length / 3 !== nodeCount) {
|
|
1233
1479
|
findings.push(finding('blocker', `result.frames.${index}.color.node-count-mismatch`, 'Frame color vectors must match base nodes.'));
|
|
1234
1480
|
}
|
|
1481
|
+
if (frame.scalarValues != null) {
|
|
1482
|
+
pushFiniteArrayFindings(findings, `frames.${index}.scalarValues`, frame.scalarValues, 1);
|
|
1483
|
+
if (frame.scalarValues.length !== nodeCount) {
|
|
1484
|
+
findings.push(finding('blocker', `result.frames.${index}.scalar-values.node-count-mismatch`, 'Frame scalarValues must match base nodes.'));
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1235
1487
|
}
|
|
1236
1488
|
}
|
|
1237
1489
|
validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNodeCount);
|