@geotechcli/core 0.4.112 → 0.4.114
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/engineering-evidence.d.ts +48 -0
- package/dist/fem/engineering-evidence.d.ts.map +1 -1
- package/dist/fem/engineering-evidence.js +473 -23
- package/dist/fem/engineering-evidence.js.map +1 -1
- package/dist/fem/ground-model-draft.d.ts +6 -0
- package/dist/fem/ground-model-draft.d.ts.map +1 -1
- package/dist/fem/ground-model-draft.js +14 -0
- package/dist/fem/ground-model-draft.js.map +1 -1
- package/dist/fem/index.d.ts +1 -1
- package/dist/fem/index.d.ts.map +1 -1
- package/dist/fem/index.js +1 -1
- package/dist/fem/index.js.map +1 -1
- package/dist/fem/plane-strain-assembly.d.ts +52 -0
- package/dist/fem/plane-strain-assembly.d.ts.map +1 -1
- package/dist/fem/plane-strain-assembly.js +167 -8
- package/dist/fem/plane-strain-assembly.js.map +1 -1
- package/dist/fem/production-readiness.js +2 -2
- package/dist/fem/production-readiness.js.map +1 -1
- package/dist/ingest/document-evidence-packet.d.ts +8 -8
- package/dist/meta/metadata.json +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { calculateLateralEarthPressure } from '../geo/lateral-earth-pressure.js';
|
|
2
|
-
import { buildPlaneStrainRectangularMesh, runPlaneStrainBiotConsolidation, runPlaneStrainDruckerPragerLoadSteps, runPlaneStrainQuad4Assembly, runPlaneStrainSteadySeepage, } from './plane-strain-assembly.js';
|
|
3
|
+
import { buildPlaneStrainRectangularMesh, runPlaneStrainBiotConsolidation, runPlaneStrainDruckerPragerBiotPressureReplay, runPlaneStrainDruckerPragerLoadSteps, runPlaneStrainQuad4Assembly, runPlaneStrainSteadySeepage, } from './plane-strain-assembly.js';
|
|
3
4
|
import { runFemSupportMemberDesignCheck } from './support-design.js';
|
|
4
5
|
export const DEFAULT_FEM_CONVERGENCE_POLICY = {
|
|
5
6
|
schemaVersion: 'fem-convergence-policy.v1',
|
|
@@ -161,6 +162,33 @@ function round(value, digits = 6) {
|
|
|
161
162
|
const scale = 10 ** digits;
|
|
162
163
|
return Math.round(value * scale) / scale;
|
|
163
164
|
}
|
|
165
|
+
function canonicalFemBenchmarkJson(value) {
|
|
166
|
+
if (value === null || value === undefined)
|
|
167
|
+
return 'null';
|
|
168
|
+
if (typeof value === 'number') {
|
|
169
|
+
if (!Number.isFinite(value))
|
|
170
|
+
throw new Error('Cannot hash non-finite FEM benchmark value.');
|
|
171
|
+
return JSON.stringify(round(value, 12));
|
|
172
|
+
}
|
|
173
|
+
if (typeof value === 'string' || typeof value === 'boolean') {
|
|
174
|
+
return JSON.stringify(value);
|
|
175
|
+
}
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
return `[${value.map((item) => canonicalFemBenchmarkJson(item)).join(',')}]`;
|
|
178
|
+
}
|
|
179
|
+
if (typeof value === 'object') {
|
|
180
|
+
const entries = Object.entries(value)
|
|
181
|
+
.filter(([, itemValue]) => itemValue !== undefined)
|
|
182
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
183
|
+
return `{${entries
|
|
184
|
+
.map(([key, itemValue]) => `${JSON.stringify(key)}:${canonicalFemBenchmarkJson(itemValue)}`)
|
|
185
|
+
.join(',')}}`;
|
|
186
|
+
}
|
|
187
|
+
return JSON.stringify(String(value));
|
|
188
|
+
}
|
|
189
|
+
function hashFemBenchmarkPayload(value) {
|
|
190
|
+
return createHash('sha256').update(canonicalFemBenchmarkJson(value)).digest('hex');
|
|
191
|
+
}
|
|
164
192
|
function isNonEmptyString(value) {
|
|
165
193
|
return typeof value === 'string' && value.trim().length > 0;
|
|
166
194
|
}
|
|
@@ -183,6 +211,63 @@ function hasReferenceSolverCitation(citation) {
|
|
|
183
211
|
function hasValidSha256(value) {
|
|
184
212
|
return typeof value === 'string' && /^[a-f0-9]{64}$/i.test(value);
|
|
185
213
|
}
|
|
214
|
+
function hasSolverRunMetadata(value) {
|
|
215
|
+
if (value == null || typeof value !== 'object')
|
|
216
|
+
return false;
|
|
217
|
+
const metadata = value;
|
|
218
|
+
return isNonEmptyString(metadata.name) &&
|
|
219
|
+
isNonEmptyString(metadata.version) &&
|
|
220
|
+
(metadata.solverType === 'geotechcli-kernel' ||
|
|
221
|
+
metadata.solverType === 'published-reference' ||
|
|
222
|
+
metadata.solverType === 'commercial-solver' ||
|
|
223
|
+
metadata.solverType === 'open-source-solver' ||
|
|
224
|
+
metadata.solverType === 'deterministic-fixture');
|
|
225
|
+
}
|
|
226
|
+
function hasFiniteSeriesStatistics(value) {
|
|
227
|
+
if (value == null || typeof value !== 'object')
|
|
228
|
+
return false;
|
|
229
|
+
const statistics = value;
|
|
230
|
+
return Number.isFinite(statistics.min) &&
|
|
231
|
+
Number.isFinite(statistics.max) &&
|
|
232
|
+
Number.isFinite(statistics.final) &&
|
|
233
|
+
(statistics.mean == null || Number.isFinite(statistics.mean));
|
|
234
|
+
}
|
|
235
|
+
function hasValidSeriesSummary(value) {
|
|
236
|
+
if (value == null || typeof value !== 'object')
|
|
237
|
+
return false;
|
|
238
|
+
const summary = value;
|
|
239
|
+
const pointCount = summary.pointCount;
|
|
240
|
+
const maxAbsoluteError = summary.maxAbsoluteError;
|
|
241
|
+
const maxRelativeError = summary.maxRelativeError;
|
|
242
|
+
const rmsError = summary.rmsError;
|
|
243
|
+
return isNonEmptyString(summary.xQuantity) &&
|
|
244
|
+
isNonEmptyString(summary.xUnit) &&
|
|
245
|
+
isNonEmptyString(summary.yQuantity) &&
|
|
246
|
+
isNonEmptyString(summary.yUnit) &&
|
|
247
|
+
typeof pointCount === 'number' &&
|
|
248
|
+
Number.isInteger(pointCount) &&
|
|
249
|
+
pointCount > 0 &&
|
|
250
|
+
hasFiniteSeriesStatistics(summary.actual) &&
|
|
251
|
+
hasFiniteSeriesStatistics(summary.expected) &&
|
|
252
|
+
typeof maxAbsoluteError === 'number' &&
|
|
253
|
+
Number.isFinite(maxAbsoluteError) &&
|
|
254
|
+
maxAbsoluteError >= 0 &&
|
|
255
|
+
typeof maxRelativeError === 'number' &&
|
|
256
|
+
Number.isFinite(maxRelativeError) &&
|
|
257
|
+
maxRelativeError >= 0 &&
|
|
258
|
+
(rmsError == null || (Number.isFinite(rmsError) && rmsError >= 0)) &&
|
|
259
|
+
hasValidSha256(summary.seriesHashSha256);
|
|
260
|
+
}
|
|
261
|
+
function seriesSummarySatisfiesRequirementTolerance(summary, requirement) {
|
|
262
|
+
if (requirement.toleranceType === 'absolute') {
|
|
263
|
+
return summary.maxAbsoluteError <= requirement.tolerance;
|
|
264
|
+
}
|
|
265
|
+
if (requirement.toleranceType === 'relative') {
|
|
266
|
+
return summary.maxRelativeError <= requirement.tolerance;
|
|
267
|
+
}
|
|
268
|
+
return summary.maxAbsoluteError <= requirement.tolerance ||
|
|
269
|
+
summary.maxRelativeError <= requirement.tolerance;
|
|
270
|
+
}
|
|
186
271
|
function copyExternalBenchmarkReference(reference) {
|
|
187
272
|
return {
|
|
188
273
|
...reference,
|
|
@@ -210,6 +295,24 @@ function copyExternalBenchmarkQuantityRequirement(requirement) {
|
|
|
210
295
|
function copyExternalBenchmarkComparisonResult(result) {
|
|
211
296
|
return {
|
|
212
297
|
...result,
|
|
298
|
+
candidateSolver: result.candidateSolver != null
|
|
299
|
+
? { ...result.candidateSolver }
|
|
300
|
+
: result.candidateSolver,
|
|
301
|
+
...(result.referenceSolver ? { referenceSolver: { ...result.referenceSolver } } : {}),
|
|
302
|
+
...(result.seriesSummary
|
|
303
|
+
? {
|
|
304
|
+
seriesSummary: {
|
|
305
|
+
...result.seriesSummary,
|
|
306
|
+
actual: result.seriesSummary.actual != null
|
|
307
|
+
? { ...result.seriesSummary.actual }
|
|
308
|
+
: result.seriesSummary.actual,
|
|
309
|
+
expected: result.seriesSummary.expected != null
|
|
310
|
+
? { ...result.seriesSummary.expected }
|
|
311
|
+
: result.seriesSummary.expected,
|
|
312
|
+
...(result.seriesSummary.notes ? { notes: [...result.seriesSummary.notes] } : {}),
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
: {}),
|
|
213
316
|
...(result.notes ? { notes: [...result.notes] } : {}),
|
|
214
317
|
};
|
|
215
318
|
}
|
|
@@ -223,6 +326,47 @@ export function buildFemExternalBenchmarkAcceptanceContract(options = {}) {
|
|
|
223
326
|
const blockerCodes = [];
|
|
224
327
|
const referenceById = new Map(references.map((reference) => [reference.id, reference]));
|
|
225
328
|
const quantityById = new Map(requiredQuantities.map((requirement) => [requirement.id, requirement]));
|
|
329
|
+
function referenceRequiresSolverRunMetadata(reference) {
|
|
330
|
+
return reference?.sourceType === 'commercial-solver' || reference?.sourceType === 'open-source-solver';
|
|
331
|
+
}
|
|
332
|
+
function comparisonIsAcceptedForRequirement(result, requirement, reference, sourceType) {
|
|
333
|
+
if (!reference)
|
|
334
|
+
return false;
|
|
335
|
+
if (sourceType && reference.sourceType !== sourceType)
|
|
336
|
+
return false;
|
|
337
|
+
if (result.quantityRequirementId !== requirement.id || !result.accepted)
|
|
338
|
+
return false;
|
|
339
|
+
if (result.comparisonKind !== 'scalar' && result.comparisonKind !== 'series-summary')
|
|
340
|
+
return false;
|
|
341
|
+
if (result.comparisonKind === 'series-summary' && !hasValidSeriesSummary(result.seriesSummary))
|
|
342
|
+
return false;
|
|
343
|
+
if (result.comparisonKind === 'series-summary' &&
|
|
344
|
+
result.seriesSummary &&
|
|
345
|
+
!seriesSummarySatisfiesRequirementTolerance(result.seriesSummary, requirement)) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (!isNonEmptyString(result.metricName))
|
|
349
|
+
return false;
|
|
350
|
+
if (!hasSolverRunMetadata(result.candidateSolver))
|
|
351
|
+
return false;
|
|
352
|
+
if (referenceRequiresSolverRunMetadata(reference) && !hasSolverRunMetadata(result.referenceSolver))
|
|
353
|
+
return false;
|
|
354
|
+
if (result.referenceSolver != null && !hasSolverRunMetadata(result.referenceSolver))
|
|
355
|
+
return false;
|
|
356
|
+
if (!Number.isFinite(result.actual) || !Number.isFinite(result.expected))
|
|
357
|
+
return false;
|
|
358
|
+
if (!hasValidSha256(result.evidenceHashSha256) || !hasValidSha256(result.resultHashSha256))
|
|
359
|
+
return false;
|
|
360
|
+
if (result.quantity !== requirement.quantity || result.unit !== requirement.unit)
|
|
361
|
+
return false;
|
|
362
|
+
if (result.toleranceType !== requirement.toleranceType || result.tolerance > requirement.tolerance) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
const tolerance = evaluateFemTolerance(requirement.quantity, result.actual, result.expected, requirement.toleranceType === 'relative' ? 0 : requirement.tolerance, requirement.toleranceType === 'absolute'
|
|
366
|
+
? { unit: requirement.unit }
|
|
367
|
+
: { relativeTolerance: requirement.tolerance, unit: requirement.unit });
|
|
368
|
+
return tolerance.accepted;
|
|
369
|
+
}
|
|
226
370
|
if (references.length === 0) {
|
|
227
371
|
blockerCodes.push('external-benchmark-reference-corpus-missing');
|
|
228
372
|
}
|
|
@@ -321,6 +465,12 @@ export function buildFemExternalBenchmarkAcceptanceContract(options = {}) {
|
|
|
321
465
|
if (!isNonEmptyString(result.caseId)) {
|
|
322
466
|
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.case-id-missing`);
|
|
323
467
|
}
|
|
468
|
+
if (result.comparisonKind !== 'scalar' && result.comparisonKind !== 'series-summary') {
|
|
469
|
+
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.comparison-kind-invalid`);
|
|
470
|
+
}
|
|
471
|
+
if (!isNonEmptyString(result.metricName)) {
|
|
472
|
+
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.metric-name-missing`);
|
|
473
|
+
}
|
|
324
474
|
if (!isNonEmptyString(result.quantity)) {
|
|
325
475
|
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.quantity-missing`);
|
|
326
476
|
}
|
|
@@ -341,7 +491,32 @@ export function buildFemExternalBenchmarkAcceptanceContract(options = {}) {
|
|
|
341
491
|
if (!hasValidSha256(result.evidenceHashSha256)) {
|
|
342
492
|
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.evidence-hash-missing`);
|
|
343
493
|
}
|
|
494
|
+
if (!hasValidSha256(result.resultHashSha256)) {
|
|
495
|
+
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.result-hash-missing`);
|
|
496
|
+
}
|
|
497
|
+
if (!hasSolverRunMetadata(result.candidateSolver)) {
|
|
498
|
+
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.candidate-solver-metadata-missing`);
|
|
499
|
+
}
|
|
500
|
+
const reference = referenceById.get(result.referenceId);
|
|
501
|
+
if (referenceRequiresSolverRunMetadata(reference) && !hasSolverRunMetadata(result.referenceSolver)) {
|
|
502
|
+
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.reference-solver-metadata-missing`);
|
|
503
|
+
}
|
|
504
|
+
else if (result.referenceSolver != null && !hasSolverRunMetadata(result.referenceSolver)) {
|
|
505
|
+
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.reference-solver-metadata-invalid`);
|
|
506
|
+
}
|
|
507
|
+
if (result.comparisonKind === 'series-summary' && !hasValidSeriesSummary(result.seriesSummary)) {
|
|
508
|
+
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.series-summary-invalid`);
|
|
509
|
+
}
|
|
510
|
+
else if (result.seriesSummary != null && !hasValidSeriesSummary(result.seriesSummary)) {
|
|
511
|
+
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.series-summary-invalid`);
|
|
512
|
+
}
|
|
344
513
|
const requirement = quantityById.get(result.quantityRequirementId);
|
|
514
|
+
if (requirement &&
|
|
515
|
+
result.comparisonKind === 'series-summary' &&
|
|
516
|
+
hasValidSeriesSummary(result.seriesSummary) &&
|
|
517
|
+
!seriesSummarySatisfiesRequirementTolerance(result.seriesSummary, requirement)) {
|
|
518
|
+
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.series-tolerance-exceeded`);
|
|
519
|
+
}
|
|
345
520
|
if (requirement) {
|
|
346
521
|
if (result.quantity !== requirement.quantity) {
|
|
347
522
|
blockerCodes.push(`external-benchmark.comparison-results.${resultCode}.quantity-mismatch`);
|
|
@@ -371,37 +546,60 @@ export function buildFemExternalBenchmarkAcceptanceContract(options = {}) {
|
|
|
371
546
|
}
|
|
372
547
|
}
|
|
373
548
|
}
|
|
549
|
+
const acceptedComparisons = comparisonResults
|
|
550
|
+
.map((result) => ({
|
|
551
|
+
result,
|
|
552
|
+
requirement: quantityById.get(result.quantityRequirementId),
|
|
553
|
+
reference: referenceById.get(result.referenceId),
|
|
554
|
+
}))
|
|
555
|
+
.filter((item) => item.requirement != null &&
|
|
556
|
+
item.reference != null &&
|
|
557
|
+
comparisonIsAcceptedForRequirement(item.result, item.requirement, item.reference));
|
|
558
|
+
const partiallyCoveredRequiredQuantityIds = [];
|
|
559
|
+
const fullyCoveredRequiredQuantityIds = [];
|
|
374
560
|
for (const requirement of requiredQuantities) {
|
|
375
561
|
if (!isNonEmptyString(requirement.id))
|
|
376
562
|
continue;
|
|
563
|
+
const acceptedSourceTypesForRequirement = new Set(acceptedComparisons
|
|
564
|
+
.filter((item) => item.requirement.id === requirement.id)
|
|
565
|
+
.map((item) => item.reference.sourceType));
|
|
566
|
+
if (acceptedSourceTypesForRequirement.size > 0) {
|
|
567
|
+
partiallyCoveredRequiredQuantityIds.push(requirement.id);
|
|
568
|
+
}
|
|
569
|
+
if (requirement.requiredReferenceSourceTypes.length > 0 &&
|
|
570
|
+
requirement.requiredReferenceSourceTypes.every((sourceType) => acceptedSourceTypesForRequirement.has(sourceType))) {
|
|
571
|
+
fullyCoveredRequiredQuantityIds.push(requirement.id);
|
|
572
|
+
}
|
|
377
573
|
for (const sourceType of requirement.requiredReferenceSourceTypes) {
|
|
378
|
-
const hasAcceptedComparison =
|
|
379
|
-
|
|
380
|
-
if (!reference || reference.sourceType !== sourceType)
|
|
381
|
-
return false;
|
|
382
|
-
if (result.quantityRequirementId !== requirement.id || !result.accepted)
|
|
383
|
-
return false;
|
|
384
|
-
if (!Number.isFinite(result.actual) || !Number.isFinite(result.expected))
|
|
385
|
-
return false;
|
|
386
|
-
if (!hasValidSha256(result.evidenceHashSha256))
|
|
387
|
-
return false;
|
|
388
|
-
if (result.quantity !== requirement.quantity || result.unit !== requirement.unit)
|
|
389
|
-
return false;
|
|
390
|
-
if (result.toleranceType !== requirement.toleranceType || result.tolerance > requirement.tolerance) {
|
|
391
|
-
return false;
|
|
392
|
-
}
|
|
393
|
-
const tolerance = evaluateFemTolerance(requirement.quantity, result.actual, result.expected, requirement.toleranceType === 'relative' ? 0 : requirement.tolerance, requirement.toleranceType === 'absolute'
|
|
394
|
-
? { unit: requirement.unit }
|
|
395
|
-
: { relativeTolerance: requirement.tolerance, unit: requirement.unit });
|
|
396
|
-
return tolerance.accepted;
|
|
397
|
-
});
|
|
574
|
+
const hasAcceptedComparison = acceptedComparisons.some((item) => item.requirement.id === requirement.id &&
|
|
575
|
+
item.reference.sourceType === sourceType);
|
|
398
576
|
if (!hasAcceptedComparison) {
|
|
399
577
|
blockerCodes.push(`external-benchmark.required-quantities.${requirement.id}.accepted-comparison-missing.${sourceType}`);
|
|
400
578
|
}
|
|
401
579
|
}
|
|
402
580
|
}
|
|
581
|
+
const acceptedSourceTypes = new Set(acceptedComparisons.map((item) => item.reference.sourceType));
|
|
582
|
+
const coverageSummary = {
|
|
583
|
+
schemaVersion: 'fem-external-benchmark-coverage.v1',
|
|
584
|
+
acceptedComparisonCount: acceptedComparisons.length,
|
|
585
|
+
acceptedPublishedComparisonCount: acceptedComparisons
|
|
586
|
+
.filter((item) => item.reference.sourceType === 'published-source').length,
|
|
587
|
+
acceptedCommercialComparisonCount: acceptedComparisons
|
|
588
|
+
.filter((item) => item.reference.sourceType === 'commercial-solver').length,
|
|
589
|
+
acceptedOpenSourceComparisonCount: acceptedComparisons
|
|
590
|
+
.filter((item) => item.reference.sourceType === 'open-source-solver').length,
|
|
591
|
+
requiredQuantityCount: requiredQuantities.length,
|
|
592
|
+
partiallyCoveredRequiredQuantityIds,
|
|
593
|
+
fullyCoveredRequiredQuantityIds,
|
|
594
|
+
missingRequiredSourceTypes: REQUIRED_EXTERNAL_BENCHMARK_SOURCE_TYPES
|
|
595
|
+
.filter((sourceType) => !acceptedSourceTypes.has(sourceType)),
|
|
596
|
+
};
|
|
597
|
+
if (coverageSummary.fullyCoveredRequiredQuantityIds.length < requiredQuantities.length) {
|
|
598
|
+
blockerCodes.push('external-benchmark-comparison-results-missing');
|
|
599
|
+
}
|
|
403
600
|
const uniqueBlockerCodes = [...new Set(blockerCodes)];
|
|
404
601
|
const productionReadinessBlocked = uniqueBlockerCodes.length > 0;
|
|
602
|
+
const hasPartialAcceptedEvidence = coverageSummary.acceptedComparisonCount > 0;
|
|
405
603
|
return {
|
|
406
604
|
schemaVersion: 'fem-external-benchmark-acceptance.v2',
|
|
407
605
|
status: productionReadinessBlocked ? 'blocked' : 'accepted-comparisons-ready',
|
|
@@ -410,12 +608,226 @@ export function buildFemExternalBenchmarkAcceptanceContract(options = {}) {
|
|
|
410
608
|
references,
|
|
411
609
|
requiredQuantities,
|
|
412
610
|
comparisonResults,
|
|
611
|
+
coverageSummary,
|
|
413
612
|
blockerCodes: uniqueBlockerCodes,
|
|
414
613
|
acceptanceStatement: productionReadinessBlocked
|
|
415
|
-
?
|
|
614
|
+
? hasPartialAcceptedEvidence
|
|
615
|
+
? `Partial external benchmark evidence is present (${coverageSummary.acceptedComparisonCount} accepted comparison summaries), but production readiness remains blocked until published and commercial solver references and accepted comparison results cover every required FEM quantity.`
|
|
616
|
+
: 'External benchmark acceptance is incomplete; production readiness remains blocked until source citations, commercial solver references, and accepted comparison results cover every required FEM quantity.'
|
|
416
617
|
: 'External benchmark references and comparison results cover the required FEM quantities; this still does not approve production design without solver-route and reviewer-workflow acceptance.',
|
|
417
618
|
};
|
|
418
619
|
}
|
|
620
|
+
function femSeriesStatistics(values) {
|
|
621
|
+
if (values.length === 0)
|
|
622
|
+
throw new Error('FEM benchmark series must include at least one value.');
|
|
623
|
+
const sum = values.reduce((total, value) => total + value, 0);
|
|
624
|
+
return {
|
|
625
|
+
min: round(Math.min(...values), 10),
|
|
626
|
+
max: round(Math.max(...values), 10),
|
|
627
|
+
final: round(values[values.length - 1], 10),
|
|
628
|
+
mean: round(sum / values.length, 10),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function buildFemExternalBenchmarkSeriesSummary(input) {
|
|
632
|
+
if (input.points.length === 0) {
|
|
633
|
+
throw new Error('FEM external benchmark comparison series must include at least one point.');
|
|
634
|
+
}
|
|
635
|
+
const points = input.points.map((point) => ({
|
|
636
|
+
x: round(point.x, 10),
|
|
637
|
+
actual: round(point.actual, 10),
|
|
638
|
+
expected: round(point.expected, 10),
|
|
639
|
+
}));
|
|
640
|
+
const actual = points.map((point) => point.actual);
|
|
641
|
+
const expected = points.map((point) => point.expected);
|
|
642
|
+
const errors = points.map((point) => Math.abs(point.actual - point.expected));
|
|
643
|
+
const relativeErrors = points.map((point) => Math.abs(point.actual - point.expected) / Math.max(Math.abs(point.expected), 1e-12));
|
|
644
|
+
const rmsError = Math.sqrt(errors.reduce((sum, error) => sum + error * error, 0) / errors.length);
|
|
645
|
+
return {
|
|
646
|
+
xQuantity: input.xQuantity,
|
|
647
|
+
xUnit: input.xUnit,
|
|
648
|
+
yQuantity: input.yQuantity,
|
|
649
|
+
yUnit: input.yUnit,
|
|
650
|
+
pointCount: points.length,
|
|
651
|
+
actual: femSeriesStatistics(actual),
|
|
652
|
+
expected: femSeriesStatistics(expected),
|
|
653
|
+
maxAbsoluteError: round(Math.max(...errors), 10),
|
|
654
|
+
maxRelativeError: round(Math.max(...relativeErrors), 10),
|
|
655
|
+
rmsError: round(rmsError, 10),
|
|
656
|
+
seriesHashSha256: hashFemBenchmarkPayload({
|
|
657
|
+
schemaVersion: 'fem-external-benchmark-series.v1',
|
|
658
|
+
xQuantity: input.xQuantity,
|
|
659
|
+
xUnit: input.xUnit,
|
|
660
|
+
yQuantity: input.yQuantity,
|
|
661
|
+
yUnit: input.yUnit,
|
|
662
|
+
points,
|
|
663
|
+
}),
|
|
664
|
+
...(input.notes ? { notes: [...input.notes] } : {}),
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function externalBenchmarkFinalAccepted(input) {
|
|
668
|
+
return evaluateFemTolerance(input.quantity, input.actual, input.expected, input.toleranceType === 'relative' ? 0 : input.tolerance, input.toleranceType === 'absolute'
|
|
669
|
+
? { unit: input.unit }
|
|
670
|
+
: { relativeTolerance: input.tolerance, unit: input.unit }).accepted;
|
|
671
|
+
}
|
|
672
|
+
function buildDefaultPublishedExternalBenchmarkComparisonResults(input) {
|
|
673
|
+
const consolidationRequirement = DEFAULT_EXTERNAL_BENCHMARK_REQUIRED_QUANTITIES
|
|
674
|
+
.find((requirement) => requirement.id === 'consolidation-settlement-time-curve');
|
|
675
|
+
const biotRequirement = DEFAULT_EXTERNAL_BENCHMARK_REQUIRED_QUANTITIES
|
|
676
|
+
.find((requirement) => requirement.id === 'biot-pore-pressure-dissipation');
|
|
677
|
+
if (!consolidationRequirement || !biotRequirement) {
|
|
678
|
+
throw new Error('Default FEM external benchmark quantity requirements are missing.');
|
|
679
|
+
}
|
|
680
|
+
const consolidationPoints = input.consolidation.steps.map((step) => ({
|
|
681
|
+
x: step.timeFactor,
|
|
682
|
+
actual: step.degreeOfConsolidation,
|
|
683
|
+
expected: step.referenceDegreeOfConsolidation,
|
|
684
|
+
}));
|
|
685
|
+
const consolidationSeries = buildFemExternalBenchmarkSeriesSummary({
|
|
686
|
+
xQuantity: 'time factor',
|
|
687
|
+
xUnit: 'Tv',
|
|
688
|
+
yQuantity: 'average degree of consolidation',
|
|
689
|
+
yUnit: 'ratio',
|
|
690
|
+
points: consolidationPoints,
|
|
691
|
+
notes: [
|
|
692
|
+
'Backward-Euler drainage-column series compared with the Terzaghi average-consolidation Fourier-series reference.',
|
|
693
|
+
],
|
|
694
|
+
});
|
|
695
|
+
const consolidationActual = input.consolidation.finalStep.degreeOfConsolidation;
|
|
696
|
+
const consolidationExpected = input.consolidation.finalStep.referenceDegreeOfConsolidation;
|
|
697
|
+
const consolidationEvidencePayload = {
|
|
698
|
+
schemaVersion: 'fem-external-benchmark-evidence.v1',
|
|
699
|
+
caseId: 'terzaghi-1d-backward-euler-tv-0-197',
|
|
700
|
+
sourceId: 'terzaghi-1943-theoretical-soil-mechanics',
|
|
701
|
+
method: input.consolidation.method,
|
|
702
|
+
drainagePathM: input.consolidation.drainagePathM,
|
|
703
|
+
nodeCount: input.consolidation.nodeCount,
|
|
704
|
+
finalTimeFactor: input.consolidation.finalStep.timeFactor,
|
|
705
|
+
seriesHashSha256: consolidationSeries.seriesHashSha256,
|
|
706
|
+
};
|
|
707
|
+
const consolidationResultPayload = {
|
|
708
|
+
actual: consolidationActual,
|
|
709
|
+
expected: consolidationExpected,
|
|
710
|
+
maxAbsoluteError: consolidationSeries.maxAbsoluteError,
|
|
711
|
+
maxRelativeError: consolidationSeries.maxRelativeError,
|
|
712
|
+
resultSeriesHashSha256: consolidationSeries.seriesHashSha256,
|
|
713
|
+
};
|
|
714
|
+
const consolidationAccepted = externalBenchmarkFinalAccepted({
|
|
715
|
+
actual: consolidationActual,
|
|
716
|
+
expected: consolidationExpected,
|
|
717
|
+
tolerance: consolidationRequirement.tolerance,
|
|
718
|
+
toleranceType: consolidationRequirement.toleranceType,
|
|
719
|
+
unit: consolidationRequirement.unit,
|
|
720
|
+
quantity: consolidationRequirement.quantity,
|
|
721
|
+
}) && seriesSummarySatisfiesRequirementTolerance(consolidationSeries, consolidationRequirement);
|
|
722
|
+
const biotCvPerSecond = input.biotTerzaghiHydraulicConductivityMPerS / input.biotTerzaghiSpecificStorage1PerM;
|
|
723
|
+
const biotPoints = input.biotTerzaghi.timeSteps.map((step) => {
|
|
724
|
+
const timeFactor = step.timeSeconds * biotCvPerSecond;
|
|
725
|
+
return {
|
|
726
|
+
x: timeFactor,
|
|
727
|
+
actual: step.pressureDiagnostics.averagePorePressureKpa,
|
|
728
|
+
expected: input.biotTerzaghiInitialPressureKpa * (1 - terzaghiAverageConsolidation(timeFactor)),
|
|
729
|
+
};
|
|
730
|
+
});
|
|
731
|
+
const biotSeries = buildFemExternalBenchmarkSeriesSummary({
|
|
732
|
+
xQuantity: 'time factor',
|
|
733
|
+
xUnit: 'Tv',
|
|
734
|
+
yQuantity: 'average excess pore pressure',
|
|
735
|
+
yUnit: 'kPa',
|
|
736
|
+
points: biotPoints,
|
|
737
|
+
notes: [
|
|
738
|
+
'Alpha-zero Quad4 Biot u-p pressure-diffusion series compared with the Terzaghi drained-column analytical curve.',
|
|
739
|
+
],
|
|
740
|
+
});
|
|
741
|
+
const biotActual = input.biotTerzaghi.pressureDiagnostics.averagePorePressureKpa;
|
|
742
|
+
const biotExpected = biotPoints[biotPoints.length - 1].expected;
|
|
743
|
+
const biotEvidencePayload = {
|
|
744
|
+
schemaVersion: 'fem-external-benchmark-evidence.v1',
|
|
745
|
+
caseId: 'quad4-biot-alpha-zero-terzaghi-top-drained-tv-0-197',
|
|
746
|
+
sourceId: 'biot-1941-three-dimensional-consolidation',
|
|
747
|
+
method: input.biotTerzaghi.method,
|
|
748
|
+
pressureKind: input.biotTerzaghi.numericalContract.pressureKind,
|
|
749
|
+
pressureUnit: input.biotTerzaghi.numericalContract.pressureUnit,
|
|
750
|
+
timeStepCount: input.biotTerzaghi.timeSteps.length,
|
|
751
|
+
transientAcceptance: input.biotTerzaghi.transientAcceptance,
|
|
752
|
+
finalTimeFactor: round(biotPoints[biotPoints.length - 1].x, 10),
|
|
753
|
+
seriesHashSha256: biotSeries.seriesHashSha256,
|
|
754
|
+
};
|
|
755
|
+
const biotResultPayload = {
|
|
756
|
+
actual: biotActual,
|
|
757
|
+
expected: biotExpected,
|
|
758
|
+
maxAbsoluteError: biotSeries.maxAbsoluteError,
|
|
759
|
+
maxRelativeError: biotSeries.maxRelativeError,
|
|
760
|
+
resultSeriesHashSha256: biotSeries.seriesHashSha256,
|
|
761
|
+
};
|
|
762
|
+
const biotAccepted = externalBenchmarkFinalAccepted({
|
|
763
|
+
actual: biotActual,
|
|
764
|
+
expected: biotExpected,
|
|
765
|
+
tolerance: biotRequirement.tolerance,
|
|
766
|
+
toleranceType: biotRequirement.toleranceType,
|
|
767
|
+
unit: biotRequirement.unit,
|
|
768
|
+
quantity: biotRequirement.quantity,
|
|
769
|
+
}) && seriesSummarySatisfiesRequirementTolerance(biotSeries, biotRequirement);
|
|
770
|
+
return [
|
|
771
|
+
{
|
|
772
|
+
id: 'published-terzaghi-1d-consolidation-tv-0-197',
|
|
773
|
+
quantityRequirementId: consolidationRequirement.id,
|
|
774
|
+
referenceId: 'terzaghi-1943-theoretical-soil-mechanics',
|
|
775
|
+
caseId: 'terzaghi-1d-backward-euler-tv-0-197',
|
|
776
|
+
comparisonKind: 'series-summary',
|
|
777
|
+
metricName: 'averageDegreeOfConsolidationTimeCurve',
|
|
778
|
+
quantity: consolidationRequirement.quantity,
|
|
779
|
+
unit: consolidationRequirement.unit,
|
|
780
|
+
actual: round(consolidationActual, 10),
|
|
781
|
+
expected: round(consolidationExpected, 10),
|
|
782
|
+
tolerance: consolidationRequirement.tolerance,
|
|
783
|
+
toleranceType: consolidationRequirement.toleranceType,
|
|
784
|
+
accepted: consolidationAccepted,
|
|
785
|
+
candidateSolver: {
|
|
786
|
+
name: 'geotechCLI FEM evidence suite',
|
|
787
|
+
version: 'strong-beta',
|
|
788
|
+
solverType: 'geotechcli-kernel',
|
|
789
|
+
analysisProcedure: 'backward-Euler 1D Terzaghi consolidation time stepper',
|
|
790
|
+
elementType: '1D drainage-column finite-difference grid',
|
|
791
|
+
runId: 'terzaghi-1d-backward-euler-tv-0-197',
|
|
792
|
+
},
|
|
793
|
+
evidenceHashSha256: hashFemBenchmarkPayload(consolidationEvidencePayload),
|
|
794
|
+
resultHashSha256: hashFemBenchmarkPayload(consolidationResultPayload),
|
|
795
|
+
seriesSummary: consolidationSeries,
|
|
796
|
+
notes: [
|
|
797
|
+
'Generated published-source comparison record only; commercial solver comparison remains missing.',
|
|
798
|
+
],
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
id: 'published-biot-alpha-zero-terzaghi-pressure-dissipation-tv-0-197',
|
|
802
|
+
quantityRequirementId: biotRequirement.id,
|
|
803
|
+
referenceId: 'biot-1941-three-dimensional-consolidation',
|
|
804
|
+
caseId: 'quad4-biot-alpha-zero-terzaghi-top-drained-tv-0-197',
|
|
805
|
+
comparisonKind: 'series-summary',
|
|
806
|
+
metricName: 'averageExcessPorePressureDissipationTimeCurve',
|
|
807
|
+
quantity: biotRequirement.quantity,
|
|
808
|
+
unit: biotRequirement.unit,
|
|
809
|
+
actual: round(biotActual, 10),
|
|
810
|
+
expected: round(biotExpected, 10),
|
|
811
|
+
tolerance: biotRequirement.tolerance,
|
|
812
|
+
toleranceType: biotRequirement.toleranceType,
|
|
813
|
+
accepted: biotAccepted,
|
|
814
|
+
candidateSolver: {
|
|
815
|
+
name: 'geotechCLI FEM evidence suite',
|
|
816
|
+
version: 'strong-beta',
|
|
817
|
+
solverType: 'geotechcli-kernel',
|
|
818
|
+
analysisProcedure: 'linear-elastic Quad4 Biot u-p backward-Euler pressure diffusion',
|
|
819
|
+
elementType: 'Quad4 plane-strain u-p evidence mesh',
|
|
820
|
+
runId: 'quad4-biot-alpha-zero-terzaghi-top-drained-tv-0-197',
|
|
821
|
+
},
|
|
822
|
+
evidenceHashSha256: hashFemBenchmarkPayload(biotEvidencePayload),
|
|
823
|
+
resultHashSha256: hashFemBenchmarkPayload(biotResultPayload),
|
|
824
|
+
seriesSummary: biotSeries,
|
|
825
|
+
notes: [
|
|
826
|
+
'Generated published-source comparison record for an alpha-zero Biot pressure-diffusion specialization; commercial solver comparison remains missing.',
|
|
827
|
+
],
|
|
828
|
+
},
|
|
829
|
+
];
|
|
830
|
+
}
|
|
419
831
|
export function evaluateFemTolerance(quantity, actual, expected, absoluteTolerance, options = {}) {
|
|
420
832
|
const error = Math.abs(actual - expected);
|
|
421
833
|
const relativeError = Math.abs(expected) > 0 ? error / Math.abs(expected) : error;
|
|
@@ -1638,6 +2050,36 @@ export function runFemEngineeringEvidenceSuite(policy = DEFAULT_FEM_CONVERGENCE_
|
|
|
1638
2050
|
biotTerzaghi.transientAcceptance.monotonicMaxPressureEnvelope
|
|
1639
2051
|
? 1
|
|
1640
2052
|
: 0, 1, 0, 'Top-drained alpha-zero Biot Terzaghi fixture must pass the stricter drained-dissipation transient acceptance gate.'));
|
|
2053
|
+
const biotPressureReplay = runPlaneStrainDruckerPragerBiotPressureReplay({
|
|
2054
|
+
mechanicalModel: {
|
|
2055
|
+
schemaVersion: 'fem-plane-strain-model.v1',
|
|
2056
|
+
nodes: biotTerzaghiMesh.nodes,
|
|
2057
|
+
elements: biotTerzaghiMesh.elements,
|
|
2058
|
+
materials: [{
|
|
2059
|
+
id: 'soil',
|
|
2060
|
+
elasticModulusKpa: 30_000,
|
|
2061
|
+
poissonRatio: 0.3,
|
|
2062
|
+
frictionAngleDeg: 35,
|
|
2063
|
+
cohesionKpa: 10_000,
|
|
2064
|
+
dilationAngleDeg: 0,
|
|
2065
|
+
biotCoefficient: 0.8,
|
|
2066
|
+
}],
|
|
2067
|
+
boundaryConditions: biotTerzaghiBottomNodes.flatMap((node) => [
|
|
2068
|
+
{ nodeId: node.id, dof: 'ux' },
|
|
2069
|
+
{ nodeId: node.id, dof: 'uy' },
|
|
2070
|
+
]),
|
|
2071
|
+
policy,
|
|
2072
|
+
},
|
|
2073
|
+
biotResult: biotTerzaghi,
|
|
2074
|
+
solverOptions: { loadStepFractions: [1] },
|
|
2075
|
+
});
|
|
2076
|
+
benchmarks.push(benchmark('quad4-plane-strain-dp-sequential-biot-pressure-replay-audit', 'seepage-pore-pressure-coupling', 'internal-balance', 'sequentialPressureReplayAccepted', biotPressureReplay.converged &&
|
|
2077
|
+
biotPressureReplay.pressureReplayAudit.mode === 'sequential-one-way-biot-pressure-replay' &&
|
|
2078
|
+
biotPressureReplay.pressureReplayAudit.sourceTransientAccepted &&
|
|
2079
|
+
biotPressureReplay.hydroMechanicalCoupling?.porePressureDofCount === 0 &&
|
|
2080
|
+
biotPressureReplay.hydroMechanicalCoupling.maxAbsAppliedEffectiveStressReductionKpa > 0
|
|
2081
|
+
? 1
|
|
2082
|
+
: 0, 1, 0, 'Sequential pressure replay must feed an accepted linear Biot u-p pressure frame into the Drucker-Prager effective-stress residual while auditing that no pore-pressure DOFs or monolithic Biot-plastic tangent are assembled.'));
|
|
1641
2083
|
const coupling = runHydroMechanicalCoupling1D({
|
|
1642
2084
|
totalVerticalStressKpa: 200,
|
|
1643
2085
|
porePressureBeforeKpa: 80,
|
|
@@ -1776,7 +2218,15 @@ export function runFemEngineeringEvidenceSuite(policy = DEFAULT_FEM_CONVERGENCE_
|
|
|
1776
2218
|
.filter((item) => item.status === 'accepted')
|
|
1777
2219
|
.map((item) => item.feature))];
|
|
1778
2220
|
const status = benchmarks.every((item) => item.status === 'accepted') ? 'kernel-verified' : 'blocked';
|
|
1779
|
-
const externalBenchmarkAcceptance = buildFemExternalBenchmarkAcceptanceContract(
|
|
2221
|
+
const externalBenchmarkAcceptance = buildFemExternalBenchmarkAcceptanceContract({
|
|
2222
|
+
comparisonResults: buildDefaultPublishedExternalBenchmarkComparisonResults({
|
|
2223
|
+
consolidation,
|
|
2224
|
+
biotTerzaghi,
|
|
2225
|
+
biotTerzaghiInitialPressureKpa,
|
|
2226
|
+
biotTerzaghiHydraulicConductivityMPerS,
|
|
2227
|
+
biotTerzaghiSpecificStorage1PerM,
|
|
2228
|
+
}),
|
|
2229
|
+
});
|
|
1780
2230
|
return {
|
|
1781
2231
|
schemaVersion: 'fem-engineering-evidence.v1',
|
|
1782
2232
|
status,
|