Estimates block grades from samples, values each block, finds the most valuable pit within a slope, and reports tonnes, grades, strip ratio and a discounted schedule.
How the precision is kept
Block arrays are Float64Array and all arithmetic is 64-bit. The results are set by the method: grades are interpolated by inverse distance weighting, and the pit slope is applied in whole blocks, one layer at a time. The pit is a minimum cut found by maximum flow on 64-bit capacities with absolute thresholds.
Calculations under this policy
10 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
The block index, block center and block volume of a regular grid.
center = origin + (i + 0.5) × size; volume = dx × dy × dz
Centers come from whole-number indexes, not a running sum. Density and grade are Float64Array.
mining.js
lines 37 to 42
CivilCraft 0.8.1
export function makeModel({ nx, ny, nz, size = [1, 1, 1], origin = [0, 0, 0], density = 2.7 }) {
const n = nx * ny * nz;
return { nx, ny, nz, size, origin, density: new Float64Array(n).fill(density), grade: new Float64Array(n), rock: new Uint8Array(n),
index: (i, j, k) => i + nx * (j + ny * k), centre: (i, j, k) => [origin[0] + (i + 0.5) * size[0], origin[1] + (j + 0.5) * size[1], origin[2] + (k + 0.5) * size[2]],
volume: size[0] * size[1] * size[2] };
}
Grade by inverse distance weighting
64-bit
Each block's grade from the drill-hole samples around it.
grade = Σ (g_s ÷ d_s^p) ÷ Σ (1 ÷ d_s^p)
Distances by hypot and weights by Math.pow, summed in sample order. A sample within 1e-9 of the block center is taken as the grade itself, which avoids dividing by zero. With fewer samples in range than the minimum the grade is 0.
mining.js
lines 44 to 56
CivilCraft 0.8.1
export function idw(model, samples, { power = 2, radius = Infinity, min = 1 } = {}) {
for (let k = 0; k < model.nz; k++) for (let j = 0; j < model.ny; j++) for (let i = 0; i < model.nx; i++) {
const c = model.centre(i, j, k); let sw = 0, sv = 0, n = 0, exact = null;
for (const s of samples) {
const d = Math.hypot(c[0] - s.x, c[1] - s.y, c[2] - s.z);
if (d > radius) continue;
if (d < 1e-9) { exact = s.grade; break; }
const w = 1 / Math.pow(d, power); sw += w; sv += w * s.grade; n++;
}
model.grade[model.index(i, j, k)] = exact !== null ? exact : n >= min ? sv / sw : 0;
}
return model.grade;
}
Block value
64-bit
The money each block is worth if mined: ore margin less mining cost, or the cost of waste.
t = volume × density; ore = t × (grade × recovery × price − process cost); value = ore − t × mining cost, or −t × waste cost
One 64-bit expression per block, stored in a Float64Array. A block is ore when its process margin is above zero, with no tolerance.
mining.js
lines 58 to 65
CivilCraft 0.8.1
export function blockValues(model, { price, recovery = 1, processCost = 0, miningCost = 0, wasteCost = null }) {
const v = new Float64Array(model.grade.length);
for (let b = 0; b < v.length; b++) {
const t = model.volume * model.density[b], ore = t * (model.grade[b] * recovery * price - processCost);
v[b] = ore > 0 ? ore - t * miningCost : -t * (wasteCost ?? miningCost);
}
return v;
}
Maximum flow
64-bit
The maximum flow from which the ultimate pit is read.
Capacities are 64-bit money amounts updated by subtraction and addition along each path, so rounding residue can remain on an arc; an arc counts as open while its capacity is above 1e-12 (absolute). Precedence arcs carry 1e18, large enough that subtracting a path flow leaves them unchanged.
mining.js
lines 72 to 91
CivilCraft 0.8.1
maxflow(s, t) {
let flow = 0; this.it = new Int32Array(this.n);
while (this.bfs(s, t)) {
for (let i = 0; i < this.n; i++) this.it[i] = this.head[i];
for (;;) { // iterative DFS for one augmenting path
const stack = [s], edges = []; let found = false;
while (stack.length) {
const u = stack[stack.length - 1]; if (u === t) { found = true; break; }
let advanced = false;
for (; this.it[u] !== -1; this.it[u] = this.next[this.it[u]]) { const e = this.it[u], v = this.to[e]; if (this.cap[e] > 1e-12 && this.level[v] === this.level[u] + 1) { stack.push(v); edges.push(e); advanced = true; break; } }
if (!advanced) { stack.pop(); if (edges.length) { const e = edges.pop(); this.it[stack[stack.length - 1]] = this.next[e]; } else break; }
}
if (!found) break;
let f = Infinity; for (const e of edges) f = Math.min(f, this.cap[e]);
for (const e of edges) { this.cap[e] -= f; this.cap[e ^ 1] += f; }
flow += f;
}
}
return flow;
}
Ultimate pit
64-bit
The set of blocks to mine for the highest total value within the slope, with its value, ore and waste tonnes.
reach per layer = dz ÷ tan(slope); blocks above within round(reach ÷ dx), round(reach ÷ dy) and hypot ≤ reach
The slope is applied as whole blocks one layer up, with the reach rounded to a block count, so the pit wall follows the block size. The pit is the source side of the cut, read with a threshold of 1e-9, a different absolute value from the 1e-12 used while finding the flow. Value and tonnes are plain sums over mined blocks.
mining.js
lines 94 to 120
CivilCraft 0.8.1
/** The ultimate pit: the most valuable set of blocks that can be mined within the slope. `values` per block; `slope` in radians; pattern 8 or 4 neighbours per layer. */
export function ultimatePit(model, values, { slope = 45 * DEG, pattern = 8 } = {}) {
const { nx, ny, nz, size } = model, n = nx * ny * nz, S = n, T = n + 1, g = new Flow(n + 2), INF = 1e18;
// precedence: mining block (i,j,k) needs the blocks above it inside the cone, one layer at a time (the 1:n:… pattern generalised to any angle)
const reach = (dz) => (dz * size[2]) / Math.tan(slope); // horizontal reach at dz layers above
for (let k = 0; k < nz - 1; k++) {
const r = reach(1), rx = Math.round(r / size[0]), ry = Math.round(r / size[1]);
for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) {
const u = model.index(i, j, k);
for (let dj = -ry; dj <= ry; dj++) for (let di = -rx; di <= rx; di++) {
if (pattern === 4 && di !== 0 && dj !== 0) continue;
if (Math.hypot(di * size[0], dj * size[1]) > r + 1e-9) continue;
const ii = i + di, jj = j + dj; if (ii < 0 || jj < 0 || ii >= nx || jj >= ny) continue;
g.add(u, model.index(ii, jj, k + 1), INF);
}
}
}
let pos = 0;
for (let b = 0; b < n; b++) { if (values[b] > 0) { g.add(S, b, values[b]); pos += values[b]; } else if (values[b] < 0) g.add(b, T, -values[b]); }
const flow = g.maxflow(S, T);
// the source side of the minimum cut is the closure
const seen = new Uint8Array(n + 2), q = [S]; seen[S] = 1;
for (let h = 0; h < q.length; h++) { const u = q[h]; for (let e = g.head[u]; e !== -1; e = g.next[e]) if (g.cap[e] > 1e-9 && !seen[g.to[e]]) { seen[g.to[e]] = 1; q.push(g.to[e]); } }
const mined = new Uint8Array(n); let value = 0, ore = 0, waste = 0, blocks = 0;
for (let b = 0; b < n; b++) if (seen[b]) { mined[b] = 1; value += values[b]; blocks++; if (values[b] > 0) ore += model.volume * model.density[b]; else waste += model.volume * model.density[b]; }
return { mined, value, ore, waste, blocks, flow, bound: pos - flow };
}
Grade and tonnage curve
64-bit
The tonnes and mean grade of everything at or above each cut-off grade.
tonnes = Σ t; grade = Σ (t × g) ÷ Σ t
Two running sums over the blocks in index order; the mean is 0 when there are no tonnes.
mining.js
lines 122 to 128
CivilCraft 0.8.1
export function gradeTonnage(model, cutoffs, mined = null) {
return cutoffs.map((c) => {
let t = 0, m = 0;
for (let b = 0; b < model.grade.length; b++) if ((!mined || mined[b]) && model.grade[b] >= c) { const tt = model.volume * model.density[b]; t += tt; m += tt * model.grade[b]; }
return { cutoff: c, tonnes: t, grade: t ? m / t : 0 };
});
}
Bench report
64-bit
Ore and waste tonnes, mean grade and strip ratio for each bench.
strip = waste ÷ ore; z = origin_z + (k + 1) × dz
Running sums per bench. The strip ratio is infinite when a bench has no ore. Bench elevations come from the whole-number layer index.
mining.js
lines 130 to 141
CivilCraft 0.8.1
export function benchReport(model, mined, values, { benchBlocks = 1 } = {}) {
const out = [];
for (let k0 = model.nz - 1; k0 >= 0; k0 -= benchBlocks) {
let ore = 0, waste = 0, gm = 0;
for (let k = Math.max(0, k0 - benchBlocks + 1); k <= k0; k++) for (let j = 0; j < model.ny; j++) for (let i = 0; i < model.nx; i++) {
const b = model.index(i, j, k); if (!mined[b]) continue; const t = model.volume * model.density[b];
if (values[b] > 0) { ore += t; gm += t * model.grade[b]; } else waste += t;
}
if (ore || waste) out.push({ level: k0, z: model.origin[2] + (k0 + 1) * model.size[2], ore, waste, grade: ore ? gm / ore : 0, strip: ore ? waste / ore : Infinity });
}
return out;
}
Ramp length
64-bit
The length of ramp needed to climb a number of benches at a grade.
per bench = H ÷ grade; total = per bench × benches
This is the horizontal run (rise over grade), not the length along the slope. A grade of zero or less gives infinity.
mining.js
lines 160 to 161
CivilCraft 0.8.1
/** Length of ramp (m) to climb `benches` benches of height `benchHeight` at `grade` (rise over run). */
export function rampLength({ benchHeight, grade, benches = 1 }) { const per = grade > 0 ? benchHeight / grade : Infinity; return { perBench: per, total: per * benches }; }
Production schedule and net present value
64-bit
Assigns blocks to periods within ore and movement capacities and discounts each period's cash flow.
PV_t = cash_t ÷ (1 + discount)^t; NPV = Σ PV_t
Tonnes and cash accumulate per period as running sums; capacity tests compare the running total plus the next block with the limit, with no tolerance. The discount factor uses Math.pow with a whole-number period. The period of each block is stored as a 16-bit whole number.
mining.js
lines 163 to 183
CivilCraft 0.8.1
/**
* Production schedule over nested pits. `pits` from nestedPits(); `values` are the block values of the LAST (full-price) factor, used for the cash flow.
* oreCapacity: tonnes of ore per period (the plant); moveCapacity: tonnes of ore + waste per period (the fleet). Either may be Infinity.
*/
export function schedule(model, pits, values, { oreCapacity = Infinity, moveCapacity = Infinity, discount = 0.1, periods = 60 } = {}) {
const n = model.nx * model.ny * model.nz, per = new Int16Array(n).fill(-1), order = [], seen = new Uint8Array(n);
for (const p of pits) {
for (let k = model.nz - 1; k >= 0; k--) for (let j = 0; j < model.ny; j++) for (let i = 0; i < model.nx; i++) { const b = model.index(i, j, k); if (p.result.mined[b] && !seen[b]) { seen[b] = 1; order.push(b); } }
}
const rows = []; let t = 0, ore = 0, mov = 0, waste = 0, gm = 0, cash = 0, unscheduled = 0;
const close = () => { if (!ore && !waste) return; const pv = cash / Math.pow(1 + discount, t + 1); rows.push({ period: t + 1, ore, waste, grade: ore ? gm / ore : 0, cash, pv }); t++; ore = mov = waste = gm = cash = 0; };
for (const b of order) {
const tn = model.volume * model.density[b], isOre = values[b] > 0;
if (tn > oreCapacity || tn > moveCapacity) { unscheduled++; continue; } // a block bigger than a period's capacity can never be mined
if ((isOre && ore + tn > oreCapacity) || mov + tn > moveCapacity) { close(); if (t >= periods) { unscheduled += order.length; break; } }
if (t >= periods) { unscheduled++; continue; }
per[b] = t; mov += tn; cash += values[b]; if (isOre) { ore += tn; gm += tn * model.grade[b]; } else waste += tn;
}
close();
return { periods: rows, npv: rows.reduce((s, r) => s + r.pv, 0), undiscounted: rows.reduce((s, r) => s + r.cash, 0), period: per, unscheduled };
}
In detail
Method
Inverse distance weighting (the file cites Shepard 1968); block value = tonnes × (grade × recovery × price − process cost) − tonnes × mining cost; ultimate pit as a maximum-weight closure by Dinic's maximum flow (the file cites Lerchs and Grossmann 1965, Picard 1976, Dinic 1970); overall slope atan(H ÷ (H ÷ tan α + B)).
Number types
Float64Array for density, grade and value; Uint8Array flags; Int16Array period per block.
Tolerances
A sample within 1e-9 of a block center gives its grade exactly. Flow arcs count as open above 1e-12 while the flow is found and above 1e-9 when the pit is read from the cut; precedence arcs have capacity 1e18. Cone reach test 1e-9.
Limits
The slope cone is rounded to whole blocks per layer. The flow thresholds are absolute currency amounts and are not scaled to the block values. A block with no sample in range gets grade 0.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.