WellSim Manual
Nodal analysis, minimum connected reserves, and production forecasting for oil & gas wells — a zero-dependency web port of the field-proven M. El-Ashry Excel toolset, with the macros replaced by deterministic solvers and every formula pinned against the original workbook cells.
What’s built
| Module | Oil | Gas |
|---|---|---|
| 1 — Well model (nodal analysis) | Natural ✓ · Gas lift ✓ · ESP full stack ✓ (68-pump database, matching, design proof) · Water Well tab ✓ (producer + injector) | Natural ✓ |
| 2 — Reserve estimate | ✓ Havlena–Odeh MB · static-gauge MB · reservoir limit | ✓ p/Z prod data · p/Z from SITHP · reservoir limit |
| 3 — Forecast | ✓ Tarner (saturation-tracking MB + nodal) | ✓ p/Z tank + nodal, history overlay |
Project-wide principles: Darcy IPR is the default and dominant J for both fluids (the test J calibrates a matched permeability at your judged skin); two pressure records (Pri immutable anchor, Pr working); explicit physics only — Brill & Beggs Z everywhere, no goal-seek; well-head temperature always calculated (Ramey); gas impurities are inputs wherever gas properties appear.
Run & test
node src/server/server.js # serves http://localhost:3355 node --test # 167 tests node scripts/validation-sweep.mjs # 43 module-vs-workbook sensitivity cases
The server uses only Node built-ins; the UI is plain HTML/JS (Plotly for charts). Documentation lives in the repo: README.md, docs/user-guide.md, docs/equations.md, docs/deploy.md. A standalone portable build (WellSim.exe, no install, cases saved beside the exe) is distributed separately as WellSim-1.0.zip.
UI conventions
- Selection modules: where the workbook had parallel sheets, one radio selection is active and the rest hide (lift type, IPR basis, module, reserve pressure-source).
- Side tables beside each chart hold the plotted numbers with a Copy button (tab-separated, pastes into Excel).
- Production tables accept clipboard paste (button or Ctrl+V in a cell); column order
Date, FTHP, rate, CGR|GOR, WGR|WC, Pwf; header lines auto-skip; up to 200 rows. - Dates:
17-Nov-14,17-Nov-2014 13:00:00,05/03/2014 06:30, ISO, or a day serial. Impossible dates are rejected with the row number — nothing silently rolls over. dt = days from the first row; time-of-day honoured. - Header bar (final layout): the Oil Well | Water Well | Gas Well tabs, then Save as · Open · Print report · Sign in · Help.
- Save as / Open: the whole case — every input, selection and production table across all three tabs — saves to a JSON file and restores in one click; program-filled grey cells stay program-filled.
- Print report: prints the results only — charts, tables, summary cards and banners on a clean white page (inputs, navigation and buttons hide); use the browser dialog’s “Save as PDF” for a shareable report.
- Company case database (Sign in, header): register a company account (username + password, scrypt-hashed on the server) to save cases server-side under
data/cases/<company>/— every user of a company shares its case list (save / load / delete). The free version stays: every calculation works without an account; only the server case store needs one. Sessions are in-memory (a server restart signs everyone out); put TLS in front for internet deployment. - Mobile: below 640 px the layout stacks and the action button stays pinned.
Oil — Well model
Lift types: Natural flow · Gas lift · ESP (one active).
Defaults
| Group | Fields (default) |
|---|---|
| Well & flow | FTHP 700 psi · rate 2100 stb/d · WC 50% · GOR 5000 scf/stb · ID 2.992 in · roughness 0.00006 |
| Trajectory | top perf 2810 mAH · kick-off 1910 m · deviation 7° |
| Fluids & PVT | 46 API · γg 0.842 · Rsi 700 scf/stb · Tres 201°F · μ tubing 6 cp · water SG 1.05 · Pb blank = calc |
| Heat transfer | soil 90°F · U 3 BTU/hr·ft²·°F · OD 3.5 in · Cp 0.51 |
| IPR (Darcy) | Pri 3550 · K 50 mD · H 42.653 ft · Re 1640.5 · Rw 0.5104 · skin 0 |
| Test | 2100 stb/d @ 700 psi · Pwf blank = get Pwf |
| Gas lift | injection 2490.92 mTVD · sweep to 2 MMscf/d in 10 steps |
| ESP | pump 2985 mTVD · ΔP 1325.16 psi (manual) · tubing gas blank = formation · selecting ESP swaps the GOR default 5000→300 scf/stb (back on natural/gas lift) |
Workflow
- Solve well — IPR + VLP, operating point (rate, Pwf, calculated WHT), AOF, well-head PQ curve with WHT on the second axis.
- Calibrate from test — get Pwf marches the test Pwf from FTHP if blank; the Jones/test J is computed Pri-anchored; you judge skin (guidance table by drilling/completion method) and the program back-solves the matched K so J(Darcy) = J(test).
- Sensitivities — VLP parameter sets (blank = base) and future pressures (0.75/0.5/0.25 × Pr) with the future-J chain.
- Gas-lift performance — injection sweep, optimum, incremental dQ/dInj.
- ESP — the full pump stack: 68-pump database in the background (WD/WG/WE/FLEX/ESP-B) with add-new-pump (custom per-stage curve) and Manual ΔP fallback; affinity scaling 30–60 Hz with the down-thrust/BEP/up-thrust envelope; intake free-gas block with the default-ON gas separator (95%, “Separator required” above 10% free gas, tubing GLR cut by the BE74 fraction); the coupled solve closes ΔP→march→intake→head→ΔP and matches the bottom-up traverse (Pwf from the IPR at CONSTANT Pres) to the top-down one at the intake — PI is the IPR match factor. Match stages (first run, new pump, wear 0) solves the installed stage count with a DESIGN PROOF: all stage-dependent march pressures must stay above the design floor (input, default 300 psi) — the minimum binds at the pump intake, and a match that would starve it is capped at the stage count where intake = floor; Match wear + PI from measured Pint/Pdis (validation: recovers the workbook’s PI 2.7 as 2.698). Two ESP views: Model match with the FINAL charts (IPR vs the coupled ESP-VLP nodal plot, wellhead PQ & WHT, PumpCurve with the results block beside, and the Traverse on its own row with measured markers — also shown in Manual-ΔP mode, where the input ΔP appears as the highlighted step at pump depth) and Sensitivity — future-Pres cases (Darcy future J), each FULLY solved: the chart shows every case’s IPR with its solved node starred, and the table lists the ESP data at each node (rate, Pwf, Pint, Pdis, ΔP, head, Qg@pump, gradient, free gas %, WHT, thrust).
Oil — Reserve estimate
Prod data & macro
Per-row Pwf (input-or-march) → Pr by composite-Vogel inversion iterated with J_2 → Havlena–Odeh MB. Headline N = AVERAGE(F/Eo); slope of F vs Eo as cross-check; F–Eo crossplot + N vs Np charts.
Static Pres history
Measured memory-gauge pressures typed directly (Date | Pres) — no IPR/VLP anywhere. Np/Gp from the prod-data cumulative. The strongest data when surveys exist.
Reservoir limit
m = −slope(Pwf, t), Ct = Cg·Sg+Co·So+Cw·Sw+Cf, STOIP = q̄/(Ct·m). Defaults Sg 0.1 / So 0.8 / Sw 0.15. Early lower bound.
Gas — Well model
Defaults
| Group | Fields (default) |
|---|---|
| Well & flow | FTHP 1625 psi · 14.137 MMscf/d · CGR 57.436 · WGR 3.846 stb/MMscf · ID 2.992 in · base roughness 0.0021 in |
| Trajectory | top perf 3013 mAH · kick-off 690 m · deviation 23.65° |
| Fluids & PVT | cond. 48.7 API · γg 0.763 · N₂ 1.2% · CO₂ 3% · H₂S 2 ppm · Tres 232°F · μcond 2 cp · σ 30 dyn/cm |
| Reservoir | Pri 3800 psi · Pr blank = Pri |
| IPR (Darcy) | K 5 mD · H 80 ft · Re 1640.5 · Rw 0.5104 · skin 0 |
| Multi-rate test | 2440/5.192 · 2000/10.002 · 1625/14.137 (psi / MMscf/d), Pwf blank = get Pwf |
IPR basis (one active): Darcy Pr² (default, dominant) or C & n (grey = fitted from the test). Calibration fits C&n, computes the test J, and matches Darcy K at your judged skin. Impurities feed the sour pseudo-critical route everywhere.
Gas — Reserve estimate
Prod data & macro
Per-row Pwf (input-or-march) → Pr = √(1000q/J + Pwf²) → explicit Z → Gp trapezoid → p/Z vs Gp line → minimum connected GIIP.
Pres from SITHP
Static shut-in surveys; Pres by the static gas march (gas-head-only stations, geothermal T, per-station Z — 1.2% vs the workbook case). Gp from prod cumulative; no IPR/VLP.
Reservoir limit
GIIP = q̄/(Ct·m)/1000. Defaults Sg 0.85 / So 0 / Sw 0.15; Cg grey = from two Bg points; slim table + Pwf-decline chart.
Oil — Forecast (Tarner)
Saturation-tracking material-balance forecast (the workbook Tarner sheet). N and the start state (date, Np, Pres) are grey input-or-calculated, chained off the Reserve module. Per step the two sheet residuals are solved deterministically: pressure closes the gas balance (Gp by GOR trapezoid = Gp from MB), saturation closes So = (1−Swi)(N−Np)Bo/(N·Boi(1−ctΔp)); rel-perm from the sheet’s Kro(So)/Krg(Sg) polynomials; GOR = Rs + (Krg/Kro)(μoBo/μgBg); rate qo = J1·(Kro/μo)/Bo·(P−Pwf) with J1 = 0.00708Kh/(ln(Re/Rw)−0.75+S). Forecast Pwf source (one active): nodal at forecast FTHP (VLP-coupled — the author’s commented-out intent, default) or fixed minimum Pwf (the sheet’s active behavior). Defaults: step 30 d · FTHP 150 psi · forecast W.C 0 · min Pwf 500 · Swi 0.15 · Cw 2.63e-6 · Cf 3.25e-6 · abandonment 50 stb/d. Deviations: PVT stepped at the NEAREST SOLVED Pres — each step evaluates Rs/Bo/Bg/μ once at the previous step’s converged pressure and holds them through the solve (user directive; the saved sheet froze them at initial values); Brent/alternation to convergence instead of 5 GoalSeek loops; the rate stays coupled to the trial pressure within each step, exactly like the sheet’s K/M columns under GoalSeek. Chart: history + forecast (rate, GOR, Pres) on a calendar axis, table below.
Gas — Forecast
p/Z tank coupled to the nodal model, chained off the end of history: start date / start Gp / start Pres are grey input-or-calculated (last prod date, cumulative, minimum solved Pres). Defaults: step 30 d · FTHP 300 psi · plateau 12 MMscf/d · abandonment 1 MMscf/d · max 60 steps · GIIP & pᵢ/Zᵢ grey = from the reserve fit.
Each step: Pr from the p/Z line (first step anchors on start Pres) → Darcy IPR at Pr → nodal operating point at forecast FTHP → q = min(op, plateau) → Gp advances. Terminates on abandonment / depletion / dead well / max steps, reporting EUR and recovery %. The chart overlays history + forecast on a calendar-date axis; the results table sits below it.
Constants & conversions
| Constant | Value |
|---|---|
| scf → m³ | 0.02831684639 |
| bbl → m³ | 0.158987304 |
| Water density | 998.9926968 kg/m³ ≡ 62.36509524 lb/ft³ |
| psi → Pa | 1 / 0.000145037738 |
| π (trajectory cosine) | 22/7 (Excel legacy, preserved) |
| π (areas) | 3.14 |
| Pb calibration constant | 2.1045604254721 |
Oil PVT (Standing-metric family)
With TC = (T−32)·5/9, ρₒ = 141.5/(131.5+API)·998.9926968 kg/m³, rs in m³/m³.
Pb [psi] = 125·[ (582·rsi/γg)^0.83 · 10^(0.00164·Tc − 1768/ρo) − 1.4 ] / 14.5 · 2.1045604254721 Rs(P<Pb) : rs = (γg/582) · [ (8e-6·P_Pa + 1.4) · 10^(1768/ρo − 0.00164·Tc) ]^(1/0.83); Rs = Rsi above Pb Bo_sat = 0.9759 + 12e-5 · (177·rs·√(γg/ρo) + 2.25·Tc + 40)^1.2 Bo(P>Pb) = e^(3e-6·(Pb−P)) · Bo_sat(Rsi) μ_od (Glaso) = 3.141e10 · T_F^(−3.444) · (log10 API)^a , a = 10.313·log10(T_F) − 36.447 Beggs–Robinson : A = 10.715·(Rs+100)^−0.515 , B = 5.44·(Rs+150)^−0.338 , μo = A·μod^B Vasquez–Beggs P>Pb: μ = μob·(P/Pb)^m , m = 2.6·P^1.187·10^(−3.9e-5·P − 5)
Gas PVT
Sweet pcrits (oil family): Tpc = 169 + 314·γg Ppc = 708.75 − 57.7·γg Sour route (gas wells) : γhc = (γg − 0.9672·yN2 − 1.5195·yCO2 − 1.1762·yH2S)/(1 − Σy) Tc_hc = 187 + 330·γhc − 71.5·γhc² Pc_hc = 706 + 51.7·γhc − 11.1·γhc² Kay mixing (+227.3/547.6/672.4 T; +493/1071/1306 P) + Wichert–Aziz ε correction
Brill & Beggs Z (explicit — the ONLY Z model): A = 1.39·(Tpr−0.92)^0.5 − 0.36·Tpr − 0.101 B = (0.62−0.23·Tpr)·Ppr + (0.066/(Tpr−0.86) − 0.037)·Ppr² + 0.32/10^(9(Tpr−1))·Ppr⁶ C = 0.132 − 0.32·log10(Tpr) D = 10^(0.3106 − 0.49·Tpr + 0.1824·Tpr²) Z = A + (1−A)/e^B + C·Ppr^D
μg = μ_base(γg,T)/Tpr · exp( Dempsey poly16(Ppr,Tpr) ) (a0 = −2.462 … a11 = 0.00441, sheet column V) ρg = 28.97·γg·P / (Z·10.73·(T+460)) [lb/ft³] b (march) = 0.0283·Z·(T+460)/(P+14.5) Bg (res. limit) = 0.00504·5.61·(T+460)·Z/P [cf/scf] Bg (oil MB) = 0.0283·Z·(T+460)/(P+14.5)/5.615 [bbl/scf]
Quirk preserved: the oil family anchors Tpr at station 1; the gas march uses local Tpr.
Trajectory, temperatures & marches
TVD = kickoff + (AH − kickoff)·cos(θ·(22/7)/180) Ramey WHT (always calculated): K10 = (OD_ft·3.14·U)/((w/24)·Cp) K11 = e^(−K10·L₁) (L₁ = FIRST station spacing — sheet quirk) T_shelf(x) = T_soil + (T_res−T_soil)·TVD(x)/TVD_tot bottom-up: T(i) = T_shelf(i) + (T(i+1) − T_shelf(i))·K11
Chen friction (Fanning): 1/√f = −4·log10[ ε/3.7065 − (5.0452/Re)·log10( ε^1.1098/2.8257 + (7.149/Re)^0.8981 ) ] dP_fric/dx = (1/144)·f·w²/(7.413e10·d_ft⁵·ρ_mix)·matchFriction Head acts on ΔTVD, friction on ΔAH — explicit Euler, 25–30 stations, no iteration.
Oil march (modified Griffith) — one march for natural / gas lift / ESP:
E_L = 1 − 0.38·(1 + v_m/0.8 − √disc) (Griffith, author's 0.38) Ashry head factor (applied to the hydrostatic term): f_WC = 7.27117418e-6·WC² − 1.19359442e-3·WC + 1.00062546 GOR≤400 : f_GOR = −6.59987105e-7·GOR² + 3.56896045e-4·GOR + 1.02495053 400–5000 : f_GOR = 1.97914840e-8·GOR² − 1.61983769e-4·GOR + 1.09560265 (capped at 5000) F_head = (f_GOR·f_WC)^0.82
Segments: natural 29 stations; gas lift 15+14 around the injection point; ESP 26 + pump node + 2 (ΔP at the pump, separated tubing gas above, back-march for intake with properties at max(P, 100)).
Gas march (modified Gray) — Gray holdup with condensate + water loading, effective-roughness friction (base 0.0021 in), local Tpr. Bit-parity pins: BHP!D51 = 3598.66511095252 @ 1e-8; three real get Pwf points (3414.02 / 2913.97 / 2647.82 psi).
IPR
Oil — composite Vogel (q_gross; q_oil = q_gross·(1−WC/100)): Pr>Pb, Pwf≥Pb : q = J·(Pr − Pwf) Pr>Pb, Pwf<Pb : q = J·(Pr − Pb) + (J·Pb/1.8)·(1 − 0.2·Pwf/Pb − 0.8·(Pwf/Pb)²) Pr≤Pb : q = (J·Pr/1.8)·(1 − 0.2·Pwf/Pr − 0.8·(Pwf/Pr)²) Darcy J (dominant): J = 0.00708·K·h / [ μo·Bo·(ln(Re/Rw) − 0.75 + S) ] Calibrate: user judges skin → closed-form matched K so J_Darcy = J_test (Pri-anchored) Future J (J_2x): μo·Bo re-evaluated at the future Pr, with Beggs–Robinson A/B at the CURRENT-Pr Rs (sheet quirk). Pins: J_21–23 = 5.27146844059 / 5.8313895807703 / 6.78094220552862
Gas: Darcy Pr² : q [MMscf/d] = J·(Pr² − Pwf²)/1000 J_test = 1000·q/(Pri² − Pwf²) From K : J = 703e-6·K·h / [ μg·Z·(T+460)·(ln(0.472·Re/Rw) + S) ] C & n : q = C·(Pr² − Pwf²)^n (log-log least squares) Pr from test: Pr = √(1000·q/J + Pwf²) | Pr = √((q/C)^(1/n) + Pwf²)
Multi-layer (optional): per-layer rates at common Pwf with crossflow warnings; equivalent record gas-exact (J_t = ΣJᵢ, Pr₀ = √(ΣJᵢPrᵢ²/ΣJᵢ)); oil Pr-average solves ΣQᵢ = 0.
Nodal analysis
Operating point: Brent root of R(q) = VLP(q) − IPR(q), highest-rate crossing (stable branch) Well-head curve: WHP(q) = Pwf_IPR(q) − Pwf_VLP(q) + FTHP, with calculated WHT per rate get_Pwf: any blank test/production Pwf is marched from its FTHP at its own rate and ratios
Reserves
GAS — p/Z (selections 1 & 2): Pr per row (closed form) → Z → Gp trapezoid → least-squares p/Z vs Gp GIIP [Bscf] = −(p/Z)ᵢ / slope (minimum connected) SITHP statics: gas-head-only station march, geothermal T, per-station Z GAS & OIL — reservoir limit (selection 3): m = −slope(Pwf vs t) over ALL rows Cg = (Bg₁ − Bg₂)/Bg₁/(P₂ − P₁) (first & last rows; input-or-calculated) Ct = Cg·Sg + Co·So + Cw·Sw + Cf GIIP [Bscf] = q̄/(Ct·m)/1000 STOIP [MMstb] = q̄/(Ct·m)/1e6
OIL — Havlena–Odeh (selections 1 & 2, solution-gas drive → minimum STOIIP): Pr per row: composite-Vogel inversion fixed-point iterated with J_2 (macro loops 5×) Np, Gp trapezoids (produced gas: Rsi above Pb, row GOR below); Rp = Gp/Np F = Np·( Bo + (Rp − Rs)·Bg ) Eo = Bo + (Rsi − Rs)·Bg − Boi Headline N = AVERAGE(F/Eo) over rows with Np > 0 (sheet AD1) Cross-check N = SLOPE(F vs Eo) incl. the exact (0,0) anchor (sheet V1) Selection 2 evaluates the same MB at MEASURED gauge pressures — no IPR/VLP.
Forecast (gas)
start: date = last prod date, Gp = cumulative, Pres = MIN solved Pres (all overridable)
per Δt: (p/Z)_target = (p/Z)ᵢ·(1 − Gp/GIIP)
Pr = start Pres (first step) | Brent-invert P/Z(P) = target
Darcy IPR at Pr → nodal op at forecast FTHP → q = min(q_op, plateau)
Gp += q·Δt/1000
stops: abandonment | depleted | died | max-steps → EUR, recovery %
Documented deviations from the workbooks
- Z factor: the sheets GoalSeek Hall–Yarborough (left unconverged in places); WellSim is explicit Brill & Beggs. Station parity pinned at 1e-9 with sheet Z injected; march drift bands: natural <2%, gas-lift <5.5%, ESP <3–5%; gas march bit-exact.
- Solvers: Brent replaces GoalSeek and the forecast’s quadratic-LSQ intersection — same intent, exact crossing.
- Forecast plateau Pwf: reported at the produced (constrained) rate; the sheet’s unconstrained-intersection quirk is not replicated.
- Pressure update: exact p/Z inversion instead of the one-step-lag Z estimate.
- Oil reservoir-limit Cg anchors: first & last rows (sheet pinned rows 5 & 14) — near-immaterial for oil Ct.
- WHT: always calculated (Ramey); the input-THT path was removed by design decision.
- Rounding artifacts preserved where parity needs them: π = 22/7 trajectory cosine; the
rouhgscartifact available as a test override.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| “unparseable date row N” | Fix that row; nothing was computed. Use d-MMM-yy, dd/mm/yyyy hh:mm:ss, ISO, or a day number. |
| “no depletion signal” | The fitted p/Z (or MB Eo) is not declining — more data spread needed, or the volume genuinely is not closing. |
| Huge oil MB N | Undersaturated path + window too long. Use the early-production window (days–weeks) or trust the static-gauge selection. |
| Gas march error: CGR + WGR > 0 | Gray’s liquid terms divide by liquid rate — use a small CGR (0.1 stb/MMscf) for essentially dry gas. |
| Reserve/forecast uses the wrong well | Both always read the current Well-model inputs — calibrate the well model first. |