Resources Numerical accuracy Calculation policy

Running saved geotechnical, survey, pit and link items

Converts a saved item from project units to meters, runs the engine for it (soil profile, slope stability, settlement, traverse, pit, radio link, coverage) and converts the results back.

How the precision is kept

Lengths are multiplied by 0.3048 or 1 on the way in and divided on the way out, in 64-bit. The ground and the soil layers are sampled at a fixed number of points along a section, so the engines see a sampled copy of the model. Several thresholds are fixed numbers.

Calculations under this policy

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

geo-run.js CivilCraft 0.8.1 64-bit 9 calculations Reviewed

Tests bin/test-civilcraft-georun.mjs, bin/test-civilcraft-geo3.mjs, bin/test-civilcraft-pit.mjs

Project units to meters

64-bit

The factor that takes a project length to meters for every engine in this file.

feet: 0.3048; meters: 1

0.3048 is the exact international foot. units.js in the same build gives the project foot as 1200/3937 m, the US survey foot, which is larger by 2 parts per million. Results are converted back with the same factor, so a length that goes in and out is unchanged.

  • geo-run.js
  • lines 30 to 31
  • CivilCraft 0.8.1
export const FT = 0.3048;
export const toM = (units) => (units === 'ft' ? FT : 1);

Soil profile along a section

64-bit

The borings in meters, the layers between them, and the layer stack at evenly spaced stations.

stations = max(2, round(length ÷ step)), default step = max(length ÷ 40, 0.5 m)

Every boring coordinate, depth and layer limit is multiplied once by the unit factor. Stations are length × i ÷ n, not a running sum.

  • geo-run.js
  • lines 35 to 42
  • CivilCraft 0.8.1
export function runProfile(geo, sectionId, units, { step = null } = {}) {
  const sec = (geo?.sections || []).find((s) => s.id === sectionId); if (!sec) return null;
  const k = toM(units), borings = (geo.borings || []).map((b) => ({ ...b, x: b.x * k, y: b.y * k, z: b.z * k, depth: b.depth * k, water: b.water === null ? null : b.water * k, layers: b.layers.map((l) => ({ ...l, from: l.from * k, to: l.to * k })) }));
  const prof = sectionProfile(borings, pl(sec.line, k), { width: sec.width * k });
  const n = Math.max(2, Math.round(prof.length / (step ? step * k : Math.max(prof.length / 40, 0.5))));
  prof.stations = Array.from({ length: n + 1 }, (_, i) => { const sta = (prof.length * i) / n; return { sta, stack: stackAt(prof, sta) }; });
  return { section: sec, profile: prof, k };
}

Slope cross section for stability

64-bit

The ground line, layer tops, water level, surcharges and floor that the slope stability engine reads.

81 samples along the section; layer top = min(layer top, ground)

The ground is read from the surface at 81 evenly spaced points and joined by straight lines, so ground features between samples are not seen. The default floor is the lower of the deepest boring and the lowest ground minus half the ground's height range.

  • geo-run.js
  • lines 44 to 64
  • CivilCraft 0.8.1
export function slopeGeometry(geo, slope, units, ground, { points = 80 } = {}) {
  const p = runProfile(geo, slope.section, units); if (!p) return { problems: [{ code: 'geo.slope.nosection' }] };
  const { profile, k } = p, L = profile.length; if (!ground) return { problems: [{ code: 'geo.slope.noground' }] };
  const line = pl(p.section.line, k), pos = (d) => { let acc = 0; for (let i = 1; i < line.length; i++) { const a = line[i - 1], b = line[i], l = Math.hypot(b.x - a.x, b.y - a.y); if (d <= acc + l || i === line.length - 1) { const u = Math.max(0, Math.min(1, (d - acc) / l)); return { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u }; } acc += l; } return line[0]; };
  const gz = (d) => { const q = pos(d), v = ground.elevationAt(q.x / k, q.y / k); return v === null || v === undefined ? NaN : v * k; };
  const gp = []; for (let i = 0; i <= points; i++) { const d = (L * i) / points, z = gz(d); if (Number.isFinite(z)) gp.push({ x: d, z }); }
  if (gp.length < 3) return { problems: [{ code: 'geo.slope.noground' }] };
  // units from the profile (named strata, top first at each station); a unit's top line = its top at every station where it exists
  const units_ = []; for (const s of profile.strata) units_.push(s.unit);
  const topLine = (unit) => { const pts = []; for (let i = 0; i <= points; i++) { const d = (L * i) / points, st = stackAt(profile, d).find((q) => q.unit === unit); if (st) pts.push({ x: d, z: Math.min(st.top, gp[Math.min(i, gp.length - 1)].z) }); } return pts; };
  const mats = slope.materials || {}, byUnit = {};
  for (const b of geo.borings || []) for (const l of b.layers) if (!byUnit[l.unit]) byUnit[l.unit] = l;
  const layers = [];
  const order = units_.map((u) => ({ u, line: topLine(u) })).filter((q) => q.line.length).sort((a, b) => b.line.reduce((s, q) => s + q.z, 0) / b.line.length - a.line.reduce((s, q) => s + q.z, 0) / a.line.length);
  for (const { u, line: tl } of order) { const m = mats[u] || byUnit[u] || { gamma: 18, c: 0, phi: 28 }; layers.push({ name: u, top: tl.length > 1 ? tl : [{ x: 0, z: tl[0].z }, { x: L, z: tl[0].z }], gamma: m.gamma, c: m.c, phi: m.phi, ...(m.cu !== undefined ? { cu: m.cu } : {}) }); }
  if (!layers.length) layers.push({ name: 'soil', top: gp.map((q) => ({ ...q })), gamma: 18, c: 5, phi: 28 });
  layers[0].top = gp.map((q) => ({ ...q }));   // the first layer starts at the ground
  const floor = slope.floor !== null && slope.floor !== undefined ? slope.floor * k : Math.min(...profile.borings.map((b) => b.z - b.depth), Math.min(...gp.map((q) => q.z)) - 0.5 * (Math.max(...gp.map((q) => q.z)) - Math.min(...gp.map((q) => q.z))));
  const geom = { ground: gp, layers, water: slope.water === null || slope.water === undefined ? null : slope.water * k, ru: slope.ru || 0, surcharge: (slope.surcharge || []).map((s) => ({ x0: s.x0 * k, x1: s.x1 * k, q: s.q })), floor };
  return { geom, profile, k, problems: profile.problems || [] };
}

Slope stability run

64-bit

The factor of safety of a given circle, or the critical circle from a search.

A given circle is scaled to meters by the unit factor. The search uses a 14 by 10 grid of centers with 14 radii and 60 slices. A factor of safety below 1.5 is flagged.

  • geo-run.js
  • lines 66 to 72
  • CivilCraft 0.8.1
export function runSlope(geo, slope, units, ground, { search = true, circle = null, slices = 60 } = {}) {
  const g = slopeGeometry(geo, slope, units, ground); if (!g.geom) return { problems: g.problems };
  const o = { method: slope.method, kh: slope.kh, slices };
  if (circle) { const r = bishop(g.geom, { xc: circle.xc * g.k, zc: circle.zc * g.k, R: circle.R * g.k }, o); return { geom: g.geom, fs: r.fs, critical: { circle, fs: r.fs, xl: r.xl, xr: r.xr }, problems: r.problems, k: g.k }; }
  const r = searchSlip(g.geom, { ...o, nx: 14, nz: 10, nr: 14 });
  return { geom: g.geom, fs: r.critical ? r.critical.fs : NaN, critical: r.critical, grid: r.grid, problems: [...(r.problems || []), ...(r.critical?.problems || []), ...(r.critical ? (r.critical.fs < FS_TARGETS.long ? [{ code: 'geo.slope.low', value: r.critical.fs, limit: FS_TARGETS.long }] : []) : [])], k: g.k, tried: r.tried };
}

Settlement run

64-bit

Settlement under a footing at a boring, and the time for 50 % and 90 % consolidation.

layer top = (z − from) × k; drainage path = clay thickness ÷ 2

Layer elevations are the boring elevation minus the depth, times the unit factor. Sub-layers are 1 m at most. The time rate uses only the first layer that has both cv and Cc and assumes drainage at both faces. A total above 0.025 m is flagged whatever the user's limit.

  • geo-run.js
  • lines 74 to 85
  • CivilCraft 0.8.1
export function runSettlement(geo, s, units) {
  const b = (geo?.borings || []).find((x) => x.id === s.boring); if (!b) return { problems: [{ code: 'geo.settle.noboring' }] };
  const k = toM(units), layers = b.layers.map((l) => ({ top: (b.z - l.from) * k, bottom: (b.z - l.to) * k, gamma: l.gamma, gammaSat: l.gammaSat, cc: l.cc, cs: l.cs, e0: l.e0, sigmaC: l.sigmaC, es: l.es, nu: l.nu, cv: l.cv }));
  const water = s.water !== null && s.water !== undefined ? (b.z - s.water) * k : b.water !== null ? (b.z - b.water) * k : null;
  const base = layers[0].top + (s.load.depth || 0) * 0;
  const res = settlement({ layers, load: { q: s.load.q, B: s.load.B * k, L: s.load.L * k, depth: base - Math.max(s.load.depth, 0) * k, at: s.load.at, method: s.load.method }, water, ground: layers[0].top, step: 1 });
  const clay = b.layers.filter((l) => l.cv && l.cc);
  let time = null;
  if (clay.length && res.consolidation > 0) { const l = clay[0], H = ((l.to - l.from) * k) / 2, cv = l.cv; time = { t50: timeFor(0.5, H, cv), t90: timeFor(0.9, H, cv), curve: timeCurve(res.consolidation, H, cv, timeFor(0.95, H, cv), 20), Hdr: H, cv }; }
  const problems = res.total > 0.025 ? [{ code: 'geo.settle.total', value: res.total, limit: 0.025 }] : [];
  return { ...res, time, problems };
}

Traverse run

64-bit

A traverse closed by the compass rule from legs typed as distance and azimuth in degrees.

azimuth in radians = degrees × π ÷ 180

Azimuths are converted with the 64-bit value of π. A precision ratio below 1 : 5000 is flagged; that limit is fixed in the code.

  • geo-run.js
  • lines 116 to 121
  • CivilCraft 0.8.1
/** A traverse (start, legs of distance + azimuth in degrees, optionally a known end) closed by the compass rule. Lengths in the project unit. */
export function runTraverse(tv) {
  const r = bowditch(tv.start, tv.legs.map((l) => ({ dist: l.dist, az: l.az * D2R })), { end: tv.end || null });
  const problems = r.precision < 5000 ? [{ code: 'geo.traverse.loose', value: r.precision }] : [];
  return { ...r, problems };
}

Coverage run

64-bit

Received power over a grid round a transmitter and the farthest covered cell.

farthest = max over covered cells of √(Δx² + Δy²) ÷ k

The farthest distance is taken at cell positions, so it is known to the grid step. The number of cells across is a floor of range ÷ step.

  • geo-run.js
  • lines 162 to 172
  • CivilCraft 0.8.1
/** A coverage sketch: the received power over the ground around a transmitter (telecom.js coverage). `ground(x, y)` in the project unit, like runLink's. */
export function runCover(c, units, ground) {
  const k = toM(units), g = (x, y) => { const v = ground ? ground.elevationAt(x / k, y / k) : 0; return v === null || v === undefined ? NaN : v * k; };
  const n = Math.floor(c.range / c.step);
  if (n > 150) return { ok: false, problems: [{ code: 'tel.cover.fine', cells: (2 * n + 1) ** 2 }] };
  const r = coverage(g, { x: c.tx.x * k, y: c.tx.y * k, h: c.tx.h * k }, { f: c.f, pt: c.pt, gt: c.gt, gr: c.gr, sensitivity: c.sensitivity, rxH: c.rxH * k, radius: c.range * k, step: c.step * k, k: c.k, clutter: c.clutter * k });
  let far = 0; for (let j = 0; j < r.ny; j++) for (let i = 0; i < r.nx; i++) { const p = r.pr[i + r.nx * j]; if (p >= c.sensitivity) far = Math.max(far, Math.hypot(r.x0 + i * r.step - c.tx.x * k, r.y0 + j * r.step - c.tx.y * k)); }
  const problems = [];
  if (!r.cells) problems.push({ code: 'tel.cover.noground' });
  return { ok: true, ...r, farthest: far / k, problems };
}

Open pit run

64-bit

The block model in meters, block values, the ultimate pit and its phases.

slope in radians = degrees × π ÷ 180; phase factors from 0.4 to 1 in equal steps

Block sizes, origin and sample positions are multiplied by the unit factor. Grades are estimated by inverse distance with power 2 and no search radius. The phase price factors are fixed in the code.

  • geo-run.js
  • lines 123 to 140
  • CivilCraft 0.8.1
export function runPit(p, units) {
  const k = toM(units), model = makeModel({ nx: p.nx, ny: p.ny, nz: p.nz, size: p.size.map((v) => v * k), origin: p.origin.map((v) => v * k), density: p.density });
  idw(model, p.samples.map((s) => ({ x: s.x * k, y: s.y * k, z: s.z * k, grade: s.grade })), { power: 2, radius: Infinity });
  const values = blockValues(model, { price: p.price, recovery: p.recovery, processCost: p.processCost, miningCost: p.miningCost });
  const rampW = (p.rampWidth || 0) * k, slopeRad = p.rampOn === 'yes' && rampW > 0 ? slopeWithRamp(p.slope * D2R, p.size[2] * k, rampW) : p.slope * D2R;   // a ramp lane on every bench flattens the wall (spiral ramp)
  const result = ultimatePit(model, values, { slope: slopeRad });
  let phases = null, plan = null;
  if (p.phases > 0) {
    const n = Math.max(1, Math.round(p.phases)), factors = Array.from({ length: n }, (_, i) => (n === 1 ? 1 : 0.4 + (0.6 * i) / (n - 1)));
    phases = nestedPits(model, { price: p.price, recovery: p.recovery, processCost: p.processCost, miningCost: p.miningCost }, { factors, slope: slopeRad });
    if (p.oreCapacity > 0 || p.moveCapacity > 0) plan = schedule(model, phases, values, { oreCapacity: p.oreCapacity > 0 ? p.oreCapacity : Infinity, moveCapacity: p.moveCapacity > 0 ? p.moveCapacity : Infinity, discount: p.discount });
  }
  const summary = pitSummary(model, values, result), ramp = rampLength({ benchHeight: p.size[2] * k, grade: p.rampGrade || 0.1, benches: Math.max(0, summary.benches.length) });
  // the haul ramp laid on the pit's walls and the problems it raises (rule 29): the road is a sketch on the block model's stepped walls
  const road = rampW > 0 ? rampRoute(model, result.mined, { grade: p.rampGrade || 0.1, width: rampW }) : null;
  const problems = (road?.flags || []).filter((f) => f !== 'noRamp').map((code) => ({ code: 'pit.ramp.' + code, item: p.name || p.id }));
  return { model, values, result, phases, plan, ramp, road, problems, slopeUsed: slopeRad / D2R, summary, gradeTonnage: gradeTonnage(model, [0, 0.25, 0.5, 1, 2, 4].map((v) => v * (Math.max(...model.grade) / 4 || 1)), result.mined) };
}

In detail

Method
One conversion factor per run. Ground line for slope stability from 81 samples along the section. Settlement in sub-layers of at most 1 m. Traverse azimuths converted from degrees.
Number types
64-bit numbers.
Tolerances
Settlement flagged above 0.025 m. Slope flagged below a factor of safety of 1.5. Traverse flagged when perimeter ÷ misclosure is below 5000. Link margin flagged below 10 dB. Coverage refused above 150 cells of range.
Limits
Feet are converted with 0.3048 (the international foot), while units.js in the same build defines the project foot as 1200/3937 m; the two differ by 2 parts per million. Consolidation time uses the first clay layer only and takes its drainage path as half its thickness.

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