Resources Numerical accuracy Calculation policy

Block model, ultimate pit and mine schedule

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.

mining.js CivilCraft 0.8.1 64-bit 10 calculations Reviewed

Tests bin/test-civilcraft-mining.mjs

Overall slope angle

64-bit

The overall pit slope from bench height, face angle, berm width and ramp.

OSA = atan(H ÷ (H ÷ tan α + B + ramp × share))

One 64-bit expression. A face angle of 0 makes tan α zero and the run infinite, which returns an angle of 0.

  • mining.js
  • lines 32 to 35
  • CivilCraft 0.8.1
export function overallSlope({ benchHeight, faceAngle, berm, ramp = 0, rampShare = 1 }) {
  const run = benchHeight / Math.tan(faceAngle) + berm + (ramp ? (ramp * rampShare) : 0);
  return Math.atan(benchHeight / run);
}

Block grid

64-bit

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.

Every policy that takes this approach