import { useMemo, useState } from "react"; 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 { buildTrajectorySegments } from "../../state/trajectoryMetrics"; import { Fieldset } from "../common/Fieldset"; import { UPlotChart } from "../common/UPlotChart"; import { Wellbore3DView } from "../common/Wellbore3DView"; import type { OverlayMode } from "../common/Wellbore3DView"; type Props = { result: SolveResponse | null; loading: boolean; error: string | null; lastRunAt: string | null; elapsedSeconds: number | null; caseState: CaseState; checks: EngineeringChecks; onNavigateTab?: (tabId: string) => void; }; type CardSeries = { position: number[]; polished: number[]; downhole: number[]; }; function toSeries(solver: SolverOutput | undefined): CardSeries | null { if (!solver || !solver.card?.length) return null; return { position: solver.card.map((p) => p.position), polished: solver.card.map((p) => p.polishedLoad), downhole: solver.card.map((p) => p.downholeLoad) }; } function formatKn(value: number | undefined): string { if (value === undefined || !Number.isFinite(value)) return "—"; return (value / 1000).toFixed(2); } function computePumpPlacement(caseState: CaseState) { const segments = buildTrajectorySegments(caseState.survey); const taperLen = caseState.taper.reduce((sum, row) => sum + Math.max(0, row.length), 0); if (!segments.length) { return { nearestStationIndex: -1, nearestStationMd: null as number | null, nearestStationDistance: null as number | null, surveyEndMd: null as number | null, surveyToPumpDelta: null as number | null, rodToPumpDelta: taperLen - caseState.pumpDepth }; } const stationMds = [segments[0].a.md, ...segments.map((s) => s.b.md)]; let nearestIdx = 0; let nearestDist = Math.abs(stationMds[0] - caseState.pumpDepth); for (let i = 1; i < stationMds.length; i += 1) { const d = Math.abs(stationMds[i] - caseState.pumpDepth); if (d < nearestDist) { nearestDist = d; nearestIdx = i; } } const surveyEndMd = stationMds[stationMds.length - 1]; return { nearestStationIndex: nearestIdx, nearestStationMd: stationMds[nearestIdx], nearestStationDistance: nearestDist, surveyEndMd, surveyToPumpDelta: surveyEndMd - caseState.pumpDepth, rodToPumpDelta: taperLen - caseState.pumpDepth }; } export function ResultsTab({ result, loading, error, lastRunAt, elapsedSeconds, caseState, checks, onNavigateTab }: Props) { const primary = result?.solver; const fea = result?.solvers?.fea ?? null; const fdm = result?.solvers?.fdm ?? primary ?? null; const [overlayMode, setOverlayMode] = useState("depth"); const [selectedSegment, setSelectedSegment] = useState(null); const [traceabilityQuery, setTraceabilityQuery] = useState(""); const dlsUnitLabel = caseState.unitsSelection === 1 ? "deg/100 m" : "deg/100 ft"; const trajectorySegments = useMemo(() => buildTrajectorySegments(caseState.survey), [caseState.survey]); const sideLoadProfile = primary?.profiles?.sideLoadProfile ?? null; const pumpDiag = useMemo(() => computePumpPlacement(caseState), [caseState]); const filteredTraceability = useMemo(() => { const fields = result?.fieldTraceability?.fields ?? []; const q = traceabilityQuery.trim().toLowerCase(); if (!q) return fields; return fields.filter((row) => { return ( row.xmlKey.toLowerCase().includes(q) || row.category.toLowerCase().includes(q) || row.notes.toLowerCase().includes(q) ); }); }, [result?.fieldTraceability?.fields, traceabilityQuery]); const dynacardData = useMemo(() => { const fdmSeries = toSeries(fdm ?? undefined); const feaSeries = toSeries(fea ?? undefined); if (!fdmSeries) return null; if (feaSeries && feaSeries.position.length === fdmSeries.position.length) { return [ fdmSeries.position, fdmSeries.polished, fdmSeries.downhole, feaSeries.polished, feaSeries.downhole ] as AlignedData; } return [fdmSeries.position, fdmSeries.polished, fdmSeries.downhole] as AlignedData; }, [fdm, fea]); const hasCardLengthMismatch = useMemo(() => { const fdmSeries = toSeries(fdm ?? undefined); const feaSeries = toSeries(fea ?? undefined); if (!fdmSeries || !feaSeries) return false; return fdmSeries.position.length !== feaSeries.position.length; }, [fdm, fea]); const dynacardOptions = useMemo(() => { const seriesCount = fea ? 5 : 3; const seriesSpec: Options["series"] = [ { label: "Position (m)" }, { label: "Polished (FDM)", stroke: "#f59e0b", width: 2 }, { label: "Downhole (FDM)", stroke: "#22d3ee", width: 2 } ]; if (seriesCount === 5) { seriesSpec.push({ label: "Polished (FEA)", stroke: "#f43f5e", width: 1.5, dash: [6, 3] }); seriesSpec.push({ label: "Downhole (FEA)", stroke: "#34d399", width: 1.5, dash: [6, 3] }); } return { width: 800, height: 320, scales: { x: { time: false } }, axes: [ { label: "Polished-rod position (m)", stroke: "#cbd5f5" }, { label: "Load (N)", stroke: "#cbd5f5" } ], series: seriesSpec, legend: { show: true } } satisfies Options; }, [fea]); return ( <> {loading && (
Running simulation…
)} {error && (
Error: {error}
)} {!result && !loading && !error && (
No results yet. Edit inputs and press Run Solver.
)} {!!checks.issues.length && (
    {checks.issues.map((issue) => (
  • {issue.severity.toUpperCase()}: {issue.message}
  • ))}
)}

Tubing trajectory is colored by measured depth (hue). Rod string is overlaid with a depth gradient, and pump location is marked in red.

Click a row or 3D segment to cross-highlight. DLS is shown numerically only ({dlsUnitLabel}).

{trajectorySegments.map((segment) => ( 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" }} > ))} {!trajectorySegments.length && ( )}
# MD start MD end ΔMD DLS ({dlsUnitLabel})
{segment.index + 1} {segment.a.md.toFixed(1)} {segment.b.md.toFixed(1)} {segment.dMd.toFixed(1)} {segment.dls.toFixed(2)}
No trajectory segments to display.
= 0 ? String(pumpDiag.nearestStationIndex + 1) : "—"} />
{result && ( <>
{hasCardLengthMismatch && (
FDM and FEA card lengths differ; chart overlay is limited to FDM series for this run.
)} {dynacardData ? ( ) : (

No card data in response.

)} {primary?.fourierBaseline && primary.fourierBaseline.card?.length ? (

Fourier baseline ({primary.fourierBaseline.harmonics} harmonics): RMS polished{" "} {primary.fourierBaseline.residualRmsPolished?.toFixed(1)} N · RMS downhole{" "} {primary.fourierBaseline.residualRmsDownhole?.toFixed(1)} N

{ 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 } }} />
) : null}
{primary?.profiles && (
0 ? Math.max( 0, ...primary.profiles.sideLoadProfile.map((v) => Math.abs(v)) ).toFixed(0) : "—" } /> 0 ? Math.max( 0, ...primary.profiles.frictionProfile.map((v) => Math.abs(v)) ).toFixed(0) : "—" } />
)} {primary?.diagnostics && primary.diagnostics.chamberPressurePa.length > 0 && primary.diagnostics.gasFraction.length > 0 && (
)} {result.fieldTraceability?.fields?.length ? (

Categories mirror docs/engineering/field-traceability.md ( physics, metadata, parseCalibration, payloadInactive, parsedUnused).

setTraceabilityQuery(e.target.value)} /> Showing {filteredTraceability.length} / {result.fieldTraceability.fields.length}
{filteredTraceability.map((row) => ( ))}
XML field Category In file Notes
{row.xmlKey} {row.category} {row.presentInXml ? "yes" : "—"} {row.notes}
) : null} {result.comparison && (
)} {(result.parseWarnings?.length || primary?.warnings?.length) && (
    {(result.parseWarnings ?? []).map((w, i) => (
  • {w}
  • ))} {(primary?.warnings ?? []).map((w, i) => (
  • {w}
  • ))}
)} {result.parsed.unsupportedFields?.length ? (

Preserved on export; not consumed by the solver.

                {result.parsed.unsupportedFields.join("\n")}
              
) : null} )} ); } function exportSvg(svgId: string, filename: string) { const svg = document.getElementById(svgId); if (!svg) return; const serializer = new XMLSerializer(); const source = serializer.serializeToString(svg); const blob = new Blob([source], { type: "image/svg+xml;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } function exportSvgToPng(svgId: string, filename: string) { const svg = document.getElementById(svgId) as SVGSVGElement | null; if (!svg) return; const serializer = new XMLSerializer(); const source = serializer.serializeToString(svg); const svgBlob = new Blob([source], { type: "image/svg+xml;charset=utf-8" }); const url = URL.createObjectURL(svgBlob); const img = new Image(); img.onload = () => { const vb = svg.viewBox.baseVal; const width = vb.width || svg.clientWidth || 800; const height = vb.height || svg.clientHeight || 400; const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); if (!ctx) { URL.revokeObjectURL(url); return; } ctx.drawImage(img, 0, 0); canvas.toBlob((blob) => { if (!blob) return; const pngUrl = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = pngUrl; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(pngUrl); }, "image/png"); URL.revokeObjectURL(url); }; img.src = url; } function exportSummaryJson( filename: string, result: SolveResponse | null, checks: EngineeringChecks, pumpDiag: ReturnType ) { const payload = { exportedAt: new Date().toISOString(), checks, pumpPlacement: pumpDiag, runMetadata: result?.runMetadata ?? null, comparison: result?.comparison ?? null, warnings: { parse: result?.parseWarnings ?? [], solver: result?.solver?.warnings ?? [] } }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } function Kpi({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); }