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

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.

261 regression and security 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 · p/Z from memory gauges
3 — Forecast ✓ Tarner · Walsh generalized-MB (Rv) — 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                             # 261 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 produced from this same source tree by build.ps1.

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 wells have their own tab — and their own section: Water — Well model.

Defaults

GroupFields (default)
Well & flowFTHP 700 psi · 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 2993.08 mAH (= 2985 mTVD on the demo trajectory) · ΔP 1325.16 psi (manual) · tubing gas blank = formation · the ESP demo case sets the pump at 2985 mAH (back on natural/gas lift)
Per-lift demo cases — the table above is the NATURAL-flow demo, and each lift type is a different source workbook: selecting Gas lift or ESP on a pristine form loads that workbook’s live case (ESP: FTHP 160, WC 5%, GOR 384, User PI 2.7 @ 2650, test 2565 @ 160, 145 stages @ 50 Hz, actual Pint/Pdis 1392/2720; Gas lift: FTHP 300, WC 25%, GOR 412, User PI 1.07897 @ 5000 — the workbook’s solved Jones J, whose matched K lands on its hand-tuned 16.37 mD). Typed values are never overwritten, and Natural swaps the table above back. The IPR basis selector (Darcy | User PI) drives it: in PI mode the J is your input and the matched K shows grey (derived); Darcy geometry becomes optional.

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 × the current Pr, refilled as you edit Pr or Pri unless you have typed your own) with the future-J chain. The VLP columns follow the fluid and the ACTIVE LIFT TYPE: oil natural FTHP · GOR · W.C · tubing ID; oil gas lift + injection gas; oil ESP + frequency (Hz); water natural FTHP · tubing ID; water gas lift + injection gas; water ESP + frequency; water injector injection THP · injected-water temperature · tubing ID. Typed values survive a lift switch. The chart carries the current-Pr IPR (heavy teal) — the curve every solution is solved against — then the future-pressure family. Pri is not drawn: on a depleted well it is history, and a second high curve only competes with the one the answers belong to (the pressure AXIS still tops out at Pri, so runs stay comparable). Each set’s solved NODE is marked with a diamond, and a set that changes the water cut also gets its own thin IPR, because oil rate = gross × (1−W.C) gives it a different curve in rate terms. Every node is therefore a visible intersection of two drawn curves, and the numbers match the nodal-solution table beneath the chart, which every module carries.
  4. Sensitivity pressure axis — the IPR/VLP sensitivity chart tops its pressure axis at Pri on oil, water and gas wells: the inflow side can never exceed the initial reservoir pressure, and the VLP tail that does (high rates the well could never reach) would otherwise stretch the scale and squash the families. The injector keeps an auto axis — injection deliberately pushes ABOVE Pri, so capping it would clip the very curves it exists to show.
  5. Pump @ solution point — wherever an ESP node is solved (the model match AND every sensitivity case) the pump’s full state is reported in one identical parameter set: pump · stages · frequency (with the curve’s reference frequency) · wear · head total and per stage · ΔP · rate at the pump before and after separation · composite gradient · free gas at intake · separator efficiency · thrust with its down/BEP/up window · hydraulic power · ΔP convergence. In the sensitivity view the sets sit side by side, one column each, so the trade-off reads straight across.
  6. Every sensitivity set is solved — a table under the chart gives each VLP set its own NODAL SOLUTION against the current IPR (rate, Pwf, WHP, calculated WHT). On ESP wells it also carries the frequency, solved ΔP, head, intake/discharge and thrust per set, and two more charts appear: the pump curve per sensitivity (each set at its own frequency, node starred, over the shared thrust envelope) and the traverse per sensitivity (each set’s top-down march and IPR back-calc with its ΔP step at pump depth). Oil and water alike.
  7. ESP frequency sensitivity — with a pump selected (oil or water — both tabs read the same 68-pump database) the coupled ΔP is re-solved per rate at each frequency (affinity-scaled curve, intake state and all); with Manual ΔP the quoted ΔP is scaled by the affinity law (f/f₀)². Beyond the pump’s flow range the head is zero, so the frequency curves legitimately merge.
  8. Injector sensitivities — the injector has its own family: available BHIP vs rate per set against the injectivity line at future pressures. Injection water is incompressible with a fixed viscosity, so its temperature moves the BOTTOMHOLE TEMPERATURE, not the pressure — the BHT families are drawn on the second axis.
  9. Gas-lift performance — injection sweep, optimum, incremental dQ/dInj.
  10. 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 — its thrust-marker rows are coloured: row 4 blue = down-thrust limit, row 6 gold = BEP, row 8 red = up-thrust limit, the fixed rows the physics reads) 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, and the anchor Pwf is written grey into the test-Pwf cell (the workbook macro’s C22 — overwrite it to anchor the match on a measured value); Match wear (actual Pint/Pdis) applies the wear factor only — the implied PI and matched K are reported as QC, never written to your inputs (on the demo’s actual gauges the QC 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 at 0.9 / 0.8 / 0.7 × Pr (shallower than the 0.75/0.5/0.25 used elsewhere, because a pump still has to lift at modest depletion), each FULLY solved: the Darcy future J, then a complete coupled ESP solve. The chart draws the current-Pr IPR (heavy teal), each case’s IPR and the coupled ESP VLP — one curve, since pump, tubing and THP do not change with Pr — so every solved node is a visible crossing marked by a diamond, with the pressure axis topped at Pri as elsewhere. The table lists the ESP state AT that node — rate, Pwf, Pint, Pdis, ΔP, head, Qg@pump, gradient, free gas %, WHT and thrust — which is how you see the pump working harder and the intake falling as the reservoir depletes. ESP cases also appear in the main VLP/IPR sensitivity block (frequency column, coupled solve per set), but that block solves at the CURRENT Pr only.
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.

Water — Well model

Well type: Producer | Injector · producer lift types: Natural flow · Gas lift · ESP.

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.

Defaults

GroupFields (default)
Well & flowFTHP 200 psi (producer) · injection THP 2000 psi (injector) · ID 2.992 in · roughness 0.00006
Trajectorytop perf 2810 mAH · kick-off 1910 m · deviation 7°
Fluidswater SG 1.05 · lift-gas SG 0.842 · Tres 201°F · injection water temp 90°F
Heat transfersoil 90°F · U 3 BTU/hr·ft²·°F · OD 3.5 in · Cp 0.51
IPRbasis Darcy | User PI · Pri 4800 · K 50 mD · H 42.653 ft · Re 1640.5 · Rw 0.5104 · skin 0 · fracture gradient 0.7 psi/ft (injector parting check; blank = off)
Test2000 bbl/d @ 200 psi · Pwf blank = get Pwf
ESPpump 2993.08 mAH · 145 stages @ 50 Hz · wear 0 · no separator (water is gas-free) · Manual ΔP 1325.16 psi
Water ESP — the water tab drives the same 68-pump database as the oil tab: select a pump and the ΔP is SOLVED from its curve (stages × affinity × wear) rather than typed, with the head and the down-thrust/BEP/up-thrust window reported at the operating point, and Match stages solving the stage count against the same design-floor proof. Water is gas-free, so there is no free-gas intake block and no separator — the pump curve works on the water volume at the intake. Manual ΔP stays in the dropdown for a pump known only by its ΔP (a frequency sensitivity then scales it by (f/f₀)²), and Custom pump (add new)… takes a per-stage curve exactly as on the oil tab — same 11-row table, same coloured thrust-marker rows. The water tab also carries the IPR basis selector: User PI types J directly on the linear water IPR (Pb = 0), matched K grey, Darcy geometry optional. Solving draws two extra charts: the PumpCurve (30–60 Hz family, the down-thrust/BEP/up-thrust envelope and the operating point starred, with the ESP results block beside it) and the Traverse (top-down vs the IPR back-calc, with the pump ΔP step at pump depth).
Injector — the Water Well tab has a Producer | Injector selector. An injector has no FLOWING tubing head pressure — every surface pressure is the injection THP, through the marches, the injectivity IPR and the sensitivity columns (inputs read Injection THP / Test injection THP, the sensitivity column Inj THP psi). A fracture gradient input (psi/ft, default 0.7 — blank disables the check) gives the formation parting pressure at the perforations. When the solved BHIP exceeds it the summary says so and reports the injection THP that would land exactly on parting, solved by re-running the nodal solve at trial THPs; if no THP both injects and stays below parting, it says that instead. The control is the surface pressure, not the rate — available BHIP = THP + head − friction is HIGHEST at low rate, so injecting less raises the bottomhole pressure rather than lowering it. Sensitivity defaults: injection THP 100 / 1000 / 2000 psi and future pressures 0.75 / 0.5 / 0.25 × the current Pr. Running them adds the Injectivity sensitivities chart — each THP set solved against Pri AND each future Pres (a full nodal grid), plotted as injection THP (y) against the solved injection rate (x), one line per reservoir pressure, with the grid tabulated beneath. 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 hides for the injector; the sensitivities switch to the injector’s own set (injection THP · injected-water temperature · tubing ID). Its results carry only three result rows — injectivity node, available-BHIP sensitivities and injection-THP-required — the gas-lift, pump-curve and pump-traverse rows belong to producers and are cleared; switching well type wipes the previous type’s results so nothing stale is read as the injector’s. When THP + head cannot reach Pr the THP deficit is reported. Laminar Fanning 16/Re below the transition, Chen above.
IPR basis — the same Darcy | User PI selector as the oil tab: in PI mode you type J directly on the linear water IPR (Pb = 0, no Vogel curvature), the matched K shows grey (derived from your J at the water μ = 0.5 cp, Bw = 1), and the Darcy geometry becomes optional. There is no water source workbook — this tab is WellSim’s own extension of the oil march at its limiting case, pinned by its own tests.

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

The reserve survey tables — oil memory-gauge, gas SITHP and gas memory-gauge — take Paste from clipboard, Import CSV, +10 rows and Clear, and grow to the data just like prod_data. Clipboard cells are read in the table’s own input-column order; a first line starting with a letter is treated as a header and skipped (a 17-Nov-14 date is not).

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.

4
Pres from memory gauges

Measured static Pr — no march, no IPR: the gauge reading IS the datum. Inputs Date | Pr; calculated z | Gp | p/Z. Each reading must be a stabilized build-up corrected to datum. Defaults are the pressures selection 2 computes, so the two routes cross-check (both 120.19 Bscf).

Oil — Forecast (Tarner | Walsh)

Saturation-tracking material-balance forecast with a Method selector (one active). N and the start state (date, Np, Pres) are grey input-or-calculated, chained off the Reserve module — the date defaults to the last prod-data date, and both methods book an anchor row at that date so the curve begins there and joins the history. The forecast stream continues the well as last measured: Forecast THP, Forecast W.C and Forecast GOR are grey input-or-calculated, defaulting to the last prod row and overridable. All three are wellbore conditions held for the whole run — the GOR feeds the lift march every step, so a well producing free gas keeps that gas helping it flow, while the charted F GOR stays the MB reservoir GOR; both methods share the same inputs and the same Pwf source selection: nodal at forecast THP (VLP-coupled, default) or fixed minimum Pwf. Per step, both solve the same two residuals deterministically: pressure closes the gas balance (Gp by GOR trapezoid = Gp from MB), saturation closes the MB saturation; rel-perm from the sheet’s Kro(So)/Krg(Sg) polynomials; Brent/alternation to convergence instead of GoalSeek loops, the rate coupled to the trial pressure. PVT (Rs/Bo/Bg/μ) is evaluated at the trial pressure inside the solve — the training-material policy (“all PVT data evaluated at the assumed reservoir pressure”).

1
Tarner (default)

The classic method (workbook Tarner sheet): standard MB, So = (1−Swi)(N−Np)Bo/(N·Boi(1−ctΔp)), GOR = Rs + (Krg/Kro)(μoBo/μgBg), rate from the mobility PI J1 = 0.00708Kh/(ln(Re/Rw)−0.75+S).

2
Walsh (generalized MB, Rv)

Walsh’s generalized material balance (PETSOC 95-01-07) with the volatilized oil-gas ratio Rv(P): (Bg−Rv·Bo) terms in both the Gp and So equations, Foo-corrected GOR, and the CONSTANT calibrated PI on total mobility (qt = J·λt·Δp, qo = qt·λo/(λt·Bo)). Rv from the workbook’s tuned polynomial. Quirks preserved: μg = μo (the sheet’s Gas-Visc column reads the oil-viscosity cell); Bg in cf/scf.

Defaults: step 30 d · THP, W.C and GOR from the last prod row (300 psi / 60% / 5000 scf/stb on the demo well) · min Pwf 500 · Swi 0.15 · Cw 2.63e-6 · Cf 3.25e-6 · abandonment 50 stb/d · max 60 steps. Validation: 7 cell-pinned tests reproduce the Walsh sheet’s first solved row to 9+ digits; replaying the sheet’s Pwf column tracks its 64-step trajectory within ≈1% on pressure (EUR −2.2%, attributable to the sheet reading PVT from its parallel Tarner column). 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)
  — also places the ESP pump: its setting depth is entered as MEASURED
    depth (mAH) and converted to TVD on this trajectory
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)
User PI basis: type J + Pres directly (ESP workbook route, "Iput PI"); K becomes the
  DERIVED cell — same closed-form match against the typed J, grey = program-filled.
  Geometry optional. Lift-type switch loads that workbook’s demo case on a pristine form.
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 %

Forecast (oil — Tarner & Walsh)

Both march the macro's GoalSeek PAIR per step, alternated to convergence:
  AE: Gp by the producing-GOR trapezoid over Np = Gp from the MB  → solve P  (Brent)
  AF: the assumed So                           = So from the MB  → solve So (substitution)
  Pwf frozen for the step; PVT at the TRIAL pressure; 25 passes, |ΔP|<1e-6, |ΔSo|<1e-10

shared:  Kro(So), Krg(Sg) = the sheet 6th-order polynomials (Tarner!F16/G16), verbatim
         Sg = max(0, 1 − Swi − So);   So clamped to [0.05, 1 − Swi]
         ct = (Cw·Swi + Cf)/(1 − Swi)·(Pri − P)    Swi .15, Cw 2.63e-6, Cf 3.25e-6 /psi
         Np += qo·Δt/1e6;   Gp += (GOR_prev + GOR_new)/2 · ΔNp
         Pwf 'vlp'   = nodal vs the REAL oil march at forecast FTHP, floored at minPwf
             'fixed' = constant minPwf (the sheet's active behaviour)
         an anchor row at the start date (Δt = 0) joins the forecast to its history

Tarner — solution-gas MB, MOBILITY Darcy PI (no μ·B; mobility enters through λ):
  J1  = 0.00708·K·h/(ln(Re/Rw) − 0.75 + S)     λt = Kro/μo + Krg/μg    λo = Kro/μo
  qo  = J1·(λo/Bo)·(P − Pwf)
  So  = (1 − Swi)·(N − Np)·Bo / (N·Boi·(1 − ct))                      (Tarner!Y)
  Gp  = N·(Rsi − Rs) − (N·Boi − (N − Np)·Bo)/Bg + N·Boi·ct/Bg + Np·Rs  (Tarner!O)
  GOR = Rs + (Krg/Kro)·(μo·Bo)/(μg·Bg)                                (Tarner!P)
  Bg in bbl/scf;  μg at reservoir T, sweet pseudo-criticals, LOCAL Tpr

Walsh — generalized (volatile-oil) MB with Rv, on the CONSTANT calibrated PI J:
  Rv(P) = the sheet 6th-order polynomial (Walsh!AH) / 1000                 [bbl/scf]
  qt  = J·λt·(P − Pwf);   qo = qt·λo/(λt·Bo)              J = Walsh!$I$13 = J_2
  Gp  = (N·(Boi·(1 − Rv·Rs) − (Bg − Rv·Bo)·Rsi − (Bo − Rs·Bg)) + Np·Bo − Np·Rs·Bg)
        / (Bo·Rv − Bg)  +  (N·Boi/Bg)·ct/(1 − Swi)                    (Walsh!O)
  So  = (1 − Swi)·((1 − Np/N)·Bo·Bg − Boi·Rv·Bo)
        / (Boi·(Bg − Rv·Bo)·(1 − ct))                              (Walsh!Y)
  Foo = 1/(1 + (Bo·Krg·μo·Rv)/(Bg·Kro·μg))                            (Walsh!AI)
  GOR = Foo·(Rs + (Krg/Kro)·(μo·Bo)/(μg·Bg))                          (Walsh!P)
  QUIRKS KEPT: μg = μo (Walsh!W reads the OIL-visc cell BHP!AQ)
               Bg in cf/scf, NOT bbl/scf — the Walsh MB is written in those units
  start Gp = Np₀·Rsi   (Tarner starts at 0)

stops: qo < abandon (50 stb/d) | Np ≥ .999·N | P ≤ max(1.02·minPwf, 120) | dead | max-steps
       → EUR, recovery %
LIMIT: neither MB carries a water term — the Forecast W.C affects LIFT only, never
       the balance. On a high-water-cut well that is a real limit of the method.

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.
  8. Oil-forecast PVT evaluation point: Rs/Bo/Bg/μ are evaluated at the TRIAL pressure inside each step’s solve, for both methods. The saved Tarner sheet froze PVT at initial values; the training material directs otherwise, and the Walsh Gp residual moves with pressure almost entirely through the PVT (Bg, Rs, Rv) — freezing it leaves no root.
  9. Walsh PVT table pressure: the sheet reads its PVT table at the parallel TARNER sheet’s pressures (a shared-table convenience of the workbook); WellSim reads it at Walsh’s own solved pressure.

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.

Contact

Questions, bug reports, or a workbook you would like ported — write to [email protected].