@geotechcli/core 0.4.105 → 0.4.107
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 +414 -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 +9 -7
- package/dist/fem/engineering-evidence.js.map +1 -1
- package/dist/fem/index.d.ts +2 -2
- 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/nonlinear-column-solver.d.ts.map +1 -1
- package/dist/fem/nonlinear-column-solver.js +137 -6
- package/dist/fem/nonlinear-column-solver.js.map +1 -1
- package/dist/fem/plane-strain-assembly.d.ts +35 -0
- package/dist/fem/plane-strain-assembly.d.ts.map +1 -1
- package/dist/fem/plane-strain-assembly.js +110 -14
- 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 +7 -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 +98 -6
- package/dist/fem/types.d.ts.map +1 -1
- package/dist/fem/validation.d.ts.map +1 -1
- package/dist/fem/validation.js +438 -22
- 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,20 @@ 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
|
+
const FEM_MIN_BIOT_TRANSIENT_STEPS = 3;
|
|
23
|
+
const FEM_MAX_BIOT_TIME_STEP_GROWTH_RATIO = 8;
|
|
24
|
+
function expectedMeshCounts(mesh) {
|
|
25
|
+
if (mesh.elementType === 'quad4_plane_strain') {
|
|
26
|
+
return {
|
|
27
|
+
nodes: (mesh.divisionsX + 1) * (mesh.divisionsY + 1),
|
|
28
|
+
elements: mesh.divisionsX * mesh.divisionsY,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
nodes: (mesh.divisionsX + 1) * (mesh.divisionsY + 1) * (mesh.divisionsZ + 1),
|
|
33
|
+
elements: mesh.divisionsX * mesh.divisionsY * mesh.divisionsZ,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
22
36
|
function isFiniteNumber(value) {
|
|
23
37
|
return typeof value === 'number' && Number.isFinite(value);
|
|
24
38
|
}
|
|
@@ -53,7 +67,7 @@ function isFemAnalysisCaseShape(value) {
|
|
|
53
67
|
const groundwater = value.groundwater;
|
|
54
68
|
return (isRecord(geometry) &&
|
|
55
69
|
isRecord(geometry.domain) &&
|
|
56
|
-
(isRecord(geometry.raft) || isRecord(geometry.excavation) || isRecord(geometry.tunnel) || isRecord(geometry.consolidation)) &&
|
|
70
|
+
(isRecord(geometry.raft) || isRecord(geometry.excavation) || isRecord(geometry.tunnel) || isRecord(geometry.consolidation) || isRecord(geometry.biot)) &&
|
|
57
71
|
isRecord(mesh) &&
|
|
58
72
|
isRecord(groundwater) &&
|
|
59
73
|
isRecord(value.units) &&
|
|
@@ -173,7 +187,7 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
173
187
|
const validFieldLocations = new Set(['surface_nodes', 'outline_nodes', 'envelope']);
|
|
174
188
|
const validFieldQuantities = new Set(['displacement', 'reaction', 'load', 'stage_count', 'pore_pressure', 'degree_of_consolidation', 'strength_ratio']);
|
|
175
189
|
const validFieldComponents = new Set(['x', 'y', 'z', 'magnitude']);
|
|
176
|
-
const validDatasetSources = new Set(['visualization.disp', 'visualization.frame', 'envelope']);
|
|
190
|
+
const validDatasetSources = new Set(['visualization.disp', 'visualization.frame', 'visualization.scalar-frame', 'envelope']);
|
|
177
191
|
const fieldIds = new Set();
|
|
178
192
|
const stepIds = new Set();
|
|
179
193
|
const stepIndexes = new Set();
|
|
@@ -209,6 +223,21 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
209
223
|
['max_mobilized_strength_ratio', manifest.envelope.maxMobilizedStrengthRatio],
|
|
210
224
|
['drainage_path', manifest.envelope.drainagePathM],
|
|
211
225
|
['consolidation_duration', manifest.envelope.consolidationDurationYears],
|
|
226
|
+
['time_step_count', manifest.envelope.timeStepCount],
|
|
227
|
+
['min_pore_pressure', manifest.envelope.minPorePressureKpa],
|
|
228
|
+
['max_pore_pressure', manifest.envelope.maxPorePressureKpa],
|
|
229
|
+
['max_biot_coupling', manifest.envelope.maxBiotCouplingKpa],
|
|
230
|
+
['pore_pressure_mass_balance_error_ratio', manifest.envelope.porePressureMassBalanceErrorRatio],
|
|
231
|
+
['max_free_pore_pressure_residual', manifest.envelope.maxFreePorePressureResidualM3PerS],
|
|
232
|
+
['free_pore_pressure_residual_l1', manifest.envelope.freePorePressureResidualL1M3PerS],
|
|
233
|
+
['prescribed_pore_pressure_residual_l1', manifest.envelope.prescribedPorePressureResidualL1M3PerS],
|
|
234
|
+
['average_pore_pressure', manifest.envelope.averagePorePressureKpa],
|
|
235
|
+
['average_free_pore_pressure', manifest.envelope.averageFreePorePressureKpa],
|
|
236
|
+
['pore_pressure_dissipation_ratio', manifest.envelope.porePressureDissipationRatio],
|
|
237
|
+
['max_pore_pressure_change_rate', manifest.envelope.maxPorePressureChangeRateKpaPerS],
|
|
238
|
+
['coupled_unknown_count', manifest.envelope.coupledUnknownCount],
|
|
239
|
+
['displacement_dof_count', manifest.envelope.displacementDofCount],
|
|
240
|
+
['pore_pressure_dof_count', manifest.envelope.porePressureDofCount],
|
|
212
241
|
['tunnel_diameter', manifest.envelope.tunnelDiameterM],
|
|
213
242
|
['tunnel_axis_depth', manifest.envelope.tunnelAxisDepthM],
|
|
214
243
|
['volume_loss', manifest.envelope.volumeLossPercent],
|
|
@@ -235,8 +264,11 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
235
264
|
if (fieldInfo.component != null && !validFieldComponents.has(String(fieldInfo.component))) {
|
|
236
265
|
findings.push(finding('blocker', `result.fields.${index}.component.invalid`, `Unsupported result field component: ${String(fieldInfo.component)}.`));
|
|
237
266
|
}
|
|
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.'));
|
|
267
|
+
if (fieldInfo.location !== 'envelope' && fieldInfo.quantity !== 'displacement' && fieldInfo.quantity !== 'pore_pressure') {
|
|
268
|
+
findings.push(finding('blocker', `result.fields.${index}.node-quantity-invalid`, 'Surface and outline node result fields must describe displacement or pore-pressure quantities.'));
|
|
269
|
+
}
|
|
270
|
+
if (fieldInfo.location === 'outline_nodes' && fieldInfo.quantity === 'pore_pressure') {
|
|
271
|
+
findings.push(finding('blocker', `result.fields.${index}.outline-pore-pressure-invalid`, 'Pore-pressure result fields must be surface-node scalar fields.'));
|
|
240
272
|
}
|
|
241
273
|
if (fieldInfo.location !== 'envelope' && fieldInfo.quantity === 'displacement' && fieldInfo.component == null) {
|
|
242
274
|
findings.push(finding('blocker', `result.fields.${index}.component.missing`, 'Node displacement result fields must include a displacement component.'));
|
|
@@ -271,6 +303,7 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
271
303
|
}
|
|
272
304
|
}
|
|
273
305
|
}
|
|
306
|
+
let previousTimeSeconds;
|
|
274
307
|
if (Array.isArray(steps)) {
|
|
275
308
|
for (const [index, step] of steps.entries()) {
|
|
276
309
|
const id = pushUniqueStringFinding(findings, stepIds, step.id, `result.steps.${index}.id`, 'Result step id');
|
|
@@ -294,6 +327,26 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
294
327
|
if (step.depthM != null && (!Number.isFinite(step.depthM) || step.depthM < 0)) {
|
|
295
328
|
findings.push(finding('blocker', `result.steps.${index}.depth-invalid`, 'Result step depth must be finite and non-negative when present.'));
|
|
296
329
|
}
|
|
330
|
+
if (step.timeSeconds != null && (!Number.isFinite(step.timeSeconds) || step.timeSeconds < 0)) {
|
|
331
|
+
findings.push(finding('blocker', `result.steps.${index}.time-invalid`, 'Result step timeSeconds must be finite and non-negative when present.'));
|
|
332
|
+
}
|
|
333
|
+
if (step.deltaTimeSeconds != null && (!Number.isFinite(step.deltaTimeSeconds) || step.deltaTimeSeconds <= 0)) {
|
|
334
|
+
findings.push(finding('blocker', `result.steps.${index}.delta-time-invalid`, 'Result step deltaTimeSeconds must be finite and positive when present.'));
|
|
335
|
+
}
|
|
336
|
+
if (manifest.analysisCase.objective === 'seepage_groundwater_coupling') {
|
|
337
|
+
if (!isFiniteNumber(step.timeSeconds) || step.timeSeconds <= 0) {
|
|
338
|
+
findings.push(finding('blocker', `result.steps.${index}.time-required`, 'Biot consolidation result steps must carry a positive timeSeconds value.'));
|
|
339
|
+
}
|
|
340
|
+
if (!isFiniteNumber(step.deltaTimeSeconds) || step.deltaTimeSeconds <= 0) {
|
|
341
|
+
findings.push(finding('blocker', `result.steps.${index}.delta-time-required`, 'Biot consolidation result steps must carry a positive deltaTimeSeconds value.'));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (isFiniteNumber(step.timeSeconds)) {
|
|
345
|
+
if (previousTimeSeconds != null && step.timeSeconds <= previousTimeSeconds) {
|
|
346
|
+
findings.push(finding('blocker', `result.steps.${index}.time-order-invalid`, 'Result step timeSeconds values must increase monotonically.'));
|
|
347
|
+
}
|
|
348
|
+
previousTimeSeconds = step.timeSeconds;
|
|
349
|
+
}
|
|
297
350
|
if (manifest.analysisCase.objective === 'excavation_deformation') {
|
|
298
351
|
if (!step.analysisStageId) {
|
|
299
352
|
findings.push(finding('blocker', `result.steps.${index}.stage-required`, 'Excavation result steps must reference an analysis stage.'));
|
|
@@ -367,15 +420,31 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
367
420
|
findings.push(finding('blocker', `result.datasets.${index}.source.invalid`, `Unsupported result dataset source: ${String(dataset.source)}.`));
|
|
368
421
|
}
|
|
369
422
|
const location = fieldLocations.get(dataset.fieldId);
|
|
370
|
-
|
|
423
|
+
const fieldInfo = fieldInfoById.get(dataset.fieldId);
|
|
424
|
+
const isVisualizationDataset = dataset.source === 'visualization.disp' ||
|
|
425
|
+
dataset.source === 'visualization.frame' ||
|
|
426
|
+
dataset.source === 'visualization.scalar-frame';
|
|
427
|
+
if (isVisualizationDataset && dataset.stepId == null) {
|
|
371
428
|
findings.push(finding('blocker', `result.datasets.${index}.step-required`, 'Visualization datasets must reference a result step.'));
|
|
372
429
|
}
|
|
373
|
-
if (
|
|
430
|
+
if (isVisualizationDataset && location === 'envelope') {
|
|
374
431
|
findings.push(finding('blocker', `result.datasets.${index}.visualization-field-invalid`, 'Visualization datasets must reference surface or outline node result fields, not envelope fields.'));
|
|
375
432
|
}
|
|
376
433
|
if (dataset.source === 'envelope' && location !== 'envelope') {
|
|
377
434
|
findings.push(finding('blocker', `result.datasets.${index}.envelope-field-invalid`, 'Envelope datasets must reference an envelope result field.'));
|
|
378
435
|
}
|
|
436
|
+
if ((dataset.source === 'visualization.disp' || dataset.source === 'visualization.frame') && dataset.stride !== 3) {
|
|
437
|
+
findings.push(finding('blocker', `result.datasets.${index}.visualization-stride-invalid`, 'Displacement visualization datasets must use stride 3.'));
|
|
438
|
+
}
|
|
439
|
+
if ((dataset.source === 'visualization.disp' || dataset.source === 'visualization.frame') && fieldInfo?.quantity !== 'displacement') {
|
|
440
|
+
findings.push(finding('blocker', `result.datasets.${index}.visualization-quantity-invalid`, 'Displacement visualization datasets must reference displacement result fields.'));
|
|
441
|
+
}
|
|
442
|
+
if (dataset.source === 'visualization.scalar-frame' && dataset.stride !== 1) {
|
|
443
|
+
findings.push(finding('blocker', `result.datasets.${index}.scalar-stride-invalid`, 'Scalar-frame visualization datasets must use stride 1.'));
|
|
444
|
+
}
|
|
445
|
+
if (dataset.source === 'visualization.scalar-frame' && fieldInfo?.quantity !== 'pore_pressure') {
|
|
446
|
+
findings.push(finding('blocker', `result.datasets.${index}.scalar-quantity-invalid`, 'Scalar-frame visualization datasets must reference pore-pressure result fields.'));
|
|
447
|
+
}
|
|
379
448
|
if (dataset.source === 'visualization.disp' && !arraysApproximatelyEqual(dataset.values, manifest.visualization.disp, 1e-12)) {
|
|
380
449
|
findings.push(finding('blocker', `result.datasets.${index}.disp-values-mismatch`, 'Visualization displacement dataset values must match the manifest visualization displacement array.'));
|
|
381
450
|
}
|
|
@@ -388,6 +457,18 @@ function validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNo
|
|
|
388
457
|
findings.push(finding('blocker', `result.datasets.${index}.frame-values-mismatch`, 'Visualization frame dataset values must match the referenced frame displacement array.'));
|
|
389
458
|
}
|
|
390
459
|
}
|
|
460
|
+
if (dataset.source === 'visualization.scalar-frame' && dataset.stepId != null) {
|
|
461
|
+
const frame = frameByKey.get(`${dataset.fieldId}:${dataset.stepId}`);
|
|
462
|
+
if (!frame) {
|
|
463
|
+
findings.push(finding('blocker', `result.datasets.${index}.scalar-frame-missing`, 'Scalar-frame dataset has no matching frame payload.'));
|
|
464
|
+
}
|
|
465
|
+
else if (!Array.isArray(frame.scalarValues)) {
|
|
466
|
+
findings.push(finding('blocker', `result.datasets.${index}.scalar-frame-values-missing`, 'Scalar-frame dataset must reference a frame with scalarValues.'));
|
|
467
|
+
}
|
|
468
|
+
else if (!arraysApproximatelyEqual(dataset.values, frame.scalarValues, 1e-12)) {
|
|
469
|
+
findings.push(finding('blocker', `result.datasets.${index}.scalar-frame-values-mismatch`, 'Scalar-frame dataset values must match the referenced frame scalarValues array.'));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
391
472
|
const valueCount = dataset.values.length / dataset.stride;
|
|
392
473
|
if (location === 'surface_nodes' && valueCount !== nodeCount) {
|
|
393
474
|
findings.push(finding('blocker', `result.datasets.${index}.surface-count-mismatch`, 'Surface-node dataset values must match visualization base node count.'));
|
|
@@ -480,7 +561,7 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
480
561
|
finding('blocker', 'schema.shape-invalid', 'FEM analysis case is missing required geometry, mesh, groundwater, or array fields.'),
|
|
481
562
|
]);
|
|
482
563
|
}
|
|
483
|
-
const { domain, raft, excavation, tunnel, consolidation } = caseFile.geometry;
|
|
564
|
+
const { domain, raft, excavation, tunnel, consolidation, biot } = caseFile.geometry;
|
|
484
565
|
if (caseFile.schemaVersion !== 'fem-analysis-case.v0') {
|
|
485
566
|
findings.push(finding('blocker', 'schema.unsupported', 'Only fem-analysis-case.v0 is supported.'));
|
|
486
567
|
}
|
|
@@ -499,7 +580,7 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
499
580
|
if (!caseFile.experimental) {
|
|
500
581
|
findings.push(finding('blocker', 'mode.experimental-required', 'FEM cases must be explicitly marked experimental.'));
|
|
501
582
|
}
|
|
502
|
-
if (!['foundation_settlement', 'excavation_deformation', 'tunnel_volume_loss_settlement', 'staged_settlement_consolidation'].includes(caseFile.objective)) {
|
|
583
|
+
if (!['foundation_settlement', 'excavation_deformation', 'tunnel_volume_loss_settlement', 'staged_settlement_consolidation', 'seepage_groundwater_coupling'].includes(caseFile.objective)) {
|
|
503
584
|
findings.push(finding('blocker', 'objective.unsupported', `Unsupported FEM objective: ${caseFile.objective}.`));
|
|
504
585
|
}
|
|
505
586
|
if (caseFile.objective === 'foundation_settlement' && caseFile.analysisType !== 'static_3d_small_strain') {
|
|
@@ -514,6 +595,9 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
514
595
|
if (caseFile.objective === 'staged_settlement_consolidation' && caseFile.analysisType !== 'time_dependent_1d_consolidation') {
|
|
515
596
|
findings.push(finding('blocker', 'analysis.unsupported', `Unsupported analysis type: ${caseFile.analysisType}.`));
|
|
516
597
|
}
|
|
598
|
+
if (caseFile.objective === 'seepage_groundwater_coupling' && caseFile.analysisType !== 'time_dependent_2d_biot_consolidation') {
|
|
599
|
+
findings.push(finding('blocker', 'analysis.unsupported', `Unsupported analysis type: ${caseFile.analysisType}.`));
|
|
600
|
+
}
|
|
517
601
|
if (caseFile.geometry.domain.type !== 'box') {
|
|
518
602
|
findings.push(finding('blocker', 'geometry.domain.type-invalid', `Unsupported domain type: ${String(caseFile.geometry.domain.type)}.`));
|
|
519
603
|
}
|
|
@@ -691,6 +775,75 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
691
775
|
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
776
|
}
|
|
693
777
|
}
|
|
778
|
+
if (caseFile.objective === 'seepage_groundwater_coupling') {
|
|
779
|
+
if (!biot) {
|
|
780
|
+
findings.push(finding('blocker', 'geometry.biot-missing', 'Seepage/groundwater coupling cases require plane-strain Biot geometry.'));
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
if (biot.type !== 'plane_strain_biot_column') {
|
|
784
|
+
findings.push(finding('blocker', 'geometry.biot.type-invalid', `Unsupported Biot geometry type: ${String(biot.type)}.`));
|
|
785
|
+
}
|
|
786
|
+
pushPositiveNumberFindings(findings, [
|
|
787
|
+
[biot.widthM, 'geometry.biot.width', 'Biot column width'],
|
|
788
|
+
[biot.heightM, 'geometry.biot.height', 'Biot column height'],
|
|
789
|
+
[biot.thicknessM, 'geometry.biot.thickness', 'Biot column thickness'],
|
|
790
|
+
]);
|
|
791
|
+
pushFiniteNumberFinding(findings, biot.initialPorePressureKpa, 'geometry.biot.initial-pore-pressure', 'Initial pore pressure', { nonNegative: true });
|
|
792
|
+
if (!isApproxEqual(domain.lengthM, biot.widthM, 1e-9)) {
|
|
793
|
+
findings.push(finding('blocker', 'geometry.biot.domain-width-mismatch', 'Biot column width must match the box-domain length.'));
|
|
794
|
+
}
|
|
795
|
+
if (!isApproxEqual(domain.depthM, biot.heightM, 1e-9)) {
|
|
796
|
+
findings.push(finding('blocker', 'geometry.biot.domain-height-mismatch', 'Biot column height must match the box-domain depth.'));
|
|
797
|
+
}
|
|
798
|
+
if (!isApproxEqual(domain.widthM, biot.thicknessM, 1e-9)) {
|
|
799
|
+
findings.push(finding('blocker', 'geometry.biot.domain-thickness-mismatch', 'Biot column thickness must match the box-domain width.'));
|
|
800
|
+
}
|
|
801
|
+
if (!Array.isArray(biot.timeStepsSeconds) || biot.timeStepsSeconds.length === 0) {
|
|
802
|
+
findings.push(finding('blocker', 'geometry.biot.time-steps-missing', 'Biot consolidation cases require at least one positive time step.'));
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
if (biot.timeStepsSeconds.length < FEM_MIN_BIOT_TRANSIENT_STEPS) {
|
|
806
|
+
findings.push(finding('blocker', 'geometry.biot.time-steps.too-few', `Biot consolidation cases require at least ${FEM_MIN_BIOT_TRANSIENT_STEPS} accepted transient steps for preview diagnostics.`));
|
|
807
|
+
}
|
|
808
|
+
let previousStepSeconds = 0;
|
|
809
|
+
let previousDeltaSeconds;
|
|
810
|
+
for (const [index, timeSeconds] of biot.timeStepsSeconds.entries()) {
|
|
811
|
+
if (!Number.isFinite(timeSeconds) || timeSeconds <= previousStepSeconds) {
|
|
812
|
+
findings.push(finding('blocker', `geometry.biot.time-steps.${index}.invalid`, 'Biot time steps must be finite, positive, and strictly increasing.'));
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
const deltaSeconds = timeSeconds - previousStepSeconds;
|
|
816
|
+
if (previousDeltaSeconds != null &&
|
|
817
|
+
deltaSeconds / previousDeltaSeconds > FEM_MAX_BIOT_TIME_STEP_GROWTH_RATIO) {
|
|
818
|
+
findings.push(finding('blocker', `geometry.biot.time-steps.${index}.growth-ratio`, `Biot time-step growth ratio must not exceed ${FEM_MAX_BIOT_TIME_STEP_GROWTH_RATIO} between accepted steps.`));
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
previousStepSeconds = timeSeconds;
|
|
822
|
+
previousDeltaSeconds = deltaSeconds;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (!Array.isArray(biot.porePressureBoundaries) || biot.porePressureBoundaries.length === 0) {
|
|
826
|
+
findings.push(finding('blocker', 'geometry.biot.pressure-boundary-missing', 'Biot consolidation cases require at least one prescribed pore-pressure boundary.'));
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
const boundaryIds = new Set();
|
|
830
|
+
const validPressureBoundaries = new Set(['top', 'bottom', 'left', 'right']);
|
|
831
|
+
for (const [index, boundary] of biot.porePressureBoundaries.entries()) {
|
|
832
|
+
const prefix = `geometry.biot.pressure-boundaries.${index}`;
|
|
833
|
+
if (!isRecord(boundary)) {
|
|
834
|
+
findings.push(finding('blocker', `${prefix}.shape-invalid`, 'Pore-pressure boundary must be an object.'));
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
pushUniqueStringFinding(findings, boundaryIds, boundary.id, `${prefix}.id`, 'Pore-pressure boundary id');
|
|
838
|
+
if (!validPressureBoundaries.has(String(boundary.boundary))) {
|
|
839
|
+
findings.push(finding('blocker', `${prefix}.boundary-invalid`, `Unsupported pore-pressure boundary: ${String(boundary.boundary)}.`));
|
|
840
|
+
}
|
|
841
|
+
pushFiniteNumberFinding(findings, boundary.porePressureKpa, `${prefix}.pore-pressure`, 'Prescribed pore pressure', { nonNegative: true });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
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.'));
|
|
845
|
+
}
|
|
846
|
+
}
|
|
694
847
|
if (caseFile.materials.length === 0) {
|
|
695
848
|
findings.push(finding('blocker', 'material.missing', 'At least one material is required.'));
|
|
696
849
|
}
|
|
@@ -733,6 +886,28 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
733
886
|
pushFiniteNumberFinding(findings, material.hydraulicConductivityMPerS, `${prefix}.hydraulic-conductivity`, 'Hydraulic conductivity', { positive: true });
|
|
734
887
|
}
|
|
735
888
|
}
|
|
889
|
+
if (caseFile.objective === 'seepage_groundwater_coupling') {
|
|
890
|
+
if (material.model !== 'linear_elastic') {
|
|
891
|
+
findings.push(finding('blocker', `${prefix}.biot-model-required`, 'Biot u-p preview requires a linear_elastic material with hydraulic coupling parameters.'));
|
|
892
|
+
}
|
|
893
|
+
const hasHydraulicX = material.hydraulicConductivityXMPerS != null || material.hydraulicConductivityMPerS != null;
|
|
894
|
+
if (!hasHydraulicX) {
|
|
895
|
+
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.'));
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
pushFiniteNumberFinding(findings, material.hydraulicConductivityXMPerS ?? material.hydraulicConductivityMPerS, `${prefix}.hydraulic-conductivity-x`, 'Hydraulic conductivity X', { positive: true });
|
|
899
|
+
}
|
|
900
|
+
if (material.hydraulicConductivityYMPerS != null) {
|
|
901
|
+
pushFiniteNumberFinding(findings, material.hydraulicConductivityYMPerS, `${prefix}.hydraulic-conductivity-y`, 'Hydraulic conductivity Y', { positive: true });
|
|
902
|
+
}
|
|
903
|
+
if (material.hydraulicConductivityMPerS != null) {
|
|
904
|
+
pushFiniteNumberFinding(findings, material.hydraulicConductivityMPerS, `${prefix}.hydraulic-conductivity`, 'Isotropic hydraulic conductivity', { positive: true });
|
|
905
|
+
}
|
|
906
|
+
if (!isFiniteNumber(material.biotCoefficient) || material.biotCoefficient < 0 || material.biotCoefficient > 1) {
|
|
907
|
+
findings.push(finding('blocker', `${prefix}.biot-coefficient-invalid`, 'Biot coefficient must be finite and between 0 and 1.'));
|
|
908
|
+
}
|
|
909
|
+
pushFiniteNumberFinding(findings, material.specificStorage1PerM, `${prefix}.specific-storage`, 'Specific storage', { positive: true });
|
|
910
|
+
}
|
|
736
911
|
validateEvidenceRefs(findings, material.evidenceRefs, prefix);
|
|
737
912
|
validateAssumptions(findings, material.assumptions, prefix);
|
|
738
913
|
}
|
|
@@ -740,6 +915,9 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
740
915
|
if (caseFile.objective === 'tunnel_volume_loss_settlement' && caseFile.loads.length > 0) {
|
|
741
916
|
findings.push(finding('blocker', 'load.unsupported-for-objective', 'Tunnel volume-loss settlement previews use explicit volume loss and must not include pressure loads.'));
|
|
742
917
|
}
|
|
918
|
+
if (caseFile.objective === 'seepage_groundwater_coupling' && caseFile.loads.length > 0) {
|
|
919
|
+
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.'));
|
|
920
|
+
}
|
|
743
921
|
if (caseFile.loads.length === 0) {
|
|
744
922
|
if (caseFile.objective === 'foundation_settlement') {
|
|
745
923
|
findings.push(finding('blocker', 'load.missing', 'A raft pressure load is required.'));
|
|
@@ -775,6 +953,9 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
775
953
|
if (caseFile.objective === 'staged_settlement_consolidation' && load.target !== 'ground_surface') {
|
|
776
954
|
findings.push(finding('blocker', `${prefix}.target-invalid`, 'Staged consolidation loads must target ground_surface.'));
|
|
777
955
|
}
|
|
956
|
+
if (caseFile.objective === 'seepage_groundwater_coupling') {
|
|
957
|
+
findings.push(finding('blocker', `${prefix}.target-invalid`, 'Biot u-p seepage previews do not accept pressure loads.'));
|
|
958
|
+
}
|
|
778
959
|
if (caseFile.objective === 'staged_settlement_consolidation' && consolidation) {
|
|
779
960
|
if (caseFile.loads.length !== consolidation.stages.length) {
|
|
780
961
|
findings.push(finding('blocker', 'load.stage-count-mismatch', 'Staged consolidation load count must match the consolidation stage count.'));
|
|
@@ -789,19 +970,30 @@ export function validateFemAnalysisCase(caseFile) {
|
|
|
789
970
|
}
|
|
790
971
|
}
|
|
791
972
|
const { divisionsX, divisionsY, divisionsZ } = caseFile.mesh;
|
|
792
|
-
if (caseFile.mesh.elementType !== 'hex8') {
|
|
973
|
+
if (caseFile.mesh.elementType !== 'hex8' && caseFile.mesh.elementType !== 'quad4_plane_strain') {
|
|
793
974
|
findings.push(finding('blocker', 'mesh.element-type-invalid', `Unsupported mesh element type: ${String(caseFile.mesh.elementType)}.`));
|
|
794
975
|
}
|
|
976
|
+
if (caseFile.objective === 'seepage_groundwater_coupling' && caseFile.mesh.elementType !== 'quad4_plane_strain') {
|
|
977
|
+
findings.push(finding('blocker', 'mesh.element-type-biot-required', 'Biot u-p seepage previews require quad4_plane_strain mesh elements.'));
|
|
978
|
+
}
|
|
979
|
+
if (caseFile.objective !== 'seepage_groundwater_coupling' && caseFile.mesh.elementType !== 'hex8') {
|
|
980
|
+
findings.push(finding('blocker', 'mesh.element-type-hex8-required', 'Non-Biot FEM preview cases require hex8 mesh elements.'));
|
|
981
|
+
}
|
|
795
982
|
if (!Number.isInteger(divisionsX) ||
|
|
796
983
|
!Number.isInteger(divisionsY) ||
|
|
797
984
|
!Number.isInteger(divisionsZ)) {
|
|
798
985
|
findings.push(finding('blocker', 'mesh.divisions-integer', 'Mesh divisions must be finite integers.'));
|
|
799
986
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
987
|
+
else {
|
|
988
|
+
if (caseFile.mesh.elementType === 'quad4_plane_strain') {
|
|
989
|
+
if (divisionsX < 1 || divisionsY < 1 || divisionsZ !== 1) {
|
|
990
|
+
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.'));
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
else if (divisionsX < 2 || divisionsY < 2 || divisionsZ < 1) {
|
|
994
|
+
findings.push(finding('blocker', 'mesh.too-coarse', 'Mesh divisions must be at least 2 x 2 x 1.'));
|
|
995
|
+
}
|
|
996
|
+
const nodeCount = expectedMeshCounts(caseFile.mesh).nodes;
|
|
805
997
|
if (!Number.isFinite(nodeCount) || nodeCount > FEM_MAX_PREVIEW_MESH_NODES) {
|
|
806
998
|
findings.push(finding('blocker', 'mesh.preview-node-limit', `Experimental FEM/WebGL previews are capped at ${FEM_MAX_PREVIEW_MESH_NODES} nodes.`));
|
|
807
999
|
}
|
|
@@ -945,6 +1137,135 @@ function pushApproximateMatchFinding(findings, actual, expected, code, label, to
|
|
|
945
1137
|
findings.push(finding('blocker', code, `${label} must match the embedded FEM analysis case within ${tolerance}.`));
|
|
946
1138
|
}
|
|
947
1139
|
}
|
|
1140
|
+
function validateNonlinearSolverConvergenceReport(findings, manifest, expectedLoadSteps) {
|
|
1141
|
+
const fallback = { forceBalanceTolerance: 1e-3, residualTolerance: 1e-6 };
|
|
1142
|
+
const report = manifest.solverConvergence;
|
|
1143
|
+
if (!isRecord(report)) {
|
|
1144
|
+
findings.push(finding('blocker', 'result.solver-convergence.missing', 'Nonlinear column solver manifests must include explicit convergence policy and load-step residual history.'));
|
|
1145
|
+
return fallback;
|
|
1146
|
+
}
|
|
1147
|
+
if (report.schemaVersion !== 'fem-solver-convergence-report.v1') {
|
|
1148
|
+
findings.push(finding('blocker', 'result.solver-convergence.schema-invalid', 'Unsupported solver convergence report schema.'));
|
|
1149
|
+
}
|
|
1150
|
+
if (report.status !== 'converged' && report.status !== 'nonconverged') {
|
|
1151
|
+
findings.push(finding('blocker', 'result.solver-convergence.status-invalid', 'Solver convergence status must be converged or nonconverged.'));
|
|
1152
|
+
}
|
|
1153
|
+
if (report.status === 'nonconverged') {
|
|
1154
|
+
findings.push(finding('blocker', 'result.solver-convergence.nonconverged', 'Nonlinear column solver did not satisfy its configured convergence policy.'));
|
|
1155
|
+
}
|
|
1156
|
+
const policy = report.policy;
|
|
1157
|
+
let forceBalanceTolerance = fallback.forceBalanceTolerance;
|
|
1158
|
+
let residualTolerance = fallback.residualTolerance;
|
|
1159
|
+
if (!isRecord(policy)) {
|
|
1160
|
+
findings.push(finding('blocker', 'result.solver-convergence.policy.missing', 'Solver convergence report must include a policy object.'));
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
if (policy.schemaVersion !== 'fem-convergence-policy.v1') {
|
|
1164
|
+
findings.push(finding('blocker', 'result.solver-convergence.policy.schema-invalid', 'Solver convergence policy schema is unsupported.'));
|
|
1165
|
+
}
|
|
1166
|
+
if (pushFiniteNumberFinding(findings, policy.forceBalanceTolerance, 'result.solver-convergence.policy.force-balance-tolerance', 'Solver force-balance tolerance', { positive: true })) {
|
|
1167
|
+
forceBalanceTolerance = policy.forceBalanceTolerance;
|
|
1168
|
+
}
|
|
1169
|
+
if (pushFiniteNumberFinding(findings, policy.residualTolerance, 'result.solver-convergence.policy.residual-tolerance', 'Solver residual tolerance', { positive: true })) {
|
|
1170
|
+
residualTolerance = policy.residualTolerance;
|
|
1171
|
+
}
|
|
1172
|
+
pushFiniteNumberFinding(findings, policy.porePressureMassBalanceTolerance, 'result.solver-convergence.policy.pore-pressure-mass-balance-tolerance', 'Solver pore-pressure mass-balance tolerance', { positive: true });
|
|
1173
|
+
if (!Number.isInteger(policy.maxIterations) || policy.maxIterations < 1) {
|
|
1174
|
+
findings.push(finding('blocker', 'result.solver-convergence.policy.max-iterations-invalid', 'Solver maxIterations must be a positive integer.'));
|
|
1175
|
+
}
|
|
1176
|
+
if (!Number.isInteger(policy.minAcceptedSteps) || policy.minAcceptedSteps < 1) {
|
|
1177
|
+
findings.push(finding('blocker', 'result.solver-convergence.policy.min-accepted-steps-invalid', 'Solver minAcceptedSteps must be a positive integer.'));
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (!Array.isArray(report.loadSteps)) {
|
|
1181
|
+
findings.push(finding('blocker', 'result.solver-convergence.load-steps.invalid', 'Solver convergence loadSteps must be an array.'));
|
|
1182
|
+
return { forceBalanceTolerance, residualTolerance };
|
|
1183
|
+
}
|
|
1184
|
+
if (report.loadSteps.length !== expectedLoadSteps) {
|
|
1185
|
+
findings.push(finding('blocker', 'result.solver-convergence.load-steps.count-mismatch', 'Solver convergence load steps must match consolidation stages.'));
|
|
1186
|
+
}
|
|
1187
|
+
const validTerminationReasons = new Set([
|
|
1188
|
+
'converged',
|
|
1189
|
+
'max_iterations',
|
|
1190
|
+
'force_residual_exceeded',
|
|
1191
|
+
'yield_residual_exceeded',
|
|
1192
|
+
'material_nonconvergence',
|
|
1193
|
+
'consolidation_nonconvergence',
|
|
1194
|
+
]);
|
|
1195
|
+
for (const [index, step] of report.loadSteps.entries()) {
|
|
1196
|
+
const prefix = `result.solver-convergence.loadSteps.${index}`;
|
|
1197
|
+
if (!isRecord(step)) {
|
|
1198
|
+
findings.push(finding('blocker', `${prefix}.shape-invalid`, 'Solver convergence load step must be an object.'));
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
const stepOk = pushFiniteNumberFinding(findings, step.step, `${prefix}.step`, 'Solver convergence load-step number', { positive: true });
|
|
1202
|
+
if (stepOk && (!Number.isInteger(step.step) || step.step !== index + 1)) {
|
|
1203
|
+
findings.push(finding('blocker', `${prefix}.step.sequence-invalid`, 'Solver convergence load-step numbers must be sequential.'));
|
|
1204
|
+
}
|
|
1205
|
+
if (step.stageId != null && !isNonEmptyString(step.stageId)) {
|
|
1206
|
+
findings.push(finding('blocker', `${prefix}.stage-id.invalid`, 'Solver convergence stageId must be a non-empty string when present.'));
|
|
1207
|
+
}
|
|
1208
|
+
if (!Number.isInteger(step.iterations) || step.iterations < 0) {
|
|
1209
|
+
findings.push(finding('blocker', `${prefix}.iterations.invalid`, 'Solver convergence iterations must be a non-negative integer.'));
|
|
1210
|
+
}
|
|
1211
|
+
const residualOk = pushFiniteNumberFinding(findings, step.residualRatio, `${prefix}.residual-ratio`, 'Solver convergence residual ratio', { nonNegative: true });
|
|
1212
|
+
const forceToleranceOk = pushFiniteNumberFinding(findings, step.forceBalanceTolerance, `${prefix}.force-balance-tolerance`, 'Solver convergence force-balance tolerance', { positive: true });
|
|
1213
|
+
const stepForceTolerance = forceToleranceOk ? step.forceBalanceTolerance : forceBalanceTolerance;
|
|
1214
|
+
if (step.yieldResidualRatio != null) {
|
|
1215
|
+
const yieldResidualOk = pushFiniteNumberFinding(findings, step.yieldResidualRatio, `${prefix}.yield-residual-ratio`, 'Solver convergence yield residual ratio', { nonNegative: true });
|
|
1216
|
+
const stepResidualTolerance = isFiniteNumber(step.residualTolerance) && step.residualTolerance > 0
|
|
1217
|
+
? step.residualTolerance
|
|
1218
|
+
: residualTolerance;
|
|
1219
|
+
if (yieldResidualOk && step.yieldResidualRatio > stepResidualTolerance) {
|
|
1220
|
+
findings.push(finding('blocker', `${prefix}.yield-residual-too-large`, 'Solver convergence yield residual exceeds the reported tolerance.'));
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (step.residualTolerance != null) {
|
|
1224
|
+
pushFiniteNumberFinding(findings, step.residualTolerance, `${prefix}.residual-tolerance`, 'Solver convergence residual tolerance', { positive: true });
|
|
1225
|
+
}
|
|
1226
|
+
if (typeof step.converged !== 'boolean') {
|
|
1227
|
+
findings.push(finding('blocker', `${prefix}.converged.invalid`, 'Solver convergence load-step converged must be boolean.'));
|
|
1228
|
+
}
|
|
1229
|
+
else if (!step.converged) {
|
|
1230
|
+
findings.push(finding('blocker', `${prefix}.nonconverged`, 'Solver convergence load step did not satisfy the reported policy.'));
|
|
1231
|
+
}
|
|
1232
|
+
if (typeof step.terminationReason !== 'string' || !validTerminationReasons.has(step.terminationReason)) {
|
|
1233
|
+
findings.push(finding('blocker', `${prefix}.termination-reason.invalid`, 'Solver convergence termination reason is unsupported.'));
|
|
1234
|
+
}
|
|
1235
|
+
if (residualOk && step.residualRatio > stepForceTolerance) {
|
|
1236
|
+
findings.push(finding('blocker', `${prefix}.force-residual-too-large`, 'Solver convergence force residual exceeds the reported tolerance.'));
|
|
1237
|
+
}
|
|
1238
|
+
if (!Array.isArray(step.residualHistory) || step.residualHistory.length === 0) {
|
|
1239
|
+
findings.push(finding('blocker', `${prefix}.residual-history.missing`, 'Solver convergence load step must include non-empty residual history.'));
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
for (const [historyIndex, history] of step.residualHistory.entries()) {
|
|
1243
|
+
const historyPrefix = `${prefix}.residualHistory.${historyIndex}`;
|
|
1244
|
+
if (!isRecord(history)) {
|
|
1245
|
+
findings.push(finding('blocker', `${historyPrefix}.shape-invalid`, 'Solver convergence residual history entry must be an object.'));
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
if (!Number.isInteger(history.iteration) || history.iteration < 0) {
|
|
1249
|
+
findings.push(finding('blocker', `${historyPrefix}.iteration.invalid`, 'Solver convergence residual history iteration must be a non-negative integer.'));
|
|
1250
|
+
}
|
|
1251
|
+
pushFiniteNumberFinding(findings, history.residualRatio, `${historyPrefix}.residual-ratio`, 'Solver convergence residual history ratio', { nonNegative: true });
|
|
1252
|
+
pushFiniteNumberFinding(findings, history.forceBalanceTolerance, `${historyPrefix}.force-balance-tolerance`, 'Solver convergence residual history force-balance tolerance', { positive: true });
|
|
1253
|
+
if (history.yieldResidualRatio != null) {
|
|
1254
|
+
pushFiniteNumberFinding(findings, history.yieldResidualRatio, `${historyPrefix}.yield-residual-ratio`, 'Solver convergence residual history yield residual ratio', { nonNegative: true });
|
|
1255
|
+
}
|
|
1256
|
+
if (history.residualTolerance != null) {
|
|
1257
|
+
pushFiniteNumberFinding(findings, history.residualTolerance, `${historyPrefix}.residual-tolerance`, 'Solver convergence residual history residual tolerance', { positive: true });
|
|
1258
|
+
}
|
|
1259
|
+
if (typeof history.converged !== 'boolean') {
|
|
1260
|
+
findings.push(finding('blocker', `${historyPrefix}.converged.invalid`, 'Solver convergence residual history converged must be boolean.'));
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
if (report.status === 'nonconverged' && !isRecord(report.failure)) {
|
|
1265
|
+
findings.push(finding('blocker', 'result.solver-convergence.failure.missing', 'Nonconverged solver reports must include failure details.'));
|
|
1266
|
+
}
|
|
1267
|
+
return { forceBalanceTolerance, residualTolerance };
|
|
1268
|
+
}
|
|
948
1269
|
function validateResultEnvelopeSemantics(findings, manifest) {
|
|
949
1270
|
const { envelope, analysisCase } = manifest;
|
|
950
1271
|
const maxSettlementOk = pushFiniteNumberFinding(findings, envelope.maxSettlementMm, 'result.envelope.max-settlement', 'Envelope max settlement', { nonNegative: true });
|
|
@@ -960,6 +1281,7 @@ function validateResultEnvelopeSemantics(findings, manifest) {
|
|
|
960
1281
|
['excavation_deformation', ['builtin-staged-excavation-demo']],
|
|
961
1282
|
['tunnel_volume_loss_settlement', ['builtin-tunnel-volume-loss-demo']],
|
|
962
1283
|
['staged_settlement_consolidation', ['builtin-staged-consolidation-1d', 'builtin-nonlinear-column-v0']],
|
|
1284
|
+
['seepage_groundwater_coupling', ['builtin-biot-up-plane-strain-v0']],
|
|
963
1285
|
]);
|
|
964
1286
|
const expectedBackends = expectedBackendByObjective.get(analysisCase.objective);
|
|
965
1287
|
if (expectedBackends && !expectedBackends.includes(manifest.backend.id)) {
|
|
@@ -1077,6 +1399,7 @@ function validateResultEnvelopeSemantics(findings, manifest) {
|
|
|
1077
1399
|
pushApproximateMatchFinding(findings, envelope.drainagePathM, expectedDrainagePathM, 'result.envelope.consolidation-drainage-path-mismatch', 'Consolidation drainage path', 0.001);
|
|
1078
1400
|
pushApproximateMatchFinding(findings, envelope.consolidationDurationYears, expectedDurationYears, 'result.envelope.consolidation-duration-mismatch', 'Consolidation duration', 0.001);
|
|
1079
1401
|
if (manifest.backend.id === 'builtin-nonlinear-column-v0') {
|
|
1402
|
+
const solverTolerances = validateNonlinearSolverConvergenceReport(findings, manifest, consolidation.stages.length);
|
|
1080
1403
|
const loadStepsOk = pushFiniteNumberFinding(findings, envelope.solverLoadSteps, 'result.envelope.solver-load-steps', 'Envelope solver load steps', { positive: true });
|
|
1081
1404
|
const iterationsOk = pushFiniteNumberFinding(findings, envelope.solverIterations, 'result.envelope.solver-iterations', 'Envelope solver iterations', { positive: true });
|
|
1082
1405
|
const solverResidualOk = pushFiniteNumberFinding(findings, envelope.maxSolverResidualRatio, 'result.envelope.max-solver-residual-ratio', 'Envelope max solver residual ratio', { nonNegative: true });
|
|
@@ -1088,14 +1411,105 @@ function validateResultEnvelopeSemantics(findings, manifest) {
|
|
|
1088
1411
|
if (iterationsOk && !Number.isInteger(envelope.solverIterations)) {
|
|
1089
1412
|
findings.push(finding('blocker', 'result.envelope.solver-iterations-integer', 'Nonlinear column solver iterations must be an integer.'));
|
|
1090
1413
|
}
|
|
1091
|
-
if (solverResidualOk && envelope.maxSolverResidualRatio >
|
|
1414
|
+
if (solverResidualOk && envelope.maxSolverResidualRatio > solverTolerances.forceBalanceTolerance) {
|
|
1092
1415
|
findings.push(finding('blocker', 'result.envelope.solver-residual-too-large', 'Nonlinear column solver residual exceeds the force-balance tolerance.'));
|
|
1093
1416
|
}
|
|
1094
|
-
if (yieldResidualOk && envelope.maxYieldResidualRatio >
|
|
1417
|
+
if (yieldResidualOk && envelope.maxYieldResidualRatio > solverTolerances.residualTolerance) {
|
|
1095
1418
|
findings.push(finding('blocker', 'result.envelope.yield-residual-too-large', 'Nonlinear column solver yield residual exceeds the material return-map tolerance.'));
|
|
1096
1419
|
}
|
|
1097
1420
|
}
|
|
1098
1421
|
}
|
|
1422
|
+
if (analysisCase.objective === 'seepage_groundwater_coupling') {
|
|
1423
|
+
const biot = analysisCase.geometry.biot;
|
|
1424
|
+
if (!biot)
|
|
1425
|
+
return;
|
|
1426
|
+
const boundaryPressureValues = biot.porePressureBoundaries.map((boundary) => boundary.porePressureKpa);
|
|
1427
|
+
const expectedMaxPressureKpa = Math.max(biot.initialPorePressureKpa, ...boundaryPressureValues);
|
|
1428
|
+
const minPorePressureOk = pushFiniteNumberFinding(findings, envelope.minPorePressureKpa, 'result.envelope.min-pore-pressure', 'Envelope minimum pore pressure', { nonNegative: true });
|
|
1429
|
+
const maxPorePressureOk = pushFiniteNumberFinding(findings, envelope.maxPorePressureKpa, 'result.envelope.max-pore-pressure', 'Envelope maximum pore pressure', { nonNegative: true });
|
|
1430
|
+
const maxExcessOk = pushFiniteNumberFinding(findings, envelope.maxExcessPorePressureKpa, 'result.envelope.max-excess-pore-pressure', 'Envelope maximum excess pore pressure', { nonNegative: true });
|
|
1431
|
+
pushFiniteNumberFinding(findings, envelope.maxBiotCouplingKpa, 'result.envelope.max-biot-coupling', 'Envelope maximum Biot coupling', { nonNegative: true });
|
|
1432
|
+
const massBalanceOk = pushFiniteNumberFinding(findings, envelope.porePressureMassBalanceErrorRatio, 'result.envelope.pore-pressure-mass-balance-error-ratio', 'Envelope pore-pressure mass-balance error ratio', { nonNegative: true });
|
|
1433
|
+
pushFiniteNumberFinding(findings, envelope.maxFreePorePressureResidualM3PerS, 'result.envelope.max-free-pore-pressure-residual', 'Envelope maximum free pore-pressure residual', { nonNegative: true });
|
|
1434
|
+
pushFiniteNumberFinding(findings, envelope.freePorePressureResidualL1M3PerS, 'result.envelope.free-pore-pressure-residual-l1', 'Envelope free pore-pressure residual L1 norm', { nonNegative: true });
|
|
1435
|
+
pushFiniteNumberFinding(findings, envelope.prescribedPorePressureResidualL1M3PerS, 'result.envelope.prescribed-pore-pressure-residual-l1', 'Envelope prescribed pore-pressure residual L1 norm', { nonNegative: true });
|
|
1436
|
+
const averagePorePressureOk = pushFiniteNumberFinding(findings, envelope.averagePorePressureKpa, 'result.envelope.average-pore-pressure', 'Envelope average pore pressure', { nonNegative: true });
|
|
1437
|
+
const averageFreePorePressureOk = pushFiniteNumberFinding(findings, envelope.averageFreePorePressureKpa, 'result.envelope.average-free-pore-pressure', 'Envelope average free pore pressure', { nonNegative: true });
|
|
1438
|
+
const dissipationRatioOk = pushFiniteNumberFinding(findings, envelope.porePressureDissipationRatio, 'result.envelope.pore-pressure-dissipation-ratio', 'Envelope pore-pressure dissipation ratio', { nonNegative: true });
|
|
1439
|
+
pushFiniteNumberFinding(findings, envelope.maxPorePressureChangeRateKpaPerS, 'result.envelope.max-pore-pressure-change-rate', 'Envelope maximum pore-pressure change rate', { nonNegative: true });
|
|
1440
|
+
const timeStepCountOk = pushFiniteNumberFinding(findings, envelope.timeStepCount, 'result.envelope.time-step-count', 'Envelope time-step count', { positive: true });
|
|
1441
|
+
const coupledUnknownsOk = pushFiniteNumberFinding(findings, envelope.coupledUnknownCount, 'result.envelope.coupled-unknown-count', 'Envelope coupled unknown count', { positive: true });
|
|
1442
|
+
const displacementDofsOk = pushFiniteNumberFinding(findings, envelope.displacementDofCount, 'result.envelope.displacement-dof-count', 'Envelope displacement DOF count', { positive: true });
|
|
1443
|
+
const porePressureDofsOk = pushFiniteNumberFinding(findings, envelope.porePressureDofCount, 'result.envelope.pore-pressure-dof-count', 'Envelope pore-pressure DOF count', { positive: true });
|
|
1444
|
+
if (minPorePressureOk && maxPorePressureOk && envelope.minPorePressureKpa > envelope.maxPorePressureKpa) {
|
|
1445
|
+
findings.push(finding('blocker', 'result.envelope.pore-pressure-range-invalid', 'Envelope minimum pore pressure cannot exceed maximum pore pressure.'));
|
|
1446
|
+
}
|
|
1447
|
+
if (maxPorePressureOk && envelope.maxPorePressureKpa > expectedMaxPressureKpa + 1e-6) {
|
|
1448
|
+
findings.push(finding('blocker', 'result.envelope.pore-pressure-upper-bound-invalid', 'Envelope maximum pore pressure cannot exceed the initial or prescribed boundary pressure envelope.'));
|
|
1449
|
+
}
|
|
1450
|
+
if (averagePorePressureOk && minPorePressureOk && maxPorePressureOk && (envelope.averagePorePressureKpa < envelope.minPorePressureKpa - 1e-6 ||
|
|
1451
|
+
envelope.averagePorePressureKpa > envelope.maxPorePressureKpa + 1e-6)) {
|
|
1452
|
+
findings.push(finding('blocker', 'result.envelope.average-pore-pressure-range-invalid', 'Envelope average pore pressure must stay within the reported pore-pressure range.'));
|
|
1453
|
+
}
|
|
1454
|
+
if (averageFreePorePressureOk && maxPorePressureOk && envelope.averageFreePorePressureKpa > envelope.maxPorePressureKpa + 1e-6) {
|
|
1455
|
+
findings.push(finding('blocker', 'result.envelope.average-free-pore-pressure-range-invalid', 'Envelope average free pore pressure cannot exceed the reported maximum pore pressure.'));
|
|
1456
|
+
}
|
|
1457
|
+
if (dissipationRatioOk && envelope.porePressureDissipationRatio > 1) {
|
|
1458
|
+
findings.push(finding('blocker', 'result.envelope.pore-pressure-dissipation-ratio-invalid', 'Envelope pore-pressure dissipation ratio must be between 0 and 1.'));
|
|
1459
|
+
}
|
|
1460
|
+
if (maxExcessOk && maxPorePressureOk) {
|
|
1461
|
+
pushApproximateMatchFinding(findings, envelope.maxExcessPorePressureKpa, envelope.maxPorePressureKpa, 'result.envelope.max-excess-pore-pressure-mismatch', 'Maximum excess pore pressure', 1e-6);
|
|
1462
|
+
}
|
|
1463
|
+
if (timeStepCountOk && (!Number.isInteger(envelope.timeStepCount) || envelope.timeStepCount !== biot.timeStepsSeconds.length)) {
|
|
1464
|
+
findings.push(finding('blocker', 'result.envelope.time-step-count-mismatch', 'Biot envelope time-step count must match the embedded time-step schedule.'));
|
|
1465
|
+
}
|
|
1466
|
+
if (timeStepCountOk && envelope.timeStepCount < FEM_MIN_BIOT_TRANSIENT_STEPS) {
|
|
1467
|
+
findings.push(finding('blocker', 'result.envelope.time-step-count-too-small', 'Biot envelope time-step count is below the preview transient-step policy.'));
|
|
1468
|
+
}
|
|
1469
|
+
if (coupledUnknownsOk && !Number.isInteger(envelope.coupledUnknownCount)) {
|
|
1470
|
+
findings.push(finding('blocker', 'result.envelope.coupled-unknown-count-integer', 'Coupled unknown count must be an integer.'));
|
|
1471
|
+
}
|
|
1472
|
+
if (displacementDofsOk && !Number.isInteger(envelope.displacementDofCount)) {
|
|
1473
|
+
findings.push(finding('blocker', 'result.envelope.displacement-dof-count-integer', 'Displacement DOF count must be an integer.'));
|
|
1474
|
+
}
|
|
1475
|
+
if (porePressureDofsOk && !Number.isInteger(envelope.porePressureDofCount)) {
|
|
1476
|
+
findings.push(finding('blocker', 'result.envelope.pore-pressure-dof-count-integer', 'Pore-pressure DOF count must be an integer.'));
|
|
1477
|
+
}
|
|
1478
|
+
if (coupledUnknownsOk && displacementDofsOk && porePressureDofsOk && envelope.coupledUnknownCount > envelope.displacementDofCount + envelope.porePressureDofCount) {
|
|
1479
|
+
findings.push(finding('blocker', 'result.envelope.coupled-unknown-count-mismatch', 'Coupled unknown count cannot exceed displacement plus pore-pressure DOF counts.'));
|
|
1480
|
+
}
|
|
1481
|
+
if (massBalanceOk && envelope.porePressureMassBalanceErrorRatio > 1e-3) {
|
|
1482
|
+
findings.push(finding('blocker', 'result.envelope.pore-pressure-mass-balance-too-large', 'Biot pore-pressure mass-balance error ratio exceeds the preview tolerance.'));
|
|
1483
|
+
}
|
|
1484
|
+
pushApproximateMatchFinding(findings, envelope.totalLoadKn, 0, 'result.envelope.biot-total-load-invalid', 'Biot preview total load', 0.001);
|
|
1485
|
+
pushApproximateMatchFinding(findings, envelope.reactionKn, 0, 'result.envelope.biot-reaction-invalid', 'Biot preview reaction', 0.001);
|
|
1486
|
+
if (maxSettlementOk && isFiniteNumber(envelope.finalSettlementMm)) {
|
|
1487
|
+
pushApproximateMatchFinding(findings, envelope.maxSettlementMm, envelope.finalSettlementMm, 'result.envelope.biot-max-settlement-mismatch', 'Biot maximum settlement', 0.001);
|
|
1488
|
+
}
|
|
1489
|
+
const pressureAudit = manifest.pressureAudit;
|
|
1490
|
+
if (!pressureAudit) {
|
|
1491
|
+
findings.push(finding('blocker', 'result.pressure-audit.missing', 'Biot result manifests must include a pressure audit.'));
|
|
1492
|
+
}
|
|
1493
|
+
else {
|
|
1494
|
+
const pressureAuditEntries = [
|
|
1495
|
+
['freePorePressureResidualL1M3PerS', 'free-pore-pressure-residual-l1', 'Free pore-pressure residual L1 norm'],
|
|
1496
|
+
['prescribedPorePressureResidualL1M3PerS', 'prescribed-pore-pressure-residual-l1', 'Prescribed pore-pressure residual L1 norm'],
|
|
1497
|
+
['netPrescribedPressureBoundaryFlowM3PerS', 'net-prescribed-pressure-boundary-flow', 'Net prescribed pressure boundary flow'],
|
|
1498
|
+
['appliedNodalFluxSumM3PerS', 'applied-nodal-flux-sum', 'Applied nodal flux sum'],
|
|
1499
|
+
['storageRateSumM3PerS', 'storage-rate-sum', 'Storage-rate sum'],
|
|
1500
|
+
['couplingRateSumM3PerS', 'coupling-rate-sum', 'Coupling-rate sum'],
|
|
1501
|
+
['darcyFlowRateSumM3PerS', 'darcy-flow-rate-sum', 'Darcy flow-rate sum'],
|
|
1502
|
+
];
|
|
1503
|
+
for (const [key, code, label] of pressureAuditEntries) {
|
|
1504
|
+
pushFiniteNumberFinding(findings, pressureAudit[key], `result.pressure-audit.${code}`, label);
|
|
1505
|
+
}
|
|
1506
|
+
pushApproximateMatchFinding(findings, pressureAudit.freePorePressureResidualL1M3PerS, envelope.freePorePressureResidualL1M3PerS, 'result.pressure-audit.free-residual-envelope-mismatch', 'Pressure-audit free residual', 1e-12);
|
|
1507
|
+
pushApproximateMatchFinding(findings, pressureAudit.prescribedPorePressureResidualL1M3PerS, envelope.prescribedPorePressureResidualL1M3PerS, 'result.pressure-audit.prescribed-residual-envelope-mismatch', 'Pressure-audit prescribed residual', 1e-12);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
else if (manifest.pressureAudit != null) {
|
|
1511
|
+
findings.push(finding('blocker', 'result.pressure-audit.unexpected', 'Pressure audit is only valid for Biot u-p seepage result manifests.'));
|
|
1512
|
+
}
|
|
1099
1513
|
}
|
|
1100
1514
|
export function validateFemResultManifest(manifest) {
|
|
1101
1515
|
const findings = [];
|
|
@@ -1130,6 +1544,7 @@ export function validateFemResultManifest(manifest) {
|
|
|
1130
1544
|
'builtin-tunnel-volume-loss-demo',
|
|
1131
1545
|
'builtin-staged-consolidation-1d',
|
|
1132
1546
|
'builtin-nonlinear-column-v0',
|
|
1547
|
+
'builtin-biot-up-plane-strain-v0',
|
|
1133
1548
|
]);
|
|
1134
1549
|
if (!validBackendIds.has(manifest.backend.id)) {
|
|
1135
1550
|
findings.push(finding('blocker', 'result.backend.id-invalid', `Unsupported FEM result backend: ${String(manifest.backend.id)}.`));
|
|
@@ -1170,12 +1585,7 @@ export function validateFemResultManifest(manifest) {
|
|
|
1170
1585
|
}
|
|
1171
1586
|
pushIndexArrayFindings(findings, 'tri', visualization.tri, nodeCount, 3);
|
|
1172
1587
|
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;
|
|
1588
|
+
const { nodes: expectedMeshNodes, elements: expectedMeshElements } = expectedMeshCounts(manifest.analysisCase.mesh);
|
|
1179
1589
|
const meshDivisions = manifest.mesh.divisions;
|
|
1180
1590
|
if (!Number.isInteger(manifest.mesh.nodes) || manifest.mesh.nodes !== expectedMeshNodes) {
|
|
1181
1591
|
findings.push(finding('blocker', 'result.mesh.nodes-mismatch', 'Result mesh node count must match the embedded analysis case mesh divisions.'));
|
|
@@ -1232,6 +1642,12 @@ export function validateFemResultManifest(manifest) {
|
|
|
1232
1642
|
if (frame.color.length / 3 !== nodeCount) {
|
|
1233
1643
|
findings.push(finding('blocker', `result.frames.${index}.color.node-count-mismatch`, 'Frame color vectors must match base nodes.'));
|
|
1234
1644
|
}
|
|
1645
|
+
if (frame.scalarValues != null) {
|
|
1646
|
+
pushFiniteArrayFindings(findings, `frames.${index}.scalarValues`, frame.scalarValues, 1);
|
|
1647
|
+
if (frame.scalarValues.length !== nodeCount) {
|
|
1648
|
+
findings.push(finding('blocker', `result.frames.${index}.scalar-values.node-count-mismatch`, 'Frame scalarValues must match base nodes.'));
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1235
1651
|
}
|
|
1236
1652
|
}
|
|
1237
1653
|
validateOptionalResultMetadata(findings, manifest, nodeCount, outlineNodeCount);
|