- 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
170 lines
6.4 KiB
TypeScript
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
|
|
};
|
|
}
|