Methodology
gusset plate surrogate; everything below is served from the running code
Back to the app

How this works, end to end

This page is the long answer. Tables and numbers are fetched live from the running server (/api/convergence, /api/genlog, /api/weights, /api/dataset), and the source code shown at the bottom is extracted with inspect.getsource from the modules the server is executing right now, so what you read here cannot drift from what runs.

1. Physics + gates
plane-stress FEM; equilibrium, linearity, Whitmore checks refuse bad solvers
2. Mesh convergence
refine until the peak stress stops moving; pick that density
3. Dataset
Latin hypercube, separate-seed test split, resumable offline generation
4. Surrogate
GBM vs MLP, held-out metrics, weights to the browser with parity proof
5. Verify on demand
the real solver, live, for any slider point; field, mesh and overlay plots on screen

1. The physics

A corner gusset plate in linear elastic plane stress: E = 200 GPa, nu = 0.3, steel. The plate (W x H mm, thickness t) sits between a column flange (left edge) and a beam flange (bottom edge); both edges are fixed, an idealization of welding to much stiffer members. The far corner is chamfered perpendicular to the brace axis, the usual fabricated shape. A brace force P (tension or compression) enters along the brace axis at angle theta over a connection of length Lc: modeled as a uniform in-plane traction over a strip Lc long and 60 mm wide (a splice plate footprint), with the integrated resultant normalized to exactly P.

Why a strip and not a line: a line load that terminates inside a plate has a log-singular stress at its end point, so "peak von Mises" would grow forever under mesh refinement and any convergence claim would be false. A finite footprint is both more physical and convergent. The same reasoning excludes modeling bolts as point forces here.

Elements: quadratic triangles (P2). Quadratic displacement gives linear strain within each element, which resolves the stress gradient at the Whitmore zone far better per degree of freedom than linear triangles. Meshing is gmsh (frontal-Delaunay) with a distance-based size field: fine near the connection strip and the working-point corner, coarser in the far field (near-field size = far-field size / 4). Solver: direct sparse LU on the condensed system (scikit-fem assembly, scipy solve).

Outputs per solve: peak von Mises stress (evaluated at quadrature points, not smoothed) and its location, max displacement magnitude, capacity ratio = peak / Fy (a screening number), plus the Whitmore hand-calc cross-check described below.

2. Mesh convergence study

Criterion: refine the mesh family (far-field size h, near-field h/4) until the peak von Mises stress changes by under 1.5% between successive levels; use that density for the dataset. The study runs one mid-domain reference case plus four corner-of-the-box audit cases (small connection on a big plate, geometrically clamped connection on a small plate, the largest plate, extreme angles). The full table below is convergence.json, served live.

loading convergence.json...

3. Sanity gates, with the real numbers

The dataset generator refuses to produce a single sample until the solver passes three gates. This is the actual gate block from the generation log (/api/genlog, the same file the generator appends to):

loading generation log...

4. The Whitmore cross-check

The standard hand method for gusset stress is the Whitmore section: the brace force spreads at 30 degrees over the connection length, so the effective width is Lc + 2 · Lc · tan(30) and the hand stress is sigma = P / (width · t), taken at the inner end of the connection (the last bolt row or weld end), where the gusset carries the full force.

Every FEM solve computes the same quantity from the model: the normal stress along the brace axis, averaged over the Whitmore section line. The ratio of hand to FEM is documented, not forced: the hand method is a uniform-stress idealization and the FEM resolves the actual peaked distribution across that width, so the two should be close on average but not equal. Observed across the dataset so far: loading.... The FEM peak landing on the Whitmore section, visibly, in the field plot of every verification, is the cross-check working. The same construction is drawn in the app's geometry diagram: the spread zone is shaded with its width annotated, straight from fem.geometry().

5. Dataset design

6. The surrogate, and the parity chain

Two models per output, trained on the same split and judged on the same held-out test set against FEM truth: a gradient-boosted tree baseline (one HistGradientBoostingRegressor per output) and a 6 → 64 → 64 → 2 tanh MLP (standardized inputs and outputs). The MLP ships because the browser needs a plain forward pass; its metrics are reported next to the GBM's either way. Current active-model metrics:

loading metrics...

The parity chain: sklearn pipeline → manual numpy forward pass (asserted equal to 1e-8 at export) → hand-written JS forward pass in the browser, which re-predicts five exported reference points on every page load and shows PASS/FAIL in the footer. The error bars on the output cards are the held-out test MAE in physical units; nothing about them is estimated.

7. Verify on demand

The button that matters. For the current slider point the server runs the actual FEM, streaming each stage (mesh, assembly with DOF count, solve, stress recovery) over SSE while it happens, in its true wall time. The result lands next to the surrogate prediction with absolute and percent error, gets appended to a persisted verification log, and three figures of that exact solve are rendered server-side with matplotlib: the von Mises field (so the stress concentration at the Whitmore zone is visible, not asserted), the FE mesh as a wireframe (so the graded near-field refinement around the load introduction is visible), and the mesh overlaid on the field at low opacity (so you can see that the refinement sits where the stress concentrates). Verifications are accepted outside the training domain on purpose (within wider physical-sanity caps); that is how you see the domain boundary doing its job.

A verification is a background job on the server: it runs to completion and persists its result and figures whether or not any browser is listening to the event stream, and the job log replays from disk after a server restart.

8. Limits, stated plainly

9. The literal source

Extracted with inspect.getsource from the modules this server is running. The JS forward pass is sliced out of the page actually served to your browser.

loading source...