Resources Numerical accuracy Calculation policy

Board placement and routing

Where parts are placed and where tracks and vias run on a board, with the wire length reported.

How the precision is kept

Placement is simulated annealing and routing is a grid search: both are heuristics whose result depends on the seed, the iteration count and the grid pitch, not on rounding. Final part positions are rounded to 0.001 mm. Track corners lie on grid points computed in 64-bit; the search keeps its path costs in a 32-bit array, which decides between paths but is not part of any coordinate.

Open item. Keep the router's path costs in a Float64Array.

Calculations under this policy

8 calculations in 2 source files. 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.

place.js Shared shell, every app 64-bit 4 calculations Reviewed

Tests bin/test-engines.mjs, tools/pcb-test/test.mjs

Half-perimeter wire length

64-bit

The estimate of total connection length reported before and after placement.

Σ over nets of (x_max − x_min) + (y_max − y_min)

A plain 64-bit sum over the nets of the bounding boxes of their pads.

  • place.js
  • lines 12 to 17
/** Half-perimeter wire length of every net at the current positions (mm). */
export function hpwl(board) {
  const nets = new Map();
  for (const p of board.parts) for (const pad of p.pads) { const n = p.padNets[pad.num]; if (!n) continue; const w = board.padWorld(p, pad); if (!nets.has(n)) nets.set(n, []); nets.get(n).push([w.x, w.y]); }
  let t = 0; for (const pts of nets.values()) if (pts.length > 1) { const b = bbox(pts); t += (b[2] - b[0]) + (b[3] - b[1]); } return t;
}

Overlap and outline penalties

64-bit

The cost added when courtyards overlap or reach outside the board.

cost = 50 × (overlap area + area outside the margin)

Courtyard boxes are axis-aligned bounding boxes grown by half the gap. The weight 50 is a fixed number; the penalties steer the search and are not reported.

  • place.js
  • lines 43 to 44
const outPen = (b) => { const dx = Math.max(0, ob[0] + edgeMargin / 2 - b[0]) + Math.max(0, b[2] - (ob[2] - edgeMargin / 2)), dy = Math.max(0, ob[1] + edgeMargin / 2 - b[1]) + Math.max(0, b[3] - (ob[3] - edgeMargin / 2)); return dx * (b[3] - b[1]) + dy * (b[2] - b[0]); };
const partCost = (pi) => { let c = outPen(boxes[pi]) * 50; for (let j = 0; j < boxes.length; j++) if (j !== pi && board.parts[j].side === board.parts[pi].side) c += overlap(boxes[pi], boxes[j]) * 50; return c; };

Annealing schedule

64-bit

How the search cools from large moves to small ones.

T₀ = max(1, 0.15 (W + H)); T_end = 0.002 (W + H); T ← T × (T_end ÷ T₀)^(1 ÷ iterations)

The temperature is multiplied by a constant factor each iteration. A move is accepted when it lowers the cost, or with probability e^(−Δ ÷ T) drawn from a seeded 32-bit generator, so the same seed gives the same board.

  • place.js
  • lines 48 to 49
let T = Math.max(1, (W + H) * 0.15) * 1, cur = wl + pen; const Tend = 0.002 * (W + H);
const decay = Math.pow(Tend / T, 1 / Math.max(1, iterations));

Final part positions

64-bit

The positions stored on the board when placement ends.

x, y = round(value × 1000) ÷ 1000

After the search every part position, locked parts included, is rounded to 0.001 mm and stored. The wire length reported as after is computed from the rounded positions.

  • place.js
  • lines 68 to 69
for (const p of board.parts) { p.x = Math.round(p.x * 1000) / 1000; p.y = Math.round(p.y * 1000) / 1000; }
return { before, after: hpwl(board), overlaps, outside, moved, problems };
route.js Shared shell, every app 64-bit 4 calculations Reviewed

Tests bin/test-engines.mjs, tools/pcb-test/test.mjs

Routing grid

64-bit

The grid of points tracks may pass through.

pitch = (track width + clearance) ÷ 2 by default; x = ox + i × pitch

Grid coordinates are computed from the index in 64-bit. The origin is the outline's corner less one pitch, so grid points do not in general fall on pad centers.

  • route.js
  • lines 21 to 24
const pitch = opts.pitch || (R.trackWidth + R.clearance) / 2, viaCost = opts.viaCost ?? 14, budget = opts.expansionBudget || 3e6, passes = opts.passes || 3, L = board.layers.length;
const ob = board.extent(), ox = ob[0] - pitch, oy = ob[1] - pitch, Wc = Math.ceil((ob[2] - ob[0]) / pitch) + 3, Hc = Math.ceil((ob[3] - ob[1]) / pitch) + 3, N = Wc * Hc;
const cx = (x) => ox + x * pitch, cy = (y) => oy + y * pitch, cellOf = (p) => [Math.round((p[0] - ox) / pitch), Math.round((p[1] - oy) / pitch)];
const slack = 0.25 * pitch, netId = new Map(); const nid = (n) => { if (!netId.has(n)) netId.set(n, netId.size); return netId.get(n); };

Obstacle map

64-bit

Which grid points a track or a via of another net may not use.

blocked when distance to copper < clearance + track width ÷ 2 + 0.25 × pitch

Each grid point's distance to each copper shape is computed in 64-bit and compared with the grown clearance. The quarter-pitch slack covers the straight segments between grid points, so the map keeps more distance than the rule asks by that amount.

  • route.js
  • lines 31 to 37
const raster = (it) => { // copper with no net blocks everyone (owner -2)
  const n = it.net ? nid(it.net) : -2, box = primBox(it.prim);
  for (const [arr, extra] of [[blk, R.clearance + R.trackWidth / 2 + slack], [blkVia, R.clearance + R.viaDia / 2 + slack]]) {
    const r = extra, x0 = Math.max(0, Math.floor((box[0] - r - ox) / pitch)), x1 = Math.min(Wc - 1, Math.ceil((box[2] + r - ox) / pitch)), y0 = Math.max(0, Math.floor((box[1] - r - oy) / pitch)), y1 = Math.min(Hc - 1, Math.ceil((box[3] + r - oy) / pitch));
    for (const l of it.layers) { const li = board.layers.indexOf(l); if (li < 0) continue; for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) if (pointPrimDist([cx(x), cy(y)], it.prim) < r) claim(arr[li], y * Wc + x, n); }
  }
};

Track points

64-bit

The corner points of a routed track.

drop a point when |(b − a) × (c − b)| ≤ 1e-9

Grid points on a straight run are removed with an absolute test on the cross product. A track that starts outside its pad gets one extra segment from the pad center to the first grid point.

  • route.js
  • lines 94 to 97
const pts = sg.map(([, x, y]) => [cx(x), cy(y)]); const simp = [pts[0]];
for (let k = 1; k + 1 < pts.length; k++) { const a = simp[simp.length - 1], b = pts[k], c = pts[k + 1]; if (Math.abs((b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0])) > 1e-9) simp.push(b); }
if (pts.length > 1) simp.push(pts[pts.length - 1]);
if (si === 0 && pointPrimDist(simp[0], tgt.p.prim) > 0) simp.unshift([tgt.p.pad.x, tgt.p.pad.y]); // only a start outside the pad needs a stub in

In detail

Method
Simulated annealing on half-perimeter wire length plus overlap and outline penalties (the file cites Kirkpatrick et al. 1983); wave routing on a multi-layer grid with 45° moves and vias (the file cites Lee 1961 and Hart, Nilsson and Raphael 1968).
Number types
64-bit numbers for positions and costs in placement; Int32Array obstacle maps, a Float32Array of path costs and Int32Array back-pointers in routing.
Tolerances
Overlap and outside counts use 1e-9. Collinear track points are dropped when the cross product is at most 1e-9. Diagonal step cost 1.4142.
Checked by
The engines test runs a three-part example and checks that all three nets are routed and the design rule check is clean.
Limits
Grid pitch defaults to (track width + clearance) ÷ 2; obstacles are grown by a quarter pitch of slack. Tracks start on the grid, with a stub from the pad center when the first grid point is outside the pad.

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