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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user