WellSim Manual · user guide & equations reference ← Open the app

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.

167 regression tests passing 43/43 sheet-parity sensitivities gas march bit-exact vs workbook port 3355 · zero npm dependencies

What’s built

ModuleOilGas
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

1625 Input — you type it
2059.9 Input-or-calculated — blank = program fills it (grey); typing reclaims it
3511.3 Output — read-only (the workbook’s pink cells)

Oil — Well model

Lift types: Natural flow · Gas lift · ESP (one active).

Water Well tab — a dedicated tab beside Oil Well and Gas Well: the oil-tab structure without the Reserve/Forecast modules (lift types, get-Pwf calibration and sensitivities included), running the same modified-Griffith march at its limiting case: API fixed at 10° (SG = 1.000 exactly), w.c. 100 %, GOR = Rsi = 0, Pb = 0. Rates are gross water (bbl/d); the IPR is built in as the pure linear Darcy form (no Vogel curvature) with the Darcy J on water properties (μ = 0.5 cp, Bw = 1); future-pressure IPR sensitivities keep J constant. Defaults: FTHP 200 psi (producer) · 2000 bbl/d · Pri 4800 psi. Head-dominated: it flows naturally only when Pr beats THP + the static water column (≈ 0.446 psi/ft at SG 1.05) — otherwise “no-intersection” is the physics, not an error.
Injector — the Water Well tab has a Producer | Injector selector. The injector reverses the march (water flows DOWN: BHIP = injection THP + head − friction) — selecting it swaps the THP default from 200 to 2000 psi (and back on Producer) — and relaxes temperatures top-down from the injection-water temperature (input, default 90°F) toward geothermal — the bottomhole injection temperature (BHT) is the calculated output. Injectivity IPR: q = J·(Pwf − Pr) with the same water Darcy J; operating point = available BHIP (falls with rate) crossing Pr + q/J (rises). Charts: injectivity nodal + injection-THP-required vs rate with BHT on the second axis. Calibration from an injection test (BHIP input-or-marched → J injectivity → matched K). Lift and sensitivities hide for the injector; when THP + head cannot reach Pr the THP deficit is reported. Laminar Fanning 16/Re below the transition, Chen above.

Defaults

GroupFields (default)
Well & flowFTHP 700 psi · rate 2100 stb/d · WC 50% · GOR 5000 scf/stb · ID 2.992 in · roughness 0.00006
Trajectorytop perf 2810 mAH · kick-off 1910 m · deviation 7°
Fluids & PVT46 API · γg 0.842 · Rsi 700 scf/stb · Tres 201°F · μ tubing 6 cp · water SG 1.05 · Pb blank = calc
Heat transfersoil 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
Test2100 stb/d @ 700 psi · Pwf blank = get Pwf
Gas liftinjection 2490.92 mTVD · sweep to 2 MMscf/d in 10 steps
ESPpump 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

  1. Solve well — IPR + VLP, operating point (rate, Pwf, calculated WHT), AOF, well-head PQ curve with WHT on the second axis.
  2. 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).
  3. Sensitivities — VLP parameter sets (blank = base) and future pressures (0.75/0.5/0.25 × Pr) with the future-J chain.
  4. Gas-lift performance — injection sweep, optimum, incremental dQ/dInj.
  5. 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).
Multi-layer IPR (optional, oil & gas) — a Layers selector (Single default | Multi-layer): per-layer K/H/skin/Pr (Re/Rw & Pb shared) + per-layer fluids (blank = base), each layer’s Darcy J with its own μ·B (oil) / μ·z (gas) at the layer’s Pr. Layers collapse to one final J (training deck 4): oil via the theoretical average pressure + solution-point match; gas exactly (J_t = ΣJᵢ, PrAvg = √(ΣJᵢPrᵢ²/ΣJᵢ)). Blended ratios drive the marches; a Layers-@-operating-Pwf table shows each layer’s contribution with crossflow flagged. Darcy basis only (C&n has no exact collapse). Fit to test: with Multi-layer active, Calibrate also fits the commingled system — every layer K is scaled by one closed-form factor λ = J(test)/J(total) so the total J lands exactly on the Jones/test J (layer K ratios preserved; solved K’s written back into the table). Sensitivities stay single-layer.

Oil — Reserve estimate

1
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.

2
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.

3
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.

Keep the window early and short. This is an early-production method — on an undersaturated pressure path Eo is compressibility-only, and years-apart rows legitimately blow N up. Demo data (days apart) gives 14.7 / 91.9 / 1.5 MMstb across the three selections — the spread is the tool exposing input uncertainty; judge each method by its own inputs.

Gas — Well model

Defaults

GroupFields (default)
Well & flowFTHP 1625 psi · 14.137 MMscf/d · CGR 57.436 · WGR 3.846 stb/MMscf · ID 2.992 in · base roughness 0.0021 in
Trajectorytop perf 3013 mAH · kick-off 690 m · deviation 23.65°
Fluids & PVTcond. 48.7 API · γg 0.763 · N₂ 1.2% · CO₂ 3% · H₂S 2 ppm · Tres 232°F · μcond 2 cp · σ 30 dyn/cm
ReservoirPri 3800 psi · Pr blank = Pri
IPR (Darcy)K 5 mD · H 80 ft · Re 1640.5 · Rw 0.5104 · skin 0
Multi-rate test2440/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

1
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.

2
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.

3
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

ConstantValue
scf → m³0.02831684639
bbl → m³0.158987304
Water density998.9926968 kg/m³ ≡ 62.36509524 lb/ft³
psi → Pa1 / 0.000145037738
π (trajectory cosine)22/7  (Excel legacy, preserved)
π (areas)3.14
Pb calibration constant2.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

  1. 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.
  2. Solvers: Brent replaces GoalSeek and the forecast’s quadratic-LSQ intersection — same intent, exact crossing.
  3. Forecast plateau Pwf: reported at the produced (constrained) rate; the sheet’s unconstrained-intersection quirk is not replicated.
  4. Pressure update: exact p/Z inversion instead of the one-step-lag Z estimate.
  5. Oil reservoir-limit Cg anchors: first & last rows (sheet pinned rows 5 & 14) — near-immaterial for oil Ct.
  6. WHT: always calculated (Ramey); the input-THT path was removed by design decision.
  7. Rounding artifacts preserved where parity needs them: π = 22/7 trajectory cosine; the rouhgsc artifact available as a test override.

Troubleshooting

SymptomCause & 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 NUndersaturated path + window too long. Use the early-production window (days–weeks) or trust the static-gauge selection.
Gas march error: CGR + WGR > 0Gray’s liquid terms divide by liquid rate — use a small CGR (0.1 stb/MMscf) for essentially dry gas.
Reserve/forecast uses the wrong wellBoth always read the current Well-model inputs — calibrate the well model first.