feat: gate heavy solver JSON, field traceability API, GUI rod/export depth

- C: emit profiles/diagnostics/fourier only when enable flags are set; null otherwise
- API: fieldTraceability on case parse/default and solve; fix GET /solve/default options
- Tests: golden fingerprint, quality gates, C diagnostics invariants; cardQa mean empty guard
- Makefile: test-solver-sanitize ASan/UBSan target; README and COMPUTE_PLAN updates
- GUI: taper weight/MTS/guides/sinker round-trip, rod catalog, solver output toggles,
  results (profiles/diagnostics/Fourier/traceability), engineering checks and tabs
- Restore canonical WellName in base-case for regression; trace TaperGuidesCountArray

Made-with: Cursor
This commit is contained in:
2026-04-16 23:19:00 -06:00
parent 10f6ae1c2b
commit 64e9d31373
39 changed files with 1318 additions and 369 deletions

View File

@@ -3,7 +3,6 @@ import type { Options, AlignedData } from "uplot";
import type { SolveResponse, SolverOutput } from "../../types";
import type { CaseState } from "../../state/caseModel";
import type { EngineeringChecks } from "../../state/engineeringChecks";
import { DLS_BAD_SECTION_THRESHOLD } from "../../state/engineeringChecks";
import { buildTrajectorySegments } from "../../state/trajectoryMetrics";
import { Fieldset } from "../common/Fieldset";
import { UPlotChart } from "../common/UPlotChart";
@@ -88,17 +87,9 @@ export function ResultsTab({
const primary = result?.solver;
const fea = result?.solvers?.fea ?? null;
const fdm = result?.solvers?.fdm ?? primary ?? null;
const [overlayMode, setOverlayMode] = useState<OverlayMode>("dls");
const [overlayMode, setOverlayMode] = useState<OverlayMode>("depth");
const [selectedSegment, setSelectedSegment] = useState<number | null>(null);
const [badOnly, setBadOnly] = useState(false);
const trajectorySegments = useMemo(() => buildTrajectorySegments(caseState.survey), [caseState.survey]);
const filteredSegments = useMemo(
() =>
badOnly
? trajectorySegments.filter((segment) => segment.dls >= DLS_BAD_SECTION_THRESHOLD)
: trajectorySegments,
[badOnly, trajectorySegments]
);
const sideLoadProfile = primary?.profiles?.sideLoadProfile ?? null;
const pumpDiag = useMemo(() => computePumpPlacement(caseState), [caseState]);
@@ -173,17 +164,17 @@ export function ResultsTab({
)}
<Fieldset legend="3D Wellbore / Rod String / Pump">
<p className="panel-note">
Tubing trajectory is colored by dogleg severity (DLS). Rod string is overlaid with a
Tubing trajectory is colored by measured depth (hue). Rod string is overlaid with a
depth gradient, and pump location is marked in red.
</p>
<div className="button-row">
<button
type="button"
className={`btn ${overlayMode === "dls" ? "btn-primary" : ""}`}
onClick={() => setOverlayMode("dls")}
aria-pressed={overlayMode === "dls"}
className={`btn ${overlayMode === "depth" ? "btn-primary" : ""}`}
onClick={() => setOverlayMode("depth")}
aria-pressed={overlayMode === "depth"}
>
Overlay: DLS
Overlay: Depth
</button>
<button
type="button"
@@ -247,19 +238,11 @@ export function ResultsTab({
svgId="wellbore-3d-svg"
/>
</Fieldset>
<Fieldset legend="Trajectory Analytics (Segment DLS)">
<div className="button-row">
<button
type="button"
className={`btn ${badOnly ? "btn-primary" : ""}`}
onClick={() => setBadOnly((v) => !v)}
>
{badOnly ? "Showing bad segments only" : "Show only bad segments"}
</button>
<span className="panel-note" style={{ marginLeft: "auto" }}>
Click row or 3D segment to cross-highlight.
</span>
</div>
<Fieldset legend="Trajectory Analytics">
<p className="panel-note" style={{ marginBottom: 8 }}>
Click a row or 3D segment to cross-highlight. DLS is shown numerically only (deg per
100 ft MD).
</p>
<div className="table-scroll" style={{ maxHeight: 250 }}>
<table className="data-table">
<thead>
@@ -269,45 +252,35 @@ export function ResultsTab({
<th>MD end</th>
<th>ΔMD</th>
<th>DLS (deg/100)</th>
<th>Severity</th>
</tr>
</thead>
<tbody>
{filteredSegments.map((segment) => {
const severity =
segment.dls >= DLS_BAD_SECTION_THRESHOLD
? "bad"
: segment.dls >= DLS_BAD_SECTION_THRESHOLD * 0.5
? "moderate"
: "low";
return (
<tr
key={segment.index}
className={selectedSegment === segment.index ? "row-selected" : ""}
onClick={() => setSelectedSegment(segment.index)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
setSelectedSegment(segment.index);
}
}}
role="button"
tabIndex={0}
aria-label={`Select trajectory segment ${segment.index + 1}`}
style={{ cursor: "pointer" }}
>
<td>{segment.index + 1}</td>
<td>{segment.a.md.toFixed(1)}</td>
<td>{segment.b.md.toFixed(1)}</td>
<td>{segment.dMd.toFixed(1)}</td>
<td>{segment.dls.toFixed(2)}</td>
<td>{severity}</td>
</tr>
);
})}
{!filteredSegments.length && (
{trajectorySegments.map((segment) => (
<tr
key={segment.index}
className={selectedSegment === segment.index ? "row-selected" : ""}
onClick={() => setSelectedSegment(segment.index)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
setSelectedSegment(segment.index);
}
}}
role="button"
tabIndex={0}
aria-label={`Select trajectory segment ${segment.index + 1}`}
style={{ cursor: "pointer" }}
>
<td>{segment.index + 1}</td>
<td>{segment.a.md.toFixed(1)}</td>
<td>{segment.b.md.toFixed(1)}</td>
<td>{segment.dMd.toFixed(1)}</td>
<td>{segment.dls.toFixed(2)}</td>
</tr>
))}
{!trajectorySegments.length && (
<tr>
<td colSpan={6} className="empty-row">No trajectory segments to display.</td>
<td colSpan={5} className="empty-row">No trajectory segments to display.</td>
</tr>
)}
</tbody>
@@ -383,8 +356,122 @@ export function ResultsTab({
) : (
<p className="panel-note">No card data in response.</p>
)}
{primary?.fourierBaseline && primary.fourierBaseline.card?.length ? (
<div style={{ marginTop: 16 }}>
<p className="panel-note">
Fourier baseline ({primary.fourierBaseline.harmonics} harmonics): RMS polished{" "}
{primary.fourierBaseline.residualRmsPolished?.toFixed(1)} N · RMS downhole{" "}
{primary.fourierBaseline.residualRmsDownhole?.toFixed(1)} N
</p>
<UPlotChart
height={260}
data={(() => {
const n = Math.min(primary.card.length, primary.fourierBaseline.card.length);
const pos = primary.card.slice(0, n).map((p) => p.position);
const pol = primary.card.slice(0, n).map((p) => p.polishedLoad);
const four = primary.fourierBaseline.card.slice(0, n).map((p) => p.polishedLoad);
return [pos, pol, four];
})()}
options={{
width: 800,
height: 260,
scales: { x: { time: false } },
axes: [
{ label: "Position (m)", stroke: "#cbd5f5" },
{ label: "Load (N)", stroke: "#cbd5f5" }
],
series: [
{ label: "Position" },
{ label: "Polished (FDM)", stroke: "#f59e0b", width: 2 },
{ label: "Polished (Fourier)", stroke: "#a78bfa", width: 1.5, dash: [8, 4] }
],
legend: { show: true }
}}
/>
</div>
) : null}
</Fieldset>
{primary?.profiles && (
<Fieldset legend="Profiles (trajectory / side-load / friction)">
<div className="kpi-grid">
<Kpi label="Profile nodes" value={String(primary.profiles.nodeCount)} />
<Kpi
label="Max |side load| (N)"
value={
primary.profiles.sideLoadProfile.length > 0
? Math.max(
0,
...primary.profiles.sideLoadProfile.map((v) => Math.abs(v))
).toFixed(0)
: "—"
}
/>
<Kpi
label="Max |friction| (N)"
value={
primary.profiles.frictionProfile.length > 0
? Math.max(
0,
...primary.profiles.frictionProfile.map((v) => Math.abs(v))
).toFixed(0)
: "—"
}
/>
</div>
</Fieldset>
)}
{primary?.diagnostics &&
primary.diagnostics.chamberPressurePa.length > 0 &&
primary.diagnostics.gasFraction.length > 0 && (
<Fieldset legend="Diagnostics (valve / chamber / gas)">
<div className="kpi-grid">
<Kpi
label="Valve samples"
value={`${primary.diagnostics.valveStates.length} points`}
/>
<Kpi
label="Chamber P range (Pa)"
value={`${Math.min(...primary.diagnostics.chamberPressurePa).toFixed(0)}${Math.max(...primary.diagnostics.chamberPressurePa).toFixed(0)}`}
/>
<Kpi
label="Gas fraction range"
value={`${Math.min(...primary.diagnostics.gasFraction).toFixed(3)}${Math.max(...primary.diagnostics.gasFraction).toFixed(3)}`}
/>
</div>
</Fieldset>
)}
{result.fieldTraceability?.fields?.length ? (
<Fieldset legend="MVP field traceability (static map)">
<p className="panel-note">
Categories mirror <code>docs/engineering/field-traceability.md</code> (
physics, metadata, parseCalibration, payloadInactive, parsedUnused).
</p>
<div className="table-scroll" style={{ maxHeight: 280 }}>
<table className="data-table">
<thead>
<tr>
<th>XML field</th>
<th>Category</th>
<th>In file</th>
</tr>
</thead>
<tbody>
{result.fieldTraceability.fields.map((row) => (
<tr key={row.xmlKey} title={row.notes || undefined}>
<td className="mono">{row.xmlKey}</td>
<td>{row.category}</td>
<td>{row.presentInXml ? "yes" : "—"}</td>
</tr>
))}
</tbody>
</table>
</div>
</Fieldset>
) : null}
{result.comparison && (
<Fieldset legend="FDM vs FEA Comparison">
<div className="kpi-grid">