# Natural gas
## Microsim
### Live player
<div class="microsim-player">
<iframe src="https://editor.p5js.org/sciencenibber/full/7eQM3QcvQ" width="100%" height="620" frameborder="0" sandbox="allow-scripts allow-same-origin"></iframe>
</div>
<div class="microsim-fallback">
<img src="Microsims/thumbs/Natural_gas.png" alt="Natural_gas microsim poster" style="width:100%;border:1px solid #4445;border-radius:6px;">
<p><em>Live microsim (desktop) · <a href="https://editor.p5js.org/sciencenibber/sketches/7eQM3QcvQ">open sketch in the p5.js editor</a></em></p>
</div>
**Editor URL:** https://editor.p5js.org/sciencenibber/sketches/7eQM3QcvQ
**Description (100 words):**
A block-diagram simulation of the natural-gas value chain, with five main-chain stages -- RESERVOIR, WELLHEAD, GAS PLANT, PIPELINE, END USE -- drawn left-to-right with animated yellow flow tokens whose [[Density|density]] scales with feedstock rate Q_NG. Four colored side-branches drop from the GAS PLANT to byproduct terminals (NGLs, acid gas, nitrogen, and helium), each labelled with its Mcf/d output. Five sliders drive wellhead composition (NGL, N2, CO2, He percent) and feedstock flow; CH4 is the residual. A purple cliff marker on the composition strip highlights the 0.3 percent helium-recovery economic cutoff, and a live bottom-strip reads sales-gas flow, wellhead and pipeline BTU/scf, and annualized helium revenue at the 2024 USGS Grade-A spot price.
```js
// =====================================================================
// Natural_gas.js -- Wikitube microsim
// Article: Natural gas
// URL: en.wikitube.io/wiki/Natural_gas
// Room: Helium Pattern: G (block diagram, system flow)
// ---------------------------------------------------------------------
// Idea: an interactive block diagram of the natural-gas value chain --
// reservoir to end use -- with the gas-plant stage splitting raw
// wellhead gas into FIVE output streams. The reader drives the
// wellhead composition (NGLs, N2, CO2, He) and feedstock flow Q_NG,
// and watches each stream's mass-flow propagate through the system,
// with token density encoding flow rate. CH4 is the residual:
//
// y_CH4 = 1 - (y_NGL + y_N2 + y_CO2 + y_He)
//
// Main chain (left to right):
//
// [1] RESERVOIR buried kerogen, source + reservoir rock
// | migration thermogenic 3-6 km / 150-220 C
// v
// [2] WELLHEAD raw NG flow Q_NG (Mscf/d)
// | gathering
// v
// [3] GAS PLANT amine sweeten -> dehydrate -> NGL recovery
// +-> NGLs ethane / propane / butane (down branch)
// +-> ACID GAS CO2 + H2S (sulfur recovery / vent)
// +-> N2 nitrogen rejection unit
// +-> He Grade-A, ONLY if y_He >= 0.3 percent
// | sales gas
// v
// [4] PIPELINE dry methane-rich sales gas, ~1000 BTU/scf
// | transmission + distribution
// v
// [5] END USE four consumption sectors, drawn as a
// stacked bar inside the block:
// * power generation ~38 percent
// * residential heat ~22 percent
// * industrial proc. ~30 percent
// * feedstock chem. ~10 percent
//
// Canonical equations (the physics the sketch is built around):
//
// Q_NGL = Q_NG * y_NGL
// Q_acid = Q_NG * y_CO2
// Q_N2 = Q_NG * y_N2
// Q_He = Q_NG * y_He if y_He >= 0.003, else 0
// Q_sales= Q_NG * y_CH4
// BTU/scf= 1000 * y_CH4 + 1750 * y_NGL (inerts contribute 0)
//
// At y_CH4 ~ 0.86, y_NGL ~ 0.08, the heating value is ~1000 BTU/scf,
// exactly pipeline-spec. NGLs alone are 1750 BTU/scf so an NGL-rich
// feed (a "wet" gas) reads richer until the gas plant strips them.
//
// Visual layout (720 x 520):
// * top (0 - 40 ): HUD -- title + en.wikitube.io/wiki/<slug>
// * row (60 - 130): five stage blocks left-to-right + arrows
// * branch (150 - 310): four side branches dropping from GAS PLANT
// to terminal byproduct blocks (NGL/Acid/N2/He)
// * sliders(360 - 470): 3 + 2 grid of sliders for composition + flow
// * bottom (480 - 510): canonical equation in ASCII + live readouts
//
// Conventions (Wikitube Betterfire Standard v0):
// * single ARTICLE constant, single quotes
// * p5.disableFriendlyErrors = true to silence FES in the editor
// * all sliders explicitly .position(x,y).size(w) -- never floating
// * non-ASCII chars live in COMMENTS only; text() literals are ASCII
// * Energy room palette (P5_JS_EDITOR section 4)
// =====================================================================
const ARTICLE = 'Natural_gas';
const TITLE = 'Natural gas';
p5.disableFriendlyErrors = true;
// ----- Energy room palette (P5_JS_EDITOR section 4) ------------------
const BG = 18;
const FG = 240;
const DIM = [240, 240, 240, 150];
const HOT = [220, 110, 60]; // raw gas / wellhead
const COLD = [60, 130, 220]; // processed / end-use
const STRUCT = [120, 130, 150]; // block outlines / piping
const TRAJ = [240, 220, 80]; // main-chain flow tokens
const GAUGE = [120, 220, 140]; // gauges / output readouts
const ACCENT = [200, 100, 220]; // helium branch (purple)
const ACID = [200, 90, 100]; // acid-gas branch (red)
const NGLC = [240, 170, 80]; // NGL branch (amber)
const N2C = [150, 200, 230]; // nitrogen branch (pale blue)
// ----- Slider state (read once per frame in draw) --------------------
let yNGLSlider; // NGL mole percent in raw NG (1 - 10)
let yN2Slider; // nitrogen mole percent (0 - 15)
let yCO2Slider; // carbon dioxide mole percent (0 - 20)
let yHeSlider; // helium mole percent (0 - 7)
let qNGSlider; // feedstock flow rate (Mscf/d, 100 - 5000)
// ----- Animated-token state per arrow segment ------------------------
let tickPhase = 0;
// ----- Process stages (block geometry, set in setup) -----------------
let blocks = []; // five main-chain stage blocks
let branchBlocks = []; // four byproduct terminal blocks below GAS PLANT
// ----- Layout constants ----------------------------------------------
const BLOCK_W = 108;
const BLOCK_H = 60;
const ROW_Y = 70;
const BR_Y = 240; // top of branch row
const BR_W = 92;
const BR_H = 42;
// =====================================================================
// setup
// =====================================================================
function setup() {
createCanvas(720, 520);
pixelDensity(2);
textFont('system-ui');
// ----- Build the five-stage main chain ----------------------------
// Evenly spread across the canvas with a small inset.
const inset = 18;
const usable = width - 2 * inset;
const stride = usable / 5;
const stageTitles = [
['RESERVOIR', 'kerogen / cap rock'],
['WELLHEAD', 'gathering Q_NG'],
['GAS PLANT', 'sweeten / dry / NRU'],
['PIPELINE', 'sales gas (BTU/scf)'],
['END USE', 'pwr / heat / ind / fdstk']
];
// Color graduates from HOT (deep reservoir) to STRUCT (plant) to COLD (delivery).
const stageAccents = [HOT, HOT, STRUCT, COLD, COLD];
for (let i = 0; i < 5; i++) {
const cx = inset + stride * (i + 0.5);
blocks.push({
x: cx - BLOCK_W / 2,
y: ROW_Y,
w: BLOCK_W,
h: BLOCK_H,
title: stageTitles[i][0],
sub: stageTitles[i][1],
accent: stageAccents[i]
});
}
// ----- Build the four byproduct branches dropping from GAS PLANT --
// All four branches sit in a row below stage [3] (GAS PLANT, index 2).
const gp = blocks[2];
const branchTitles = [
['NGLs', 'C2H6/C3H8/C4', NGLC],
['ACID GAS', 'CO2 + H2S', ACID],
['N2', 'rejected', N2C],
['He', 'Grade-A 99.997', ACCENT]
];
// Spread branches across the lower band, centered under GAS PLANT but wide.
const branchSpread = 540;
const branchStart = gp.x + gp.w / 2 - branchSpread / 2 + BR_W / 2;
for (let i = 0; i < 4; i++) {
const cx = branchStart + (branchSpread / 3) * i;
branchBlocks.push({
x: cx - BR_W / 2,
y: BR_Y,
w: BR_W,
h: BR_H,
title: branchTitles[i][0],
sub: branchTitles[i][1],
accent: branchTitles[i][2]
});
}
// ----- Sliders (Betterfire rule: explicit .position().size()) -----
// Two-row grid below the diagram: 3 on top, 2 on bottom.
const sx1 = 24, sx2 = 254, sx3 = 484;
const sy1 = 380, sy2 = 430;
const SW = 200;
// y_NGL: NGL mole percent in raw NG.
yNGLSlider = createSlider(1, 10, 4.0, 0.1).position(sx1, sy1).size(SW);
// y_N2: nitrogen rejection feed mole percent.
yN2Slider = createSlider(0, 15, 2.0, 0.1).position(sx2, sy1).size(SW);
// y_CO2: carbon dioxide mole percent (amine-removed).
yCO2Slider = createSlider(0, 20, 1.5, 0.1).position(sx3, sy1).size(SW);
// y_He: helium mole percent (Grade-A only above 0.3 percent).
yHeSlider = createSlider(0, 7.0, 0.5, 0.05).position(sx1, sy2).size(SW);
// Q_NG: feedstock flow in million standard cubic feet per day.
qNGSlider = createSlider(100, 5000, 1500, 25).position(sx2, sy2).size(SW);
textAlign(LEFT, TOP);
}
// =====================================================================
// draw
// =====================================================================
function draw() {
background(BG);
// ----- Read sliders once -----------------------------------------
const yNGL = yNGLSlider.value() / 100;
const yN2 = yN2Slider.value() / 100;
const yCO2 = yCO2Slider.value() / 100;
const yHe = yHeSlider.value() / 100;
const qNG = qNGSlider.value();
// ----- Composition residual: methane is whatever is left ----------
// Constrain CH4 to a non-negative number; clip the others' sum if needed.
const sumOther = yNGL + yN2 + yCO2 + yHe;
const yCH4 = Math.max(0, 1 - sumOther);
// ----- Mass-balance flows (Mscf/d) --------------------------------
const qSales = qNG * yCH4; // dry sales gas
const qNGL = qNG * yNGL; // NGLs to fractionation
const qAcid = qNG * yCO2; // amine-stripped acid gas
const qN2Out = qNG * yN2; // NRU off-gas
const qHeOut = (yHe >= 0.003) ? qNG * yHe : 0; // Grade-A He, only above threshold
// ----- Heating value of sales gas (BTU/scf) -----------------------
// CH4 = 1000 BTU/scf, NGLs (C2+) average ~1750 BTU/scf; inerts = 0.
// The gas plant strips NGLs out of sales gas, so the pipeline heating
// value is computed on the SALES STREAM, which is pure CH4 here -- so
// BTU_pipe = 1000. For the WELLHEAD heating value we include NGLs.
const btuWell = 1000 * yCH4 + 1750 * yNGL;
const btuPipe = 1000; // pipeline-spec target
// ----- Annualize the helium stream --------------------------------
// 365 day/yr * $385/Mcf (2024 USGS Grade-A US spot, MCS 2025).
const heAnnual = qHeOut * 365; // Mcf/yr Grade-A He
const heRevenueMM = heAnnual * 385 / 1e6; // millions USD/yr
// Advance global animation phase.
tickPhase = (tickPhase + 0.004) % 1;
// ----- Compose the diagram ---------------------------------------
drawMainConnectors(qNG, qSales);
drawBranchConnectors(qNGL, qAcid, qN2Out, qHeOut);
for (let i = 0; i < blocks.length; i++) drawBlock(blocks[i], i + 1);
for (let i = 0; i < branchBlocks.length; i++) drawBranchBlock(branchBlocks[i], i, [qNGL, qAcid, qN2Out, qHeOut][i]);
drawCompositionStrip(yCH4, yNGL, yN2, yCO2, yHe);
drawEndUseBar(blocks[4]);
drawSliderLabels(yNGL, yN2, yCO2, yHe, qNG);
drawReadouts(qSales, btuWell, btuPipe, heRevenueMM, yHe);
drawHUD();
}
// =====================================================================
// Main-chain block + connector drawing
// =====================================================================
// Draw one main-chain stage block with title + subtitle + index badge.
function drawBlock(b, idx) {
push();
noStroke();
fill(28);
rect(b.x, b.y, b.w, b.h, 6);
// Top accent stripe
const acc = b.accent || ACCENT;
fill(acc[0], acc[1], acc[2], 220);
rect(b.x, b.y, b.w, 6, 6, 6, 0, 0);
// Frame
noFill();
stroke(...STRUCT);
strokeWeight(1);
rect(b.x, b.y, b.w, b.h, 6);
// Index badge
noStroke();
fill(...TRAJ);
circle(b.x + 12, b.y + 18, 16);
fill(BG);
textAlign(CENTER, CENTER);
textSize(10);
text(idx, b.x + 12, b.y + 18);
// Title + subtitle
fill(FG);
noStroke();
textAlign(LEFT, TOP);
textSize(11);
text(b.title, b.x + 24, b.y + 12);
fill(...DIM);
textSize(9);
text(b.sub, b.x + 24, b.y + 28);
pop();
}
// Draw the four left-to-right arrows between adjacent main-chain blocks.
// Token density encodes Q_NG up through GAS PLANT, then Q_sales after.
function drawMainConnectors(qNG, qSales) {
push();
const REF = 1200; // reference flow for density mapping
for (let i = 0; i < 4; i++) {
const a = blocks[i];
const b = blocks[i + 1];
const x0 = a.x + a.w + 2;
const x1 = b.x - 2;
const y = a.y + a.h / 2;
// Pipe
stroke(...STRUCT);
strokeWeight(2);
line(x0, y, x1, y);
drawArrowhead(x1, y, 8, 0);
// Density: full Q_NG up to gas plant exit; Q_sales beyond.
const q = (i < 2) ? qNG : qSales;
const dens = constrain(q / REF, 0.05, 1.6);
const N = Math.max(2, Math.round(dens * 10));
noStroke();
for (let k = 0; k < N; k++) {
const f = ((k / N) + tickPhase) % 1;
const px = lerp(x0 + 4, x1 - 4, f);
const a2 = 220 - 120 * Math.abs(0.5 - f) * 2;
fill(TRAJ[0], TRAJ[1], TRAJ[2], a2);
circle(px, y, 5);
}
}
pop();
}
// =====================================================================
// Branch (byproduct) drawing
// =====================================================================
// Draw a side-branch terminal block (NGL / Acid / N2 / He).
function drawBranchBlock(b, idx, qFlow) {
push();
// Active/inactive: the He block dims below the 0.3 percent threshold.
const active = (qFlow > 0);
noStroke();
fill(active ? 28 : 22);
rect(b.x, b.y, b.w, b.h, 5);
// Accent stripe
const acc = b.accent;
const a = active ? 220 : 90;
fill(acc[0], acc[1], acc[2], a);
rect(b.x, b.y, b.w, 5, 5, 5, 0, 0);
// Frame
noFill();
stroke(...STRUCT);
strokeWeight(1);
rect(b.x, b.y, b.w, b.h, 5);
// Title + subtitle
fill(active ? FG : 150);
noStroke();
textAlign(LEFT, TOP);
textSize(10);
text(b.title, b.x + 8, b.y + 8);
fill(active ? DIM : [150, 150, 150, 150]);
textSize(9);
text(b.sub, b.x + 8, b.y + 22);
// Flow readout in Mcf/d (top right of block).
fill(active ? GAUGE : [120, 120, 120, 200]);
textAlign(RIGHT, BOTTOM);
textSize(9);
text(nf(qFlow, 1, 1) + ' Mcf/d', b.x + b.w - 6, b.y + b.h - 4);
pop();
}
// Draw the four side-pipes from GAS PLANT down to each branch block.
// Token density and color match the branch.
function drawBranchConnectors(qNGL, qAcid, qN2Out, qHeOut) {
push();
const gp = blocks[2];
const sx = gp.x + gp.w / 2; // common origin x at bottom of GAS PLANT
const sy = gp.y + gp.h; // common origin y
const flows = [qNGL, qAcid, qN2Out, qHeOut];
const REFs = [60, 30, 30, 5]; // per-branch reference flow
const colors = [NGLC, ACID, N2C, ACCENT];
for (let i = 0; i < 4; i++) {
const t = branchBlocks[i];
const ex = t.x + t.w / 2;
const ey = t.y - 4;
// L-shaped pipe: down a short way from GAS PLANT, then across to
// branch column, then down into branch top.
const midY = sy + 40;
stroke(...colors[i]);
strokeWeight(2);
noFill();
line(sx, sy, sx, midY);
line(sx, midY, ex, midY);
line(ex, midY, ex, ey);
drawArrowhead(ex, ey, 7, 90);
// Flowing tokens along the pipe -- skip if inactive.
const q = flows[i];
if (q <= 0) continue;
const dens = constrain(q / REFs[i], 0.1, 1.5);
const N = Math.max(2, Math.round(dens * 8));
noStroke();
fill(colors[i][0], colors[i][1], colors[i][2], 220);
// Distribute tokens uniformly along the polyline by linear parameter.
const seg1 = midY - sy;
const seg2 = Math.abs(ex - sx);
const seg3 = ey - midY;
const total = seg1 + seg2 + seg3;
for (let k = 0; k < N; k++) {
const f = ((k / N) + tickPhase * 1.4) % 1;
const d = f * total;
let px, py;
if (d < seg1) {
px = sx;
py = sy + d;
} else if (d < seg1 + seg2) {
const dd = d - seg1;
px = lerp(sx, ex, dd / seg2);
py = midY;
} else {
const dd = d - seg1 - seg2;
px = ex;
py = midY + dd;
}
circle(px, py, 4);
}
}
pop();
}
// =====================================================================
// Composition strip + end-use bar
// =====================================================================
// Draw a horizontal stacked composition bar above the slider region,
// showing the wellhead mole fractions in absolute proportion.
function drawCompositionStrip(yCH4, yNGL, yN2, yCO2, yHe) {
push();
const x0 = 18, y0 = 320, w = width - 36, h = 16;
// Background frame
noFill();
stroke(...STRUCT);
strokeWeight(1);
rect(x0, y0, w, h, 3);
// Segments
noStroke();
let cx = x0;
const segs = [
[yCH4, [180, 200, 220]],
[yNGL, NGLC],
[yN2, N2C],
[yCO2, ACID],
[yHe, ACCENT]
];
for (let i = 0; i < segs.length; i++) {
const seg = segs[i];
const ww = seg[0] * w;
fill(seg[1][0], seg[1][1], seg[1][2], 230);
rect(cx, y0, ww, h);
cx += ww;
}
// Label
fill(...DIM);
textAlign(LEFT, BOTTOM);
textSize(10);
text('Wellhead composition (mole fraction): CH4 / NGL / N2 / CO2 / He', x0, y0 - 2);
// Threshold marker for He minimum-economic at 0.3 percent of total.
// Place a small tick at the He boundary so the reader sees the cliff.
const xHe = x0 + (1 - yHe) * w;
stroke(...ACCENT);
strokeWeight(1);
line(xHe, y0 - 2, xHe, y0 + h + 2);
pop();
}
// Draw the end-use stacked bar INSIDE block [4] (END USE), with four
// segments sized to canonical US 2024 demand shares.
function drawEndUseBar(b) {
push();
// Internal bar geometry: shrink to fit inside the block padding.
const x0 = b.x + 10;
const y0 = b.y + b.h - 14;
const w = b.w - 20;
const h = 8;
// Segments: power 38 / residential 22 / industrial 30 / feedstock 10.
const shares = [0.38, 0.22, 0.30, 0.10];
const cols = [HOT, NGLC, COLD, GAUGE];
let cx = x0;
noStroke();
for (let i = 0; i < 4; i++) {
const ww = shares[i] * w;
fill(cols[i][0], cols[i][1], cols[i][2], 220);
rect(cx, y0, ww, h, 1);
cx += ww;
}
// Tick label below the bar
fill(...DIM);
textAlign(LEFT, TOP);
textSize(7);
text('pwr 38 . res 22 . ind 30 . fdstk 10', b.x + 8, b.y + b.h - 4);
pop();
}
// =====================================================================
// Slider labels + readouts + HUD
// =====================================================================
function drawSliderLabels(yNGL, yN2, yCO2, yHe, qNG) {
push();
fill(...DIM);
noStroke();
textAlign(LEFT, BOTTOM);
textSize(10);
// Row 1
text('NGL ' + nf(yNGL * 100, 1, 1) + ' %', 24, 378);
text('N2 ' + nf(yN2 * 100, 1, 1) + ' %', 254, 378);
text('CO2 ' + nf(yCO2 * 100, 1, 1) + ' %', 484, 378);
// Row 2
text('He ' + nf(yHe * 100, 2, 2) + ' %', 24, 428);
text('Q_NG ' + nf(qNG, 1, 0) + ' Mscf/d', 254, 428);
pop();
}
// Bottom strip: BTU values, sales-gas flow, helium revenue, threshold note.
function drawReadouts(qSales, btuWell, btuPipe, heRevenueMM, yHe) {
push();
fill(...GAUGE);
noStroke();
textAlign(LEFT, BOTTOM);
textSize(11);
text('Sales gas ' + nf(qSales, 1, 0) + ' Mcf/d', 18, 498);
text('BTU/scf ' + nf(btuWell, 1, 0) + ' wellhead / ' + nf(btuPipe, 1, 0) + ' pipeline', 200, 498);
fill(...ACCENT);
text('He revenue