fix: normalize engineering checks across units
Convert pump-depth vs rod-length mismatch gating to meter-equivalent comparisons, add imperial coverage, and correct rod material mapping with exported helper tests while refreshing related GUI/docs copy. Made-with: Cursor
This commit is contained in:
@@ -109,8 +109,7 @@ make smoke
|
|||||||
| **Pumping-unit kinematics** (Svinos tables, crank motion from `PumpingUnitID`) | Harmonic default; unit geometry unused |
|
| **Pumping-unit kinematics** (Svinos tables, crank motion from `PumpingUnitID`) | Harmonic default; unit geometry unused |
|
||||||
| **Inverse calibration** | Fit damping / friction to measured downhole card |
|
| **Inverse calibration** | Fit damping / friction to measured downhole card |
|
||||||
| **Fourier** analytical diagnostic | Optional `comparison.fourier` |
|
| **Fourier** analytical diagnostic | Optional `comparison.fourier` |
|
||||||
| **GUI** 3D survey + layout | **Partial done** — Results tab includes 3D projected wellbore + rod/pump overlay + DLS contour; not a full 3D engine (no camera controls / mesh terrain yet) |
|
| **GUI** 3D survey + layout | **Partial done** — Results tab includes 3D projected wellbore + rod/pump overlay, depth-hue and side-load overlays; not a full 3D engine (no camera controls / mesh terrain yet) |
|
||||||
| **GUI diagnostic workflow** | Tabbed UI ships predictive solve end-to-end; diagnostic requires surface-card upload path in Kinematics (calls `POST /solve/validate-card` + `POST /solve` with `workflow=diagnostic`) — not wired in this pass |
|
|
||||||
| **GUI Pump / Fluid / Kinematics first-class mapping** | Tabs render editable fields but rely on `rawFields` round-trip rather than dedicated serializer logic; audit once solver-api adds explicit fields for `PumpFillageOption`, pumping-unit kinematics, etc. |
|
| **GUI Pump / Fluid / Kinematics first-class mapping** | Tabs render editable fields but rely on `rawFields` round-trip rather than dedicated serializer logic; audit once solver-api adds explicit fields for `PumpFillageOption`, pumping-unit kinematics, etc. |
|
||||||
| **GUI fatigue / API RP 11BR table** | Backend does not emit a fatigue payload yet; surface in Results tab when `solver.fatigue` exists |
|
| **GUI fatigue / API RP 11BR table** | Backend does not emit a fatigue payload yet; surface in Results tab when `solver.fatigue` exists |
|
||||||
|
|
||||||
@@ -121,15 +120,12 @@ make smoke
|
|||||||
- Fixed engineering gate in Solver tab:
|
- Fixed engineering gate in Solver tab:
|
||||||
- run blocked when `|PumpDepth - sum(TaperLengthArray)| > 15 m`.
|
- run blocked when `|PumpDepth - sum(TaperLengthArray)| > 15 m`.
|
||||||
- survey MD monotonicity and minimum station-count checks.
|
- survey MD monotonicity and minimum station-count checks.
|
||||||
- Fixed DLS bad-section threshold:
|
|
||||||
- warnings + 3D contour use `15 deg/100` as the "bad section" limit.
|
|
||||||
- Results tab now shows:
|
- Results tab now shows:
|
||||||
- uPlot dynacard overlays,
|
- uPlot dynacard overlays,
|
||||||
- 3D projected wellbore with rod gradient and pump marker,
|
- 3D projected wellbore with rod gradient and pump marker,
|
||||||
- interactive 3D view controls (rotate, pan, zoom, perspective/orthographic toggle, reset),
|
- depth-hue and side-load risk overlays,
|
||||||
- highlighted bad-DLS segments,
|
- segment highlight + clear-highlight controls,
|
||||||
- trajectory analytics table with row↔3D segment cross-highlight,
|
- trajectory analytics table with row↔3D segment cross-highlight,
|
||||||
- side-load overlay mode (when `solver.profiles.sideLoadProfile` is available),
|
|
||||||
- pump-placement diagnostics panel + navigation actions,
|
- pump-placement diagnostics panel + navigation actions,
|
||||||
- export actions (3D SVG, 3D PNG, summary JSON).
|
- export actions (3D SVG, 3D PNG, summary JSON).
|
||||||
- keyboard-accessible trajectory segment selection (`Enter`/`Space`) and clear-highlight control.
|
- keyboard-accessible trajectory segment selection (`Enter`/`Space`) and clear-highlight control.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { App } from "./App";
|
|||||||
const DEFAULT_CASE = {
|
const DEFAULT_CASE = {
|
||||||
model: {
|
model: {
|
||||||
wellName: "PLACEHOLDER-WELL",
|
wellName: "PLACEHOLDER-WELL",
|
||||||
company: "Veren",
|
company: "Majic",
|
||||||
measuredDepth: [0, 100, 200],
|
measuredDepth: [0, 100, 200],
|
||||||
inclination: [0, 10, 20],
|
inclination: [0, 10, 20],
|
||||||
azimuth: [0, 90, 180],
|
azimuth: [0, 90, 180],
|
||||||
@@ -14,7 +14,7 @@ const DEFAULT_CASE = {
|
|||||||
},
|
},
|
||||||
rawFields: {
|
rawFields: {
|
||||||
WellName: "PLACEHOLDER-WELL",
|
WellName: "PLACEHOLDER-WELL",
|
||||||
Company: "Veren",
|
Company: "Majic",
|
||||||
PumpDepth: "1727",
|
PumpDepth: "1727",
|
||||||
PumpingSpeed: "5",
|
PumpingSpeed: "5",
|
||||||
UnitsSelection: "2",
|
UnitsSelection: "2",
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ const taperBase = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("engineering checks fixed thresholds", () => {
|
describe("engineering checks fixed thresholds", () => {
|
||||||
it("blocks run when pump depth and rod length mismatch exceeds 15 m", () => {
|
it("blocks run when pump depth and rod length mismatch exceeds 15 m (SI)", () => {
|
||||||
const state = {
|
const state = {
|
||||||
...EMPTY_CASE_STATE,
|
...EMPTY_CASE_STATE,
|
||||||
|
unitsSelection: 1,
|
||||||
pumpDepth: 1000,
|
pumpDepth: 1000,
|
||||||
taper: [{ ...taperBase, length: 980, rodType: 3 }],
|
taper: [{ ...taperBase, length: 980, rodType: 3 }],
|
||||||
survey: [
|
survey: [
|
||||||
@@ -31,6 +32,23 @@ describe("engineering checks fixed thresholds", () => {
|
|||||||
expect(checks.issues.some((i) => i.code === "PUMP_ROD_MISMATCH_15M")).toBe(true);
|
expect(checks.issues.some((i) => i.code === "PUMP_ROD_MISMATCH_15M")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not block run for small mismatch in imperial native units", () => {
|
||||||
|
const state = {
|
||||||
|
...EMPTY_CASE_STATE,
|
||||||
|
unitsSelection: 2,
|
||||||
|
pumpDepth: 1000,
|
||||||
|
taper: [{ ...taperBase, length: 980, rodType: 3 }],
|
||||||
|
survey: [
|
||||||
|
{ md: 0, inc: 0, azi: 0 },
|
||||||
|
{ md: 1000, inc: 0, azi: 0 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const checks = runEngineeringChecks(state);
|
||||||
|
expect(checks.hasBlockingError).toBe(false);
|
||||||
|
expect(checks.issues.some((i) => i.code === "PUMP_ROD_MISMATCH_15M")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("warns when survey ends shallower than pump depth", () => {
|
it("warns when survey ends shallower than pump depth", () => {
|
||||||
const state = {
|
const state = {
|
||||||
...EMPTY_CASE_STATE,
|
...EMPTY_CASE_STATE,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { CaseState } from "./caseModel";
|
import type { CaseState } from "./caseModel";
|
||||||
|
|
||||||
export const PUMP_ROD_MISMATCH_M = 15;
|
export const PUMP_ROD_MISMATCH_M = 15;
|
||||||
|
const FT_TO_M = 0.3048;
|
||||||
|
|
||||||
export type EngineeringIssue = {
|
export type EngineeringIssue = {
|
||||||
severity: "warning" | "error";
|
severity: "warning" | "error";
|
||||||
@@ -20,14 +21,19 @@ export function runEngineeringChecks(state: CaseState): EngineeringChecks {
|
|||||||
const rodTotal = activeTaper.reduce((acc, t) => acc + t.length, 0);
|
const rodTotal = activeTaper.reduce((acc, t) => acc + t.length, 0);
|
||||||
const pumpDepth = state.pumpDepth;
|
const pumpDepth = state.pumpDepth;
|
||||||
if (rodTotal > 0 && pumpDepth > 0) {
|
if (rodTotal > 0 && pumpDepth > 0) {
|
||||||
const diff = Math.abs(pumpDepth - rodTotal);
|
const depthScale = toMetersScale(state.unitsSelection);
|
||||||
if (diff > PUMP_ROD_MISMATCH_M) {
|
const pumpDepthM = pumpDepth * depthScale;
|
||||||
|
const rodTotalM = rodTotal * depthScale;
|
||||||
|
const diffM = Math.abs(pumpDepthM - rodTotalM);
|
||||||
|
if (diffM > PUMP_ROD_MISMATCH_M) {
|
||||||
issues.push({
|
issues.push({
|
||||||
severity: "error",
|
severity: "error",
|
||||||
code: "PUMP_ROD_MISMATCH_15M",
|
code: "PUMP_ROD_MISMATCH_15M",
|
||||||
message: `Pump depth (${pumpDepth.toFixed(1)}) and total rod length (${rodTotal.toFixed(
|
message: `Pump depth (${pumpDepth.toFixed(1)} native, ${pumpDepthM.toFixed(
|
||||||
1
|
1
|
||||||
)}) differ by ${diff.toFixed(1)} m (> ${PUMP_ROD_MISMATCH_M} m limit).`
|
)} m) and total rod length (${rodTotal.toFixed(1)} native, ${rodTotalM.toFixed(
|
||||||
|
1
|
||||||
|
)} m) differ by ${diffM.toFixed(1)} m (> ${PUMP_ROD_MISMATCH_M} m limit).`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,3 +74,7 @@ export function runEngineeringChecks(state: CaseState): EngineeringChecks {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toMetersScale(unitsSelection: number): number {
|
||||||
|
return unitsSelection === 1 ? 1 : FT_TO_M;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -225,7 +225,9 @@ export function App() {
|
|||||||
|
|
||||||
<footer className="app-statusbar">
|
<footer className="app-statusbar">
|
||||||
<span>{statusMessage}</span>
|
<span>{statusMessage}</span>
|
||||||
<span>Company: {store.state.company || "—"}</span>
|
<span>
|
||||||
|
Well: {store.state.wellName || "—"} · Company: {store.state.company || "—"}
|
||||||
|
</span>
|
||||||
<span>Taper sections: {store.state.taper.filter((t) => t.length > 0).length}</span>
|
<span>Taper sections: {store.state.taper.filter((t) => t.length > 0).length}</span>
|
||||||
<span>Survey stations: {store.state.survey.length}</span>
|
<span>Survey stations: {store.state.survey.length}</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ export function KinematicsTab({
|
|||||||
<>
|
<>
|
||||||
<Fieldset legend="Pumping Unit / Surface Motion">
|
<Fieldset legend="Pumping Unit / Surface Motion">
|
||||||
<p className="panel-note">
|
<p className="panel-note">
|
||||||
Drives the polished-rod boundary condition via pumping speed (SPM).
|
Drives the polished-rod boundary condition via pumping speed (SPM). For measured-card
|
||||||
Diagnostic workflow with measured surface-card upload is planned for
|
diagnostics, paste card data below, validate it, then switch Solver workflow to
|
||||||
a future pass (see <code>solver-api POST /solve/validate-card</code>).
|
<code> diagnostic</code> before running.
|
||||||
</p>
|
</p>
|
||||||
<Row label="Pumping Speed (SPM)" htmlFor="pumpingSpeed">
|
<Row label="Pumping Speed (SPM)" htmlFor="pumpingSpeed">
|
||||||
<NumberField
|
<NumberField
|
||||||
|
|||||||
@@ -53,12 +53,14 @@ export function deriveTrajectoryFrictionMultiplier(model) {
|
|||||||
|
|
||||||
function rodTypeToProps(typeCode) {
|
function rodTypeToProps(typeCode) {
|
||||||
const t = Math.round(typeCode);
|
const t = Math.round(typeCode);
|
||||||
if (t === 2) {
|
if (t === 3) {
|
||||||
return { E: FIBERGLASS_E, rho: FIBERGLASS_RHO };
|
return { E: FIBERGLASS_E, rho: FIBERGLASS_RHO };
|
||||||
}
|
}
|
||||||
return { E: STEEL_E, rho: STEEL_RHO };
|
return { E: STEEL_E, rho: STEEL_RHO };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { rodTypeToProps };
|
||||||
|
|
||||||
function buildRodNodes(model) {
|
function buildRodNodes(model) {
|
||||||
const nx = 48;
|
const nx = 48;
|
||||||
const nodes = nx + 1;
|
const nodes = nx + 1;
|
||||||
|
|||||||
26
solver-api/tests/solverClient.test.js
Normal file
26
solver-api/tests/solverClient.test.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { rodTypeToProps } from "../src/solverClient.js";
|
||||||
|
|
||||||
|
describe("solverClient rod type mapping", () => {
|
||||||
|
it("maps fiberglass rod type 3 to fiberglass properties", () => {
|
||||||
|
expect(rodTypeToProps(3)).toEqual({
|
||||||
|
E: 5.5e9,
|
||||||
|
rho: 1900
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps sinker and steel rod types to steel-like properties", () => {
|
||||||
|
expect(rodTypeToProps(2)).toEqual({
|
||||||
|
E: 2.05e11,
|
||||||
|
rho: 7850
|
||||||
|
});
|
||||||
|
expect(rodTypeToProps(0)).toEqual({
|
||||||
|
E: 2.05e11,
|
||||||
|
rho: 7850
|
||||||
|
});
|
||||||
|
expect(rodTypeToProps(1)).toEqual({
|
||||||
|
E: 2.05e11,
|
||||||
|
rho: 7850
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user