@geotechcli/core 0.4.98 → 0.4.100
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 +1 -1
- package/dist/fem/engineering-evidence.d.ts.map +1 -1
- package/dist/fem/engineering-evidence.js +145 -3
- package/dist/fem/engineering-evidence.js.map +1 -1
- package/dist/fem/index.d.ts +1 -1
- package/dist/fem/index.d.ts.map +1 -1
- package/dist/fem/index.js +1 -1
- package/dist/fem/index.js.map +1 -1
- package/dist/fem/plane-strain-assembly.d.ts +123 -0
- package/dist/fem/plane-strain-assembly.d.ts.map +1 -1
- package/dist/fem/plane-strain-assembly.js +709 -0
- package/dist/fem/plane-strain-assembly.js.map +1 -1
- package/dist/fem/production-readiness.js +4 -4
- package/dist/fem/production-readiness.js.map +1 -1
- package/dist/meta/metadata.json +1 -1
- package/package.json +1 -1
|
@@ -76,6 +76,128 @@ function planeStrainD(material) {
|
|
|
76
76
|
[0, 0, factor * (1 - 2 * nu) / 2],
|
|
77
77
|
];
|
|
78
78
|
}
|
|
79
|
+
function planeStrainSigmaZ(material, strain) {
|
|
80
|
+
const e = material.elasticModulusKpa;
|
|
81
|
+
const nu = material.poissonRatio;
|
|
82
|
+
const factor = e / ((1 + nu) * (1 - 2 * nu));
|
|
83
|
+
return factor * nu * (strain[0] + strain[1]);
|
|
84
|
+
}
|
|
85
|
+
function elasticModuli(material) {
|
|
86
|
+
return {
|
|
87
|
+
bulkModulusKpa: material.elasticModulusKpa / (3 * (1 - 2 * material.poissonRatio)),
|
|
88
|
+
shearModulusKpa: material.elasticModulusKpa / (2 * (1 + material.poissonRatio)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function degToRad(degrees) {
|
|
92
|
+
return (degrees * Math.PI) / 180;
|
|
93
|
+
}
|
|
94
|
+
function druckerPragerRhoFromAngle(angleDeg) {
|
|
95
|
+
if (!Number.isFinite(angleDeg) || angleDeg < 0 || angleDeg >= 50) {
|
|
96
|
+
throw new Error('friction and dilation angles must be finite and between 0 and 50 degrees.');
|
|
97
|
+
}
|
|
98
|
+
const angle = degToRad(angleDeg);
|
|
99
|
+
const sinAngle = Math.sin(angle);
|
|
100
|
+
if (sinAngle === 0)
|
|
101
|
+
return 0;
|
|
102
|
+
return (2 * Math.SQRT2 * sinAngle) / (Math.sqrt(3) * (3 - sinAngle));
|
|
103
|
+
}
|
|
104
|
+
function druckerPragerParameters(material) {
|
|
105
|
+
const frictionAngleDeg = material.frictionAngleDeg ?? 30;
|
|
106
|
+
const cohesionKpa = material.cohesionKpa ?? 0;
|
|
107
|
+
const dilationAngleDeg = material.dilationAngleDeg ?? frictionAngleDeg;
|
|
108
|
+
if (!Number.isFinite(frictionAngleDeg) || frictionAngleDeg <= 0 || frictionAngleDeg >= 50) {
|
|
109
|
+
throw new Error(`material ${material.id} frictionAngleDeg must be finite and between 0 and 50 degrees.`);
|
|
110
|
+
}
|
|
111
|
+
assertFiniteNonNegative(cohesionKpa, `material ${material.id} cohesionKpa`);
|
|
112
|
+
assertFiniteNonNegative(material.hardeningModulusKpa ?? 0, `material ${material.id} hardeningModulusKpa`);
|
|
113
|
+
const rho = druckerPragerRhoFromAngle(frictionAngleDeg);
|
|
114
|
+
const rhoBar = Math.min(rho, druckerPragerRhoFromAngle(dilationAngleDeg));
|
|
115
|
+
const phi = degToRad(frictionAngleDeg);
|
|
116
|
+
const sinPhi = Math.sin(phi);
|
|
117
|
+
const cohesionContributionToQ = (2 * cohesionKpa * Math.cos(phi)) / (1 - sinPhi);
|
|
118
|
+
const deviatoricNormFactor = Math.sqrt(2 / 3);
|
|
119
|
+
return {
|
|
120
|
+
rho,
|
|
121
|
+
rhoBar,
|
|
122
|
+
compressionInterceptKpa: cohesionContributionToQ * (deviatoricNormFactor - rho),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function principalCompressionFromPlaneStress(stress, sigmaZKpa) {
|
|
126
|
+
const a = -stress[0];
|
|
127
|
+
const c = -stress[1];
|
|
128
|
+
const b = -stress[2];
|
|
129
|
+
const mean = (a + c) / 2;
|
|
130
|
+
const radius = Math.hypot((a - c) / 2, b);
|
|
131
|
+
const angleRad = 0.5 * Math.atan2(2 * b, a - c);
|
|
132
|
+
return {
|
|
133
|
+
values: [mean + radius, mean - radius, -sigmaZKpa],
|
|
134
|
+
angleRad,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function planeStressFromPrincipalCompression(values, angleRad) {
|
|
138
|
+
const [p1, p2, p3] = values;
|
|
139
|
+
const c = Math.cos(angleRad);
|
|
140
|
+
const s = Math.sin(angleRad);
|
|
141
|
+
const compressionX = p1 * c * c + p2 * s * s;
|
|
142
|
+
const compressionY = p1 * s * s + p2 * c * c;
|
|
143
|
+
const compressionXY = (p1 - p2) * s * c;
|
|
144
|
+
return {
|
|
145
|
+
stress: [-compressionX, -compressionY, -compressionXY],
|
|
146
|
+
sigmaZKpa: -p3,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function deviatoricNorm(values) {
|
|
150
|
+
const mean = (values[0] + values[1] + values[2]) / 3;
|
|
151
|
+
return Math.hypot(values[0] - mean, values[1] - mean, values[2] - mean);
|
|
152
|
+
}
|
|
153
|
+
function projectDruckerPragerStress(input) {
|
|
154
|
+
const { material, strain } = input;
|
|
155
|
+
const params = druckerPragerParameters(material);
|
|
156
|
+
const principal = principalCompressionFromPlaneStress(input.stress, input.sigmaZKpa);
|
|
157
|
+
const principalStress = principal.values;
|
|
158
|
+
const trace = principalStress[0] + principalStress[1] + principalStress[2];
|
|
159
|
+
const mean = trace / 3;
|
|
160
|
+
const q = deviatoricNorm(principalStress);
|
|
161
|
+
const intercept = params.compressionInterceptKpa;
|
|
162
|
+
const yieldValue = q - params.rho * trace - intercept;
|
|
163
|
+
const yieldScale = Math.max(q, Math.abs(params.rho * trace), Math.abs(intercept), 1);
|
|
164
|
+
if (yieldValue <= 0 || Math.abs(yieldValue) / yieldScale <= input.policy.residualTolerance) {
|
|
165
|
+
return {
|
|
166
|
+
strain,
|
|
167
|
+
stressKpa: input.stress,
|
|
168
|
+
outOfPlaneStressKpa: input.sigmaZKpa,
|
|
169
|
+
compressionPositivePrincipalStressKpa: principalStress,
|
|
170
|
+
yieldValueKpa: yieldValue,
|
|
171
|
+
yieldResidualRatio: Math.max(0, yieldValue) / yieldScale,
|
|
172
|
+
plasticMultiplier: 0,
|
|
173
|
+
equivalentPlasticStrain: 0,
|
|
174
|
+
state: 'elastic',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const targetQ = Math.max(0, params.rho * trace + intercept);
|
|
178
|
+
const scale = q > 0 ? targetQ / q : 0;
|
|
179
|
+
const correctedPrincipal = principalStress.map((value) => mean + (value - mean) * scale);
|
|
180
|
+
const corrected = planeStressFromPrincipalCompression(correctedPrincipal, principal.angleRad);
|
|
181
|
+
const correctedQ = deviatoricNorm(correctedPrincipal);
|
|
182
|
+
const correctedYieldValue = correctedQ - params.rho * trace - intercept;
|
|
183
|
+
const correctedScale = Math.max(correctedQ, Math.abs(params.rho * trace), Math.abs(intercept), 1);
|
|
184
|
+
const moduli = elasticModuli(material);
|
|
185
|
+
const denominator = 2 * moduli.shearModulusKpa +
|
|
186
|
+
9 * moduli.bulkModulusKpa * params.rho * params.rhoBar +
|
|
187
|
+
(material.hardeningModulusKpa ?? 0);
|
|
188
|
+
const plasticMultiplier = Math.max(0, yieldValue / Math.max(denominator, 1e-12));
|
|
189
|
+
return {
|
|
190
|
+
strain,
|
|
191
|
+
stressKpa: corrected.stress,
|
|
192
|
+
outOfPlaneStressKpa: corrected.sigmaZKpa,
|
|
193
|
+
compressionPositivePrincipalStressKpa: correctedPrincipal,
|
|
194
|
+
yieldValueKpa: correctedYieldValue,
|
|
195
|
+
yieldResidualRatio: Math.abs(correctedYieldValue) / correctedScale,
|
|
196
|
+
plasticMultiplier,
|
|
197
|
+
equivalentPlasticStrain: plasticMultiplier,
|
|
198
|
+
state: 'plastic',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
79
201
|
function shapeDerivativesNatural(xi, eta) {
|
|
80
202
|
return [
|
|
81
203
|
[-(1 - eta) / 4, -(1 - xi) / 4],
|
|
@@ -84,6 +206,14 @@ function shapeDerivativesNatural(xi, eta) {
|
|
|
84
206
|
[-(1 + eta) / 4, (1 - xi) / 4],
|
|
85
207
|
];
|
|
86
208
|
}
|
|
209
|
+
function shapeFunctions(xi, eta) {
|
|
210
|
+
return [
|
|
211
|
+
((1 - xi) * (1 - eta)) / 4,
|
|
212
|
+
((1 + xi) * (1 - eta)) / 4,
|
|
213
|
+
((1 + xi) * (1 + eta)) / 4,
|
|
214
|
+
((1 - xi) * (1 + eta)) / 4,
|
|
215
|
+
];
|
|
216
|
+
}
|
|
87
217
|
function solveDenseLinearSystem(matrix, rhs) {
|
|
88
218
|
const n = rhs.length;
|
|
89
219
|
const a = matrix.map((row, index) => [...row, rhs[index]]);
|
|
@@ -173,6 +303,73 @@ function elementMatrices(input) {
|
|
|
173
303
|
}
|
|
174
304
|
return { stiffness: k, areaM2, gauss };
|
|
175
305
|
}
|
|
306
|
+
function validateHydraulicMaterial(material) {
|
|
307
|
+
const kxMPerS = material.hydraulicConductivityXMPerS;
|
|
308
|
+
if (kxMPerS == null) {
|
|
309
|
+
throw new Error(`material ${material.id} hydraulicConductivityXMPerS is required for plane-strain seepage.`);
|
|
310
|
+
}
|
|
311
|
+
assertFinitePositive(kxMPerS, `material ${material.id} hydraulicConductivityXMPerS`);
|
|
312
|
+
const kyMPerS = material.hydraulicConductivityYMPerS ?? kxMPerS;
|
|
313
|
+
assertFinitePositive(kyMPerS, `material ${material.id} hydraulicConductivityYMPerS`);
|
|
314
|
+
const biotCoefficient = material.biotCoefficient ?? 1;
|
|
315
|
+
if (!Number.isFinite(biotCoefficient) || biotCoefficient < 0 || biotCoefficient > 1) {
|
|
316
|
+
throw new Error(`material ${material.id} biotCoefficient must be finite and between 0 and 1.`);
|
|
317
|
+
}
|
|
318
|
+
return { kxMPerS, kyMPerS, biotCoefficient };
|
|
319
|
+
}
|
|
320
|
+
function hydraulicElementMatrices(input) {
|
|
321
|
+
const { nodes, material, thicknessM } = input;
|
|
322
|
+
const { kxMPerS, kyMPerS, biotCoefficient } = validateHydraulicMaterial(material);
|
|
323
|
+
const conductivity = Array.from({ length: 4 }, () => new Array(4).fill(0));
|
|
324
|
+
const gauss = [];
|
|
325
|
+
let areaM2 = 0;
|
|
326
|
+
for (const [xi, eta, weight] of GAUSS_POINTS) {
|
|
327
|
+
const derivatives = shapeDerivativesNatural(xi, eta);
|
|
328
|
+
let j11 = 0;
|
|
329
|
+
let j12 = 0;
|
|
330
|
+
let j21 = 0;
|
|
331
|
+
let j22 = 0;
|
|
332
|
+
for (let i = 0; i < 4; i += 1) {
|
|
333
|
+
j11 += derivatives[i][0] * nodes[i].xM;
|
|
334
|
+
j12 += derivatives[i][0] * nodes[i].yM;
|
|
335
|
+
j21 += derivatives[i][1] * nodes[i].xM;
|
|
336
|
+
j22 += derivatives[i][1] * nodes[i].yM;
|
|
337
|
+
}
|
|
338
|
+
const detJ = j11 * j22 - j12 * j21;
|
|
339
|
+
if (!Number.isFinite(detJ) || detJ <= 0) {
|
|
340
|
+
throw new Error('Plane-strain seepage quad4 element has non-positive Jacobian; check node order and geometry.');
|
|
341
|
+
}
|
|
342
|
+
const invJ = [
|
|
343
|
+
[j22 / detJ, -j12 / detJ],
|
|
344
|
+
[-j21 / detJ, j11 / detJ],
|
|
345
|
+
];
|
|
346
|
+
const dNdx = [];
|
|
347
|
+
const dNdy = [];
|
|
348
|
+
for (let i = 0; i < 4; i += 1) {
|
|
349
|
+
dNdx.push(invJ[0][0] * derivatives[i][0] + invJ[0][1] * derivatives[i][1]);
|
|
350
|
+
dNdy.push(invJ[1][0] * derivatives[i][0] + invJ[1][1] * derivatives[i][1]);
|
|
351
|
+
}
|
|
352
|
+
for (let row = 0; row < 4; row += 1) {
|
|
353
|
+
for (let col = 0; col < 4; col += 1) {
|
|
354
|
+
conductivity[row][col] += (dNdx[row] * kxMPerS * dNdx[col] +
|
|
355
|
+
dNdy[row] * kyMPerS * dNdy[col]) * detJ * weight * thicknessM;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
areaM2 += detJ * weight;
|
|
359
|
+
gauss.push({
|
|
360
|
+
xi,
|
|
361
|
+
eta,
|
|
362
|
+
detJ,
|
|
363
|
+
shape: shapeFunctions(xi, eta),
|
|
364
|
+
dNdx,
|
|
365
|
+
dNdy,
|
|
366
|
+
kxMPerS,
|
|
367
|
+
kyMPerS,
|
|
368
|
+
biotCoefficient,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return { conductivity, areaM2, gauss };
|
|
372
|
+
}
|
|
176
373
|
export function buildPlaneStrainRectangularMesh(input) {
|
|
177
374
|
assertFinitePositive(input.widthM, 'widthM');
|
|
178
375
|
assertFinitePositive(input.heightM, 'heightM');
|
|
@@ -207,6 +404,206 @@ export function buildPlaneStrainRectangularMesh(input) {
|
|
|
207
404
|
}
|
|
208
405
|
return { nodes, elements };
|
|
209
406
|
}
|
|
407
|
+
export function runPlaneStrainSteadySeepage(model) {
|
|
408
|
+
if (model.schemaVersion !== 'fem-plane-strain-seepage-model.v1') {
|
|
409
|
+
throw new Error('Only fem-plane-strain-seepage-model.v1 is supported.');
|
|
410
|
+
}
|
|
411
|
+
assertArray(model.nodes, 'nodes');
|
|
412
|
+
assertArray(model.elements, 'elements');
|
|
413
|
+
assertArray(model.materials, 'materials');
|
|
414
|
+
assertArray(model.headBoundaryConditions, 'headBoundaryConditions');
|
|
415
|
+
if (model.nodes.length < 4)
|
|
416
|
+
throw new Error('Plane-strain seepage model requires at least four nodes.');
|
|
417
|
+
if (model.elements.length < 1)
|
|
418
|
+
throw new Error('Plane-strain seepage model requires at least one element.');
|
|
419
|
+
if (model.materials.length < 1)
|
|
420
|
+
throw new Error('Plane-strain seepage model requires at least one material.');
|
|
421
|
+
if (model.defaultThicknessM != null)
|
|
422
|
+
assertFinitePositive(model.defaultThicknessM, 'defaultThicknessM');
|
|
423
|
+
if (model.nodalFluxes != null)
|
|
424
|
+
assertArray(model.nodalFluxes, 'nodalFluxes');
|
|
425
|
+
const gammaWaterKpaPerM = model.gammaWaterKpaPerM ?? 9.81;
|
|
426
|
+
assertFinitePositive(gammaWaterKpaPerM, 'gammaWaterKpaPerM');
|
|
427
|
+
assertUniqueIds(model.nodes, 'node');
|
|
428
|
+
assertUniqueIds(model.materials, 'material');
|
|
429
|
+
assertUniqueIds(model.elements, 'element');
|
|
430
|
+
const policy = model.policy ?? DEFAULT_FEM_CONVERGENCE_POLICY;
|
|
431
|
+
validateConvergencePolicy(policy);
|
|
432
|
+
const nodeIndexById = new Map(model.nodes.map((node, index) => [node.id, index]));
|
|
433
|
+
const materialById = new Map(model.materials.map((material) => [material.id, material]));
|
|
434
|
+
const headDofCount = model.nodes.length;
|
|
435
|
+
if (headDofCount > MAX_DENSE_DOF_COUNT) {
|
|
436
|
+
throw new Error(`Plane-strain seepage dense assembly is capped at ${MAX_DENSE_DOF_COUNT} head DOFs for benchmark-scale evidence runs.`);
|
|
437
|
+
}
|
|
438
|
+
for (const node of model.nodes) {
|
|
439
|
+
assertFinite(node.xM, `node ${node.id} xM`);
|
|
440
|
+
assertFinite(node.yM, `node ${node.id} yM`);
|
|
441
|
+
}
|
|
442
|
+
for (const material of model.materials) {
|
|
443
|
+
validateHydraulicMaterial(material);
|
|
444
|
+
}
|
|
445
|
+
const conductivity = Array.from({ length: headDofCount }, () => new Array(headDofCount).fill(0));
|
|
446
|
+
const loads = new Array(headDofCount).fill(0);
|
|
447
|
+
for (const flux of model.nodalFluxes ?? []) {
|
|
448
|
+
const nodeIndex = nodeIndexById.get(flux.nodeId);
|
|
449
|
+
if (nodeIndex == null)
|
|
450
|
+
throw new Error(`Unknown nodal seepage flux node: ${flux.nodeId}.`);
|
|
451
|
+
const flowM3PerSPerM = flux.flowM3PerSPerM ?? 0;
|
|
452
|
+
assertFinite(flowM3PerSPerM, `nodal seepage flux ${flux.nodeId}.flowM3PerSPerM`);
|
|
453
|
+
loads[nodeIndex] += flowM3PerSPerM;
|
|
454
|
+
}
|
|
455
|
+
const elementGaussCache = [];
|
|
456
|
+
for (const element of model.elements) {
|
|
457
|
+
if (!Array.isArray(element.nodeIds) || element.nodeIds.length !== 4) {
|
|
458
|
+
throw new Error(`Plane-strain seepage element ${element.id} must reference exactly four nodes.`);
|
|
459
|
+
}
|
|
460
|
+
if (new Set(element.nodeIds).size !== 4) {
|
|
461
|
+
throw new Error(`Plane-strain seepage element ${element.id} has duplicate node references.`);
|
|
462
|
+
}
|
|
463
|
+
assertNonEmptyId(element.materialId, `element ${element.id} material`);
|
|
464
|
+
const nodeIndices = element.nodeIds.map((id) => {
|
|
465
|
+
const index = nodeIndexById.get(id);
|
|
466
|
+
if (index == null)
|
|
467
|
+
throw new Error(`Unknown seepage element node: ${id}.`);
|
|
468
|
+
return index;
|
|
469
|
+
});
|
|
470
|
+
const material = materialById.get(element.materialId);
|
|
471
|
+
if (!material)
|
|
472
|
+
throw new Error(`Unknown seepage element material: ${element.materialId}.`);
|
|
473
|
+
const thicknessM = element.thicknessM ?? model.defaultThicknessM ?? 1;
|
|
474
|
+
assertFinitePositive(thicknessM, `element ${element.id} thicknessM`);
|
|
475
|
+
const nodes = nodeIndices.map((index) => model.nodes[index]);
|
|
476
|
+
const elementData = hydraulicElementMatrices({ nodes, material, thicknessM });
|
|
477
|
+
for (let localRow = 0; localRow < 4; localRow += 1) {
|
|
478
|
+
for (let localCol = 0; localCol < 4; localCol += 1) {
|
|
479
|
+
conductivity[nodeIndices[localRow]][nodeIndices[localCol]] += elementData.conductivity[localRow][localCol];
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
elementGaussCache.push({
|
|
483
|
+
element,
|
|
484
|
+
globalNodes: nodeIndices,
|
|
485
|
+
areaM2: elementData.areaM2,
|
|
486
|
+
thicknessM,
|
|
487
|
+
gauss: elementData.gauss,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
const prescribed = new Map();
|
|
491
|
+
for (const bc of model.headBoundaryConditions) {
|
|
492
|
+
const nodeIndex = nodeIndexById.get(bc.nodeId);
|
|
493
|
+
if (nodeIndex == null)
|
|
494
|
+
throw new Error(`Unknown hydraulic head boundary node: ${bc.nodeId}.`);
|
|
495
|
+
assertFinite(bc.headM, `head boundary ${bc.nodeId}.headM`);
|
|
496
|
+
const existing = prescribed.get(nodeIndex);
|
|
497
|
+
if (existing != null && Math.abs(existing - bc.headM) > 1e-12) {
|
|
498
|
+
throw new Error(`Conflicting hydraulic head boundary condition for ${bc.nodeId}.`);
|
|
499
|
+
}
|
|
500
|
+
prescribed.set(nodeIndex, bc.headM);
|
|
501
|
+
}
|
|
502
|
+
if (prescribed.size < 2) {
|
|
503
|
+
throw new Error('Plane-strain seepage model requires at least two hydraulic head boundary conditions.');
|
|
504
|
+
}
|
|
505
|
+
const freeDofs = Array.from({ length: headDofCount }, (_, index) => index).filter((index) => !prescribed.has(index));
|
|
506
|
+
const heads = new Array(headDofCount).fill(0);
|
|
507
|
+
for (const [index, value] of prescribed)
|
|
508
|
+
heads[index] = value;
|
|
509
|
+
if (freeDofs.length > 0) {
|
|
510
|
+
const reducedK = freeDofs.map((row) => freeDofs.map((col) => conductivity[row][col]));
|
|
511
|
+
const reducedF = freeDofs.map((row) => loads[row] - [...prescribed.entries()]
|
|
512
|
+
.reduce((sum, [col, value]) => sum + conductivity[row][col] * value, 0));
|
|
513
|
+
const solved = solveDenseLinearSystem(reducedK, reducedF);
|
|
514
|
+
for (const [index, dof] of freeDofs.entries())
|
|
515
|
+
heads[dof] = solved[index];
|
|
516
|
+
}
|
|
517
|
+
const internal = matVec(conductivity, heads);
|
|
518
|
+
const residual = internal.map((value, index) => value - loads[index]);
|
|
519
|
+
const maxFreeMassResidualM3PerS = freeDofs.length > 0
|
|
520
|
+
? Math.max(...freeDofs.map((index) => Math.abs(residual[index])))
|
|
521
|
+
: 0;
|
|
522
|
+
const boundaryResiduals = Array.from(prescribed.keys()).map((index) => residual[index]);
|
|
523
|
+
const totalPositiveBoundaryFluxM3PerS = boundaryResiduals
|
|
524
|
+
.filter((value) => value > 0)
|
|
525
|
+
.reduce((sum, value) => sum + value, 0);
|
|
526
|
+
const totalNegativeBoundaryFluxM3PerS = -boundaryResiduals
|
|
527
|
+
.filter((value) => value < 0)
|
|
528
|
+
.reduce((sum, value) => sum + value, 0);
|
|
529
|
+
const boundaryFluxSum = boundaryResiduals.reduce((sum, value) => sum + value, 0);
|
|
530
|
+
const netNodalFluxM3PerS = loads.reduce((sum, value) => sum + value, 0);
|
|
531
|
+
const massScale = Math.max(totalPositiveBoundaryFluxM3PerS, totalNegativeBoundaryFluxM3PerS, Math.abs(netNodalFluxM3PerS), 1e-12);
|
|
532
|
+
const massBalanceErrorRatio = Math.abs(boundaryFluxSum + netNodalFluxM3PerS) / massScale;
|
|
533
|
+
let maxPorePressureKpa = 0;
|
|
534
|
+
let maxEffectiveStressReductionKpa = 0;
|
|
535
|
+
const elementOutputs = elementGaussCache.map((entry) => {
|
|
536
|
+
const gaussPoints = entry.gauss.map((point, index) => {
|
|
537
|
+
const localHeads = entry.globalNodes.map((nodeIndex) => heads[nodeIndex]);
|
|
538
|
+
const localNodes = entry.globalNodes.map((nodeIndex) => model.nodes[nodeIndex]);
|
|
539
|
+
const headM = point.shape.reduce((sum, shape, node) => sum + shape * localHeads[node], 0);
|
|
540
|
+
const elevationM = point.shape.reduce((sum, shape, node) => sum + shape * localNodes[node].yM, 0);
|
|
541
|
+
const gradientX = point.dNdx.reduce((sum, dNdx, node) => sum + dNdx * localHeads[node], 0);
|
|
542
|
+
const gradientY = point.dNdy.reduce((sum, dNdy, node) => sum + dNdy * localHeads[node], 0);
|
|
543
|
+
const porePressureKpa = Math.max(0, (headM - elevationM) * gammaWaterKpaPerM);
|
|
544
|
+
const effectiveStressReductionKpa = point.biotCoefficient * porePressureKpa;
|
|
545
|
+
maxPorePressureKpa = Math.max(maxPorePressureKpa, porePressureKpa);
|
|
546
|
+
maxEffectiveStressReductionKpa = Math.max(maxEffectiveStressReductionKpa, effectiveStressReductionKpa);
|
|
547
|
+
return {
|
|
548
|
+
elementId: entry.element.id,
|
|
549
|
+
gaussPoint: index + 1,
|
|
550
|
+
xi: round(point.xi, 10),
|
|
551
|
+
eta: round(point.eta, 10),
|
|
552
|
+
detJ: round(point.detJ, 10),
|
|
553
|
+
headM: round(headM, 10),
|
|
554
|
+
elevationM: round(elevationM, 10),
|
|
555
|
+
porePressureKpa: round(porePressureKpa, 8),
|
|
556
|
+
hydraulicGradient: [round(gradientX, 12), round(gradientY, 12)],
|
|
557
|
+
darcyFluxMPerS: [
|
|
558
|
+
Number((-point.kxMPerS * gradientX).toExponential(12)),
|
|
559
|
+
Number((-point.kyMPerS * gradientY).toExponential(12)),
|
|
560
|
+
],
|
|
561
|
+
biotCoefficient: round(point.biotCoefficient, 8),
|
|
562
|
+
effectiveStressReductionKpa: round(effectiveStressReductionKpa, 8),
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
return {
|
|
566
|
+
id: entry.element.id,
|
|
567
|
+
areaM2: round(entry.areaM2, 10),
|
|
568
|
+
thicknessM: round(entry.thicknessM, 10),
|
|
569
|
+
gaussPoints,
|
|
570
|
+
};
|
|
571
|
+
});
|
|
572
|
+
const nodes = model.nodes.map((node, index) => {
|
|
573
|
+
const porePressureKpa = Math.max(0, (heads[index] - node.yM) * gammaWaterKpaPerM);
|
|
574
|
+
maxPorePressureKpa = Math.max(maxPorePressureKpa, porePressureKpa);
|
|
575
|
+
return {
|
|
576
|
+
...node,
|
|
577
|
+
headM: round(heads[index], 10),
|
|
578
|
+
porePressureKpa: round(porePressureKpa, 8),
|
|
579
|
+
hydraulicResidualM3PerS: Number(residual[index].toExponential(12)),
|
|
580
|
+
};
|
|
581
|
+
});
|
|
582
|
+
return {
|
|
583
|
+
schemaVersion: 'fem-plane-strain-seepage-result.v1',
|
|
584
|
+
method: 'quad4-plane-strain-steady-darcy-seepage',
|
|
585
|
+
nodes,
|
|
586
|
+
elements: elementOutputs,
|
|
587
|
+
headDofCount,
|
|
588
|
+
freeHeadDofCount: freeDofs.length,
|
|
589
|
+
constrainedHeadDofCount: prescribed.size,
|
|
590
|
+
maxFreeMassResidualM3PerS: Number(maxFreeMassResidualM3PerS.toExponential(12)),
|
|
591
|
+
totalPositiveBoundaryFluxM3PerS: Number(totalPositiveBoundaryFluxM3PerS.toExponential(12)),
|
|
592
|
+
totalNegativeBoundaryFluxM3PerS: Number(totalNegativeBoundaryFluxM3PerS.toExponential(12)),
|
|
593
|
+
netNodalFluxM3PerS: Number(netNodalFluxM3PerS.toExponential(12)),
|
|
594
|
+
massBalanceErrorRatio: round(massBalanceErrorRatio, 12),
|
|
595
|
+
maxPorePressureKpa: round(maxPorePressureKpa, 8),
|
|
596
|
+
maxEffectiveStressReductionKpa: round(maxEffectiveStressReductionKpa, 8),
|
|
597
|
+
converged: maxFreeMassResidualM3PerS <= policy.porePressureMassBalanceTolerance &&
|
|
598
|
+
massBalanceErrorRatio <= policy.porePressureMassBalanceTolerance,
|
|
599
|
+
policy,
|
|
600
|
+
limitations: [
|
|
601
|
+
'Benchmark-scale steady saturated Darcy seepage evidence kernel only.',
|
|
602
|
+
'Solves hydraulic head on the Quad4 mesh and reports pore-pressure/effective-stress reduction metadata, but it does not add pore-pressure DOFs to the mechanical stiffness matrix.',
|
|
603
|
+
'No transient 2D/3D Biot consolidation, unsaturated flow, uplift/piping design acceptance, production sparse solver, or route-backed result manifest is provided.',
|
|
604
|
+
],
|
|
605
|
+
};
|
|
606
|
+
}
|
|
210
607
|
export function runPlaneStrainQuad4Assembly(model) {
|
|
211
608
|
if (model.schemaVersion !== 'fem-plane-strain-model.v1') {
|
|
212
609
|
throw new Error('Only fem-plane-strain-model.v1 is supported.');
|
|
@@ -397,4 +794,316 @@ export function runPlaneStrainQuad4Assembly(model) {
|
|
|
397
794
|
policy,
|
|
398
795
|
};
|
|
399
796
|
}
|
|
797
|
+
function assemblePlaneStrainSystem(model) {
|
|
798
|
+
if (model.schemaVersion !== 'fem-plane-strain-model.v1') {
|
|
799
|
+
throw new Error('Only fem-plane-strain-model.v1 is supported.');
|
|
800
|
+
}
|
|
801
|
+
assertArray(model.nodes, 'nodes');
|
|
802
|
+
assertArray(model.elements, 'elements');
|
|
803
|
+
assertArray(model.materials, 'materials');
|
|
804
|
+
assertArray(model.boundaryConditions, 'boundaryConditions');
|
|
805
|
+
if (model.nodes.length < 4)
|
|
806
|
+
throw new Error('Plane-strain model requires at least four nodes.');
|
|
807
|
+
if (model.elements.length < 1)
|
|
808
|
+
throw new Error('Plane-strain model requires at least one element.');
|
|
809
|
+
if (model.materials.length < 1)
|
|
810
|
+
throw new Error('Plane-strain model requires at least one material.');
|
|
811
|
+
if (model.defaultThicknessM != null)
|
|
812
|
+
assertFinitePositive(model.defaultThicknessM, 'defaultThicknessM');
|
|
813
|
+
if (model.nodalLoads != null)
|
|
814
|
+
assertArray(model.nodalLoads, 'nodalLoads');
|
|
815
|
+
assertUniqueIds(model.nodes, 'node');
|
|
816
|
+
assertUniqueIds(model.materials, 'material');
|
|
817
|
+
assertUniqueIds(model.elements, 'element');
|
|
818
|
+
const policy = model.policy ?? DEFAULT_FEM_CONVERGENCE_POLICY;
|
|
819
|
+
validateConvergencePolicy(policy);
|
|
820
|
+
const nodeIndexById = new Map(model.nodes.map((node, index) => [node.id, index]));
|
|
821
|
+
const materialById = new Map(model.materials.map((material) => [material.id, material]));
|
|
822
|
+
const dofCount = model.nodes.length * 2;
|
|
823
|
+
if (dofCount > MAX_DENSE_DOF_COUNT) {
|
|
824
|
+
throw new Error(`Plane-strain dense assembly is capped at ${MAX_DENSE_DOF_COUNT} DOFs for benchmark-scale evidence runs.`);
|
|
825
|
+
}
|
|
826
|
+
const stiffness = Array.from({ length: dofCount }, () => new Array(dofCount).fill(0));
|
|
827
|
+
const loads = new Array(dofCount).fill(0);
|
|
828
|
+
for (const node of model.nodes) {
|
|
829
|
+
assertFinite(node.xM, `node ${node.id} xM`);
|
|
830
|
+
assertFinite(node.yM, `node ${node.id} yM`);
|
|
831
|
+
}
|
|
832
|
+
for (const material of model.materials) {
|
|
833
|
+
planeStrainD(material);
|
|
834
|
+
druckerPragerParameters(material);
|
|
835
|
+
if (material.unitWeightKnM3 != null) {
|
|
836
|
+
assertFiniteNonNegative(material.unitWeightKnM3, `material ${material.id} unitWeightKnM3`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
for (const load of model.nodalLoads ?? []) {
|
|
840
|
+
const nodeIndex = nodeIndexById.get(load.nodeId);
|
|
841
|
+
if (nodeIndex == null)
|
|
842
|
+
throw new Error(`Unknown nodal load node: ${load.nodeId}.`);
|
|
843
|
+
const fxKn = load.fxKn ?? 0;
|
|
844
|
+
const fyKn = load.fyKn ?? 0;
|
|
845
|
+
assertFinite(fxKn, `nodal load ${load.nodeId}.fxKn`);
|
|
846
|
+
assertFinite(fyKn, `nodal load ${load.nodeId}.fyKn`);
|
|
847
|
+
loads[dofIndex(nodeIndex, 'ux')] += fxKn;
|
|
848
|
+
loads[dofIndex(nodeIndex, 'uy')] += fyKn;
|
|
849
|
+
}
|
|
850
|
+
const elementGaussCache = [];
|
|
851
|
+
for (const element of model.elements) {
|
|
852
|
+
if (!Array.isArray(element.nodeIds) || element.nodeIds.length !== 4) {
|
|
853
|
+
throw new Error(`Plane-strain element ${element.id} must reference exactly four nodes.`);
|
|
854
|
+
}
|
|
855
|
+
if (new Set(element.nodeIds).size !== 4) {
|
|
856
|
+
throw new Error(`Plane-strain element ${element.id} has duplicate node references.`);
|
|
857
|
+
}
|
|
858
|
+
assertNonEmptyId(element.materialId, `element ${element.id} material`);
|
|
859
|
+
const nodeIndices = element.nodeIds.map((id) => {
|
|
860
|
+
const index = nodeIndexById.get(id);
|
|
861
|
+
if (index == null)
|
|
862
|
+
throw new Error(`Unknown element node: ${id}.`);
|
|
863
|
+
return index;
|
|
864
|
+
});
|
|
865
|
+
const material = materialById.get(element.materialId);
|
|
866
|
+
if (!material)
|
|
867
|
+
throw new Error(`Unknown element material: ${element.materialId}.`);
|
|
868
|
+
const thicknessM = element.thicknessM ?? model.defaultThicknessM ?? 1;
|
|
869
|
+
assertFinitePositive(thicknessM, `element ${element.id} thicknessM`);
|
|
870
|
+
const nodes = nodeIndices.map((index) => model.nodes[index]);
|
|
871
|
+
const elementData = elementMatrices({ nodes, material, thicknessM });
|
|
872
|
+
const globalDofs = nodeIndices.flatMap((index) => [dofIndex(index, 'ux'), dofIndex(index, 'uy')]);
|
|
873
|
+
for (let localRow = 0; localRow < 8; localRow += 1) {
|
|
874
|
+
for (let localCol = 0; localCol < 8; localCol += 1) {
|
|
875
|
+
stiffness[globalDofs[localRow]][globalDofs[localCol]] += elementData.stiffness[localRow][localCol];
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
elementGaussCache.push({
|
|
879
|
+
element,
|
|
880
|
+
globalDofs,
|
|
881
|
+
gauss: elementData.gauss,
|
|
882
|
+
areaM2: elementData.areaM2,
|
|
883
|
+
thicknessM,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
const prescribed = new Map();
|
|
887
|
+
for (const bc of model.boundaryConditions) {
|
|
888
|
+
const nodeIndex = nodeIndexById.get(bc.nodeId);
|
|
889
|
+
if (nodeIndex == null)
|
|
890
|
+
throw new Error(`Unknown boundary-condition node: ${bc.nodeId}.`);
|
|
891
|
+
if (bc.dof !== 'ux' && bc.dof !== 'uy') {
|
|
892
|
+
throw new Error(`Boundary condition ${bc.nodeId} dof must be ux or uy.`);
|
|
893
|
+
}
|
|
894
|
+
const index = dofIndex(nodeIndex, bc.dof);
|
|
895
|
+
const value = bc.valueM ?? 0;
|
|
896
|
+
assertFinite(value, `boundary condition ${bc.nodeId}.${bc.dof}`);
|
|
897
|
+
const existing = prescribed.get(index);
|
|
898
|
+
if (existing != null && Math.abs(existing - value) > 1e-12) {
|
|
899
|
+
throw new Error(`Conflicting boundary condition for ${bc.nodeId}.${bc.dof}.`);
|
|
900
|
+
}
|
|
901
|
+
prescribed.set(index, value);
|
|
902
|
+
}
|
|
903
|
+
if (prescribed.size === 0)
|
|
904
|
+
throw new Error('Plane-strain model requires at least one displacement boundary condition.');
|
|
905
|
+
const constrainedNodeIds = new Set(model.boundaryConditions.map((bc) => bc.nodeId));
|
|
906
|
+
const hasUxConstraint = model.boundaryConditions.some((bc) => bc.dof === 'ux');
|
|
907
|
+
const hasUyConstraint = model.boundaryConditions.some((bc) => bc.dof === 'uy');
|
|
908
|
+
if (prescribed.size < 3 || constrainedNodeIds.size < 2 || !hasUxConstraint || !hasUyConstraint) {
|
|
909
|
+
throw new Error('Plane-strain model has insufficient displacement constraints to restrain rigid-body modes.');
|
|
910
|
+
}
|
|
911
|
+
const freeDofs = Array.from({ length: dofCount }, (_, index) => index).filter((index) => !prescribed.has(index));
|
|
912
|
+
return {
|
|
913
|
+
policy,
|
|
914
|
+
materialById,
|
|
915
|
+
stiffness,
|
|
916
|
+
loads,
|
|
917
|
+
prescribed,
|
|
918
|
+
freeDofs,
|
|
919
|
+
dofCount,
|
|
920
|
+
elementGaussCache,
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function evaluatePlaneStrainDruckerPragerState(input) {
|
|
924
|
+
const { system, displacement, loadFactor } = input;
|
|
925
|
+
const internal = new Array(system.dofCount).fill(0);
|
|
926
|
+
let maxYieldResidualRatio = 0;
|
|
927
|
+
let maxEquivalentPlasticStrain = 0;
|
|
928
|
+
let plasticGaussPointCount = 0;
|
|
929
|
+
const elements = system.elementGaussCache.map((entry) => {
|
|
930
|
+
const material = system.materialById.get(entry.element.materialId);
|
|
931
|
+
const d = planeStrainD(material);
|
|
932
|
+
const elementDisplacement = entry.globalDofs.map((index) => displacement[index]);
|
|
933
|
+
const gaussPoints = entry.gauss.map((point, index) => {
|
|
934
|
+
const strain = point.b.map((row) => row.reduce((sum, value, col) => sum + value * elementDisplacement[col], 0));
|
|
935
|
+
const elasticStress = d.map((row) => row.reduce((sum, value, col) => sum + value * strain[col], 0));
|
|
936
|
+
const projected = projectDruckerPragerStress({
|
|
937
|
+
material,
|
|
938
|
+
policy: system.policy,
|
|
939
|
+
strain,
|
|
940
|
+
stress: elasticStress,
|
|
941
|
+
sigmaZKpa: planeStrainSigmaZ(material, strain),
|
|
942
|
+
});
|
|
943
|
+
maxYieldResidualRatio = Math.max(maxYieldResidualRatio, projected.yieldResidualRatio);
|
|
944
|
+
maxEquivalentPlasticStrain = Math.max(maxEquivalentPlasticStrain, projected.equivalentPlasticStrain);
|
|
945
|
+
if (projected.state === 'plastic')
|
|
946
|
+
plasticGaussPointCount += 1;
|
|
947
|
+
for (let localDof = 0; localDof < 8; localDof += 1) {
|
|
948
|
+
const force = point.b.reduce((sum, bRow, component) => sum + bRow[localDof] * projected.stressKpa[component], 0) * point.detJ * entry.thicknessM;
|
|
949
|
+
internal[entry.globalDofs[localDof]] += force;
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
elementId: entry.element.id,
|
|
953
|
+
gaussPoint: index + 1,
|
|
954
|
+
xi: round(point.xi, 10),
|
|
955
|
+
eta: round(point.eta, 10),
|
|
956
|
+
detJ: round(point.detJ, 10),
|
|
957
|
+
strain: [
|
|
958
|
+
round(projected.strain[0], 12),
|
|
959
|
+
round(projected.strain[1], 12),
|
|
960
|
+
round(projected.strain[2], 12),
|
|
961
|
+
],
|
|
962
|
+
stressKpa: [
|
|
963
|
+
round(projected.stressKpa[0], 8),
|
|
964
|
+
round(projected.stressKpa[1], 8),
|
|
965
|
+
round(projected.stressKpa[2], 8),
|
|
966
|
+
],
|
|
967
|
+
outOfPlaneStressKpa: round(projected.outOfPlaneStressKpa, 8),
|
|
968
|
+
compressionPositivePrincipalStressKpa: [
|
|
969
|
+
round(projected.compressionPositivePrincipalStressKpa[0], 8),
|
|
970
|
+
round(projected.compressionPositivePrincipalStressKpa[1], 8),
|
|
971
|
+
round(projected.compressionPositivePrincipalStressKpa[2], 8),
|
|
972
|
+
],
|
|
973
|
+
yieldValueKpa: round(projected.yieldValueKpa, 10),
|
|
974
|
+
yieldResidualRatio: round(projected.yieldResidualRatio, 12),
|
|
975
|
+
plasticMultiplier: round(projected.plasticMultiplier, 12),
|
|
976
|
+
equivalentPlasticStrain: round(projected.equivalentPlasticStrain, 12),
|
|
977
|
+
state: projected.state,
|
|
978
|
+
};
|
|
979
|
+
});
|
|
980
|
+
return {
|
|
981
|
+
id: entry.element.id,
|
|
982
|
+
areaM2: round(entry.areaM2, 10),
|
|
983
|
+
thicknessM: round(entry.thicknessM, 10),
|
|
984
|
+
gaussPoints,
|
|
985
|
+
};
|
|
986
|
+
});
|
|
987
|
+
const loadsAtStep = system.loads.map((load) => load * loadFactor);
|
|
988
|
+
const residual = internal.map((value, index) => value - loadsAtStep[index]);
|
|
989
|
+
const maxFreeResidualKn = system.freeDofs.length > 0
|
|
990
|
+
? Math.max(...system.freeDofs.map((index) => Math.abs(residual[index])))
|
|
991
|
+
: 0;
|
|
992
|
+
const loadNorm = Math.max(Math.hypot(...loadsAtStep), Math.hypot(...internal), Math.hypot(...Array.from(system.prescribed.keys()).map((index) => residual[index])), 1);
|
|
993
|
+
const residualNormRatio = maxFreeResidualKn / loadNorm;
|
|
994
|
+
const externalLoadSumX = loadsAtStep.filter((_, index) => index % 2 === 0).reduce((sum, value) => sum + value, 0);
|
|
995
|
+
const externalLoadSumY = loadsAtStep.filter((_, index) => index % 2 === 1).reduce((sum, value) => sum + value, 0);
|
|
996
|
+
const reactionSumX = Array.from(system.prescribed.keys())
|
|
997
|
+
.filter((index) => index % 2 === 0)
|
|
998
|
+
.reduce((sum, index) => sum + residual[index], 0);
|
|
999
|
+
const reactionSumY = Array.from(system.prescribed.keys())
|
|
1000
|
+
.filter((index) => index % 2 === 1)
|
|
1001
|
+
.reduce((sum, index) => sum + residual[index], 0);
|
|
1002
|
+
const balanceError = Math.hypot(reactionSumX + externalLoadSumX, reactionSumY + externalLoadSumY);
|
|
1003
|
+
const balanceScale = Math.max(Math.hypot(reactionSumX, reactionSumY), Math.hypot(externalLoadSumX, externalLoadSumY), 1);
|
|
1004
|
+
const reactionBalanceRatio = 1 - balanceError / balanceScale;
|
|
1005
|
+
return {
|
|
1006
|
+
internal,
|
|
1007
|
+
residual,
|
|
1008
|
+
maxFreeResidualKn,
|
|
1009
|
+
residualNormRatio,
|
|
1010
|
+
reactionBalanceRatio,
|
|
1011
|
+
maxYieldResidualRatio,
|
|
1012
|
+
maxEquivalentPlasticStrain,
|
|
1013
|
+
plasticGaussPointCount,
|
|
1014
|
+
elements,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function normalizeLoadStepFractions(loadStepFractions) {
|
|
1018
|
+
const fractions = loadStepFractions && loadStepFractions.length > 0
|
|
1019
|
+
? [...loadStepFractions]
|
|
1020
|
+
: [0.25, 0.5, 0.75, 1];
|
|
1021
|
+
let previous = 0;
|
|
1022
|
+
for (const [index, fraction] of fractions.entries()) {
|
|
1023
|
+
if (!Number.isFinite(fraction) || fraction <= previous || fraction > 1) {
|
|
1024
|
+
throw new Error(`loadStepFractions.${index} must be finite, increasing, and between 0 and 1.`);
|
|
1025
|
+
}
|
|
1026
|
+
previous = fraction;
|
|
1027
|
+
}
|
|
1028
|
+
if (fractions[fractions.length - 1] !== 1) {
|
|
1029
|
+
throw new Error('loadStepFractions must end at 1.');
|
|
1030
|
+
}
|
|
1031
|
+
return fractions;
|
|
1032
|
+
}
|
|
1033
|
+
export function runPlaneStrainDruckerPragerLoadSteps(model, options = {}) {
|
|
1034
|
+
const system = assemblePlaneStrainSystem(model);
|
|
1035
|
+
const loadStepFractions = normalizeLoadStepFractions(options.loadStepFractions);
|
|
1036
|
+
const reducedK = system.freeDofs.map((row) => system.freeDofs.map((col) => system.stiffness[row][col]));
|
|
1037
|
+
const displacement = new Array(system.dofCount).fill(0);
|
|
1038
|
+
let finalEvaluation;
|
|
1039
|
+
const loadSteps = [];
|
|
1040
|
+
for (const [stepIndex, loadFactor] of loadStepFractions.entries()) {
|
|
1041
|
+
for (const [index, value] of system.prescribed)
|
|
1042
|
+
displacement[index] = value * loadFactor;
|
|
1043
|
+
let evaluation = evaluatePlaneStrainDruckerPragerState({ system, displacement, loadFactor });
|
|
1044
|
+
let iterations = 0;
|
|
1045
|
+
let converged = evaluation.residualNormRatio <= system.policy.forceBalanceTolerance &&
|
|
1046
|
+
evaluation.maxYieldResidualRatio <= system.policy.residualTolerance;
|
|
1047
|
+
while (!converged && iterations < system.policy.maxIterations) {
|
|
1048
|
+
iterations += 1;
|
|
1049
|
+
if (system.freeDofs.length === 0)
|
|
1050
|
+
break;
|
|
1051
|
+
const correctionRhs = system.freeDofs.map((index) => -evaluation.residual[index]);
|
|
1052
|
+
const correction = solveDenseLinearSystem(reducedK, correctionRhs);
|
|
1053
|
+
for (const [correctionIndex, dof] of system.freeDofs.entries()) {
|
|
1054
|
+
displacement[dof] += correction[correctionIndex];
|
|
1055
|
+
}
|
|
1056
|
+
for (const [index, value] of system.prescribed)
|
|
1057
|
+
displacement[index] = value * loadFactor;
|
|
1058
|
+
evaluation = evaluatePlaneStrainDruckerPragerState({ system, displacement, loadFactor });
|
|
1059
|
+
converged = evaluation.residualNormRatio <= system.policy.forceBalanceTolerance &&
|
|
1060
|
+
evaluation.maxYieldResidualRatio <= system.policy.residualTolerance;
|
|
1061
|
+
}
|
|
1062
|
+
finalEvaluation = evaluation;
|
|
1063
|
+
loadSteps.push({
|
|
1064
|
+
step: stepIndex + 1,
|
|
1065
|
+
loadFactor: round(loadFactor, 8),
|
|
1066
|
+
iterations,
|
|
1067
|
+
maxFreeResidualKn: round(evaluation.maxFreeResidualKn, 12),
|
|
1068
|
+
residualNormRatio: round(evaluation.residualNormRatio, 12),
|
|
1069
|
+
reactionBalanceRatio: round(evaluation.reactionBalanceRatio, 12),
|
|
1070
|
+
maxYieldResidualRatio: round(evaluation.maxYieldResidualRatio, 12),
|
|
1071
|
+
maxEquivalentPlasticStrain: round(evaluation.maxEquivalentPlasticStrain, 12),
|
|
1072
|
+
plasticGaussPointCount: evaluation.plasticGaussPointCount,
|
|
1073
|
+
converged,
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
if (!finalEvaluation) {
|
|
1077
|
+
throw new Error('Plane-strain nonlinear load-step solver requires at least one load step.');
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
schemaVersion: 'fem-plane-strain-drucker-prager-result.v1',
|
|
1081
|
+
method: 'quad4-plane-strain-drucker-prager-modified-newton',
|
|
1082
|
+
nodes: model.nodes.map((node, index) => ({
|
|
1083
|
+
...node,
|
|
1084
|
+
uxM: round(displacement[dofIndex(index, 'ux')], 12),
|
|
1085
|
+
uyM: round(displacement[dofIndex(index, 'uy')], 12),
|
|
1086
|
+
rxnXKn: round(finalEvaluation.residual[dofIndex(index, 'ux')], 8),
|
|
1087
|
+
rxnYKn: round(finalEvaluation.residual[dofIndex(index, 'uy')], 8),
|
|
1088
|
+
})),
|
|
1089
|
+
elements: finalEvaluation.elements,
|
|
1090
|
+
dofCount: system.dofCount,
|
|
1091
|
+
freeDofCount: system.freeDofs.length,
|
|
1092
|
+
constrainedDofCount: system.prescribed.size,
|
|
1093
|
+
loadSteps,
|
|
1094
|
+
maxFreeResidualKn: round(finalEvaluation.maxFreeResidualKn, 12),
|
|
1095
|
+
residualNormRatio: round(finalEvaluation.residualNormRatio, 12),
|
|
1096
|
+
reactionBalanceRatio: round(finalEvaluation.reactionBalanceRatio, 12),
|
|
1097
|
+
maxYieldResidualRatio: round(finalEvaluation.maxYieldResidualRatio, 12),
|
|
1098
|
+
maxEquivalentPlasticStrain: round(finalEvaluation.maxEquivalentPlasticStrain, 12),
|
|
1099
|
+
plasticGaussPointCount: finalEvaluation.plasticGaussPointCount,
|
|
1100
|
+
converged: loadSteps.every((step) => step.converged),
|
|
1101
|
+
policy: system.policy,
|
|
1102
|
+
limitations: [
|
|
1103
|
+
'Benchmark-scale modified-Newton plane-strain plasticity evidence kernel only.',
|
|
1104
|
+
'Uses elastic global tangent with Gauss-point Drucker-Prager stress projection; no production consistent tangent, sparse solver, hardening calibration, staged activation, pore-pressure DOF, or route-backed result manifest is provided.',
|
|
1105
|
+
'Use for deterministic evidence and regression tests only until independent published/commercial benchmark comparison and licensed production approval gates are complete.',
|
|
1106
|
+
],
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
400
1109
|
//# sourceMappingURL=plane-strain-assembly.js.map
|