Files
rods/gui-ts/src/state/xmlImport.ts
Conner Majic 64e9d31373 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
2026-04-16 23:19:00 -06:00

170 lines
6.4 KiB
TypeScript

import type { ParsedCase } from "../types";
import {
EMPTY_CASE_STATE,
type CaseState,
type RawFieldValue,
type SurveyRow,
type TaperRow
} from "./caseModel";
/** Flatten xml2js node to its text content (preserves attr bag in '$' if present). */
function textOf(value: RawFieldValue): string {
if (value === undefined || value === null) return "";
if (typeof value === "string") return value;
if (typeof value === "object") {
const obj = value as Record<string, unknown>;
if (typeof obj._ === "string") return obj._;
if (Object.keys(obj).length === 1 && "$" in obj) return "";
}
return "";
}
function numberOf(value: RawFieldValue, fallback = 0): number {
const text = textOf(value).trim();
if (!text) return fallback;
const n = Number(text);
return Number.isFinite(n) ? n : fallback;
}
function stringOf(value: RawFieldValue, fallback = ""): string {
const text = textOf(value).trim();
return text || fallback;
}
function parseColonArray(value: RawFieldValue): number[] {
const text = textOf(value);
if (!text) return [];
return text
.split(":")
.map((piece) => piece.trim())
.filter((piece) => piece.length > 0)
.map((piece) => Number(piece))
.filter((n) => Number.isFinite(n));
}
/**
* Hydrate a CaseState from the `parsed` block returned by the solver-api.
* Values come primarily from `rawFields` (pre-normalization) so the GUI
* edits the XML-native units directly. `parsed.model` is used only as a
* fallback when `rawFields` lacks an entry.
*/
export function hydrateFromParsed(parsed: ParsedCase): CaseState {
const raw = (parsed.rawFields ?? {}) as Record<string, RawFieldValue>;
const model = parsed.model ?? ({} as ParsedCase["model"]);
const rawFieldOrder = Object.keys(raw);
const md = parseColonArray(raw.MeasuredDepthArray);
const inc = parseColonArray(raw.InclinationFromVerticalArray);
const azi = parseColonArray(raw.AzimuthFromNorthArray);
const surveyLen = Math.max(md.length, inc.length, azi.length);
const survey: SurveyRow[] = [];
for (let i = 0; i < surveyLen; i += 1) {
survey.push({
md: md[i] ?? 0,
inc: inc[i] ?? 0,
azi: azi[i] ?? 0
});
}
const diam = parseColonArray(raw.TaperDiameterArray);
const length = parseColonArray(raw.TaperLengthArray);
const modulus = parseColonArray(raw.TaperModulusArray);
const rodType = parseColonArray(raw.RodTypeArray);
const weight = parseColonArray(raw.TaperWeightArray);
const mts = parseColonArray(raw.TaperMTSArray);
const guideCounts = parseColonArray(raw.TaperGuidesCountArray);
const rgWeights = parseColonArray(raw.RodGuideWeightArray);
const rgTypePieces = textOf(raw.RodGuideTypeArray).split(":");
const rodGuideSlotCount = Math.max(rgTypePieces.length, rgWeights.length, 1);
/** Core rod-string stations (commercial XML often pads these together). */
const coreTaperLen = Math.max(diam.length, length.length, modulus.length, rodType.length);
const taperParallelSuffix = {
weights: weight.length > coreTaperLen ? weight.slice(coreTaperLen) : [],
mts: mts.length > coreTaperLen ? mts.slice(coreTaperLen) : [],
guidesCounts: guideCounts.length > coreTaperLen ? guideCounts.slice(coreTaperLen) : []
};
const taper: TaperRow[] = [];
for (let i = 0; i < coreTaperLen; i += 1) {
const gc = guideCounts[i];
const guidesEnabled = Number.isFinite(gc) && gc >= 0;
const tok = (rgTypePieces[i] ?? "").trim();
taper.push({
diameter: diam[i] ?? 0,
length: length[i] ?? 0,
modulus: modulus[i] ?? 0,
rodType: rodType[i] ?? 0,
weightLbfPerFt: weight[i] ?? 0,
mtsLbf: mts[i] ?? 0,
guidesEnabled,
guideCount: guidesEnabled ? Math.max(0, Math.round(gc)) : 0,
guideTypeToken: tok,
rodGuideWeightLbfPerFt: rgWeights[i] ?? 0
});
}
return {
...EMPTY_CASE_STATE,
wellName: stringOf(raw.WellName, model.wellName ?? ""),
company: stringOf(raw.Company, model.company ?? ""),
pumpDepth: numberOf(raw.PumpDepth, model.pumpDepth ?? 0),
tubingAnchorLocation: numberOf(raw.TubingAnchorLocation, model.tubingAnchorLocation ?? 0),
tubingSize: numberOf(raw.TubingSize, model.tubingSize ?? 0),
pumpingSpeed: numberOf(raw.PumpingSpeed, model.pumpingSpeed ?? 0),
pumpingSpeedOption: numberOf(raw.PumpingSpeedOption, model.pumpingSpeedOption ?? 0),
pumpingUnitId: stringOf(raw.PumpingUnitID, model.pumpingUnitId ?? ""),
survey,
taper,
pumpDiameter: numberOf(raw.PumpDiameter, model.pumpDiameter ?? 0),
pumpFriction: numberOf(raw.PumpFriction, model.pumpFriction ?? 0),
pumpIntakePressure: numberOf(raw.PumpIntakePressure, model.pumpIntakePressure ?? 0),
pumpFillageOption: numberOf(raw.PumpFillageOption, model.pumpFillageOption ?? 0),
percentPumpFillage: numberOf(raw.PercentPumpFillage, model.percentPumpFillage ?? 0),
percentUpstrokeTime: numberOf(raw.PercentageUpstrokeTime, model.percentUpstrokeTime ?? 50),
percentDownstrokeTime: numberOf(raw.PercentageDownstrokeTime, model.percentDownstrokeTime ?? 50),
waterCut: numberOf(raw.WaterCut, model.waterCut ?? 0),
waterSpecGravity: numberOf(raw.WaterSpecGravity, model.waterSpecGravity ?? 1),
fluidLevelOilGravity: numberOf(raw.FluidLevelOilGravity, model.fluidLevelOilGravity ?? 35),
tubingGradient: numberOf(raw.TubingGradient, model.tubingGradient ?? 0),
rodFrictionCoefficient: numberOf(
raw.RodFrictionCoefficient,
model.rodFrictionCoefficient ?? 0
),
stuffingBoxFriction: numberOf(raw.StuffingBoxFriction, model.stuffingBoxFriction ?? 0),
moldedGuideFrictionRatio: numberOf(
raw.MoldedGuideFrictionRatio,
model.moldedGuideFrictionRatio ?? 1
),
wheeledGuideFrictionRatio: numberOf(
raw.WheeledGuideFrictionRatio,
model.wheeledGuideFrictionRatio ?? 1
),
otherGuideFrictionRatio: numberOf(
raw.OtherGuideFrictionRatio,
model.otherGuideFrictionRatio ?? 1
),
sinkerBarDiameter: numberOf(raw.SinkerBarDiameter, 0),
sinkerBarLength: numberOf(raw.SinkerBarLength, 0),
rodGuideSlotCount,
taperParallelSuffix,
upStrokeDamping: numberOf(raw.UpStrokeDampingFactor, model.upStrokeDamping ?? 0),
downStrokeDamping: numberOf(raw.DownStrokeDampingFactor, model.downStrokeDamping ?? 0),
nonDimensionalFluidDamping: numberOf(
raw.NonDimensionalFluidDamping,
model.nonDimensionalFluidDamping ?? 0
),
unitsSelection: numberOf(raw.UnitsSelection, model.unitsSelection ?? 0),
rawFields: { ...raw },
rawFieldOrder
};
}