Resources Numerical accuracy Calculation policy

Radio coverage over the terrain

Works out the received power from a transmitter at every cell of a grid over the terrain and the share of cells that are covered.

How the precision is kept

Each cell's received power is computed in 64-bit and then stored in a Float32Array, which is what the coverage map, the reach and the values handed to a connected assistant read back. The covered share is counted from the 64-bit value before it is stored. The method itself is approximate: free-space loss plus the loss of the single worst obstruction on the path, found at up to 40 samples.

Open item. Store the received power in a Float64Array so the map, the reach and the reported cell values use the same 64-bit numbers as the covered share.

Calculations under this policy

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

telecom.js CivilCraft 0.8.1 32-bit in part 2 calculations Reviewed

Tests bin/test-civilcraft-coverage.mjs, bin/test-civilcraft-telecom.mjs

Received power at each grid cell

32-bit in part

The received power at every cell around the transmitter and the share of cells at or above the receiver's sensitivity.

Pr = Pt + Gt + Gr − (FSPL + J(v_max) + other); covered = cells with Pr ≥ sensitivity ÷ known cells

The power is computed in 64-bit and written into a Float32Array. The covered share compares the 64-bit value with the sensitivity, while code that reads the grid afterwards compares the 32-bit value, so a cell within rounding of the threshold can be counted one way and drawn the other. Cell positions are x₀ + i × step, one multiplication each.

  • telecom.js
  • lines 119 to 146
  • CivilCraft 0.8.1
/**
 * The received power from a transmitter at every point of a square grid around it, over the terrain. `ground(x, y)` → m or NaN (unknown: the cell is left out).
 * Each cell: free-space loss + the knife-edge loss of the worst obstruction on the straight path (the main-edge method; several edges add more loss in
 * reality, so the picture is a little optimistic where the path crosses many ridges). Antennas stand `tx.h` and `rxH` above the ground, the earth bulge uses k.
 *   coverage(ground, tx{x,y,h}, { f (GHz), pt, gt, gr (dBi), sensitivity (dBm), rxH, radius (m), step (m), k, clutter, samples, other }) → { nx, ny, x0, y0, step, pr: Float32Array (NaN unknown), covered (fraction of known cells), cells }
 */
export function coverage(ground, tx, { f = 2, pt = 30, gt = 10, gr = 0, sensitivity = -90, rxH = 2, radius = 5000, step = 100, k = 4 / 3, clutter = 0, samples = 40, other = 0 } = {}) {
  const lam = C / (f * 1000), n = Math.max(1, Math.floor(radius / step)), nx = 2 * n + 1, pr = new Float32Array(nx * nx).fill(NaN), x0 = tx.x - n * step, y0 = tx.y - n * step;
  const gz = ground(tx.x, tx.y); if (!Number.isFinite(gz)) return { nx, ny: nx, x0, y0, step, pr, covered: 0, cells: 0 };
  const zA = gz + tx.h; let known = 0, ok = 0;
  for (let j = 0; j < nx; j++) for (let i = 0; i < nx; i++) {
    const x = x0 + i * step, y = y0 + j * step, D = Math.hypot(x - tx.x, y - tx.y);
    if (D > radius + 1e-9) continue;
    const z = ground(x, y); if (!Number.isFinite(z)) continue;
    known++;
    const dKm = Math.max(D, step / 4) / 1000, zB = z + rxH, m = Math.max(2, Math.min(samples, Math.ceil(D / step * 2)));
    let vmax = -Infinity;
    for (let s = 1; s < m; s++) {
      const u = s / m, px = tx.x + (x - tx.x) * u, py = tx.y + (y - tx.y) * u, gp = ground(px, py); if (!Number.isFinite(gp)) continue;
      const d1 = D * u, d2 = D - d1, line = zA + (zB - zA) * u;
      const bulge = ((d1 / 1000) * (d2 / 1000)) / (12.74 * k), hh = gp + clutter + bulge - line, v = diffractionV(hh, d1, d2, lam);
      if (v > vmax) vmax = v;
    }
    const loss = fspl(dKm, f * 1000) + (vmax > -Infinity ? knifeEdgeLoss(vmax) : 0) + other, p = pt + gt + gr - loss;
    pr[i + nx * j] = p; if (p >= sensitivity) ok++;
  }
  return { nx, ny: nx, x0, y0, step, pr, covered: known ? ok / known : 0, cells: known };
}

Diffraction parameter

64-bit

The parameter that measures how far an obstruction rises into the path, used for the knife-edge loss.

v = h × √(2(d₁ + d₂) ÷ (λ·d₁·d₂))

One 64-bit expression. The wavelength is the exact speed of light divided by the frequency. The path samples exclude both ends, so d₁ and d₂ are not zero in the division.

  • telecom.js
  • lines 112 to 116
  • CivilCraft 0.8.1
/**
 * Single knife-edge diffraction loss, ITU-R P.526 (the closed approximation): v = h·√(2(d1+d2)/(λ·d1·d2)); J(v) = 6.9 + 20·log10(√((v − 0.1)² + 1) + v − 0.1) dB for v > −0.78, else 0.
 * h = how far the obstruction rises above the straight line (m, negative when the line clears it), d1, d2 in metres.
 */
export function diffractionV(h, d1, d2, lambda) { return h * Math.sqrt((2 * (d1 + d2)) / (lambda * d1 * d2)); }

In detail

Method
Free-space loss plus the knife-edge loss of the worst obstruction on the straight path (the main-edge method, as the file says), with the earth bulge added to the terrain.
Number types
64-bit numbers per cell; Float32Array for the stored grid of received power.
Tolerances
Cells farther than the radius plus 1e-9 are left out; the distance used for the loss is at least a quarter of the grid step.
Limits
Several obstructions add more loss than the single worst one, so the file itself calls the picture a little optimistic. The path is sampled at up to 40 points. Stored values carry about 7 significant digits.

Approach: 32-bit step, flagged

One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.

Every policy that takes this approach