Reading and writing chip layout streams, flattening cell hierarchies, layer areas and the basic width, spacing and area checks.
How the precision is kept
GDSII stores coordinates as 32-bit integers in database units and real numbers with a 56-bit base-16 mantissa. Reading a real into a 64-bit number can round away up to 3 bits; writing one is exact. Flattened coordinates are 64-bit after rotation and scaling to user units. The width and spacing found by the checks are rounded to 4 decimals before they are stored in the problem.
Open item. Round coordinates to the nearest database unit, and check their range, before they are written with setInt32.
Calculations under this policy
12 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.
gds.jsShared shell, every app64-bit12 calculationsReviewed
The 56-bit mantissa is accumulated in a 64-bit number, which holds 53 bits, so up to 3 low bits are rounded as it is read. Dividing by 2⁵⁶ only changes the exponent.
gds.js
lines 14 to 16
export function real8(bytes, o) { // GDSII 8-byte real: sign, 7-bit excess-64 exponent (base 16), 56-bit mantissa
const s = bytes[o] & 0x80 ? -1 : 1, e = (bytes[o] & 0x7f) - 64; let m = 0; for (let i = 1; i < 8; i++) m = m * 256 + bytes[o + i]; return s * (m / 72057594037927936) * Math.pow(16, e);
}
Real number when writing
Exact
A number as an 8-byte GDSII real.
mantissa = round(v × 2⁵⁶) after scaling v into [1/16, 1) by powers of 16
Scaling by 16 only changes the binary exponent, and a 64-bit value has at most 53 significant bits, which fit in the 56-bit mantissa, so the conversion loses nothing. The exponent is not checked against the 7-bit range of the format.
gds.js
lines 17 to 20
export function toReal8(v) {
const out = new Uint8Array(8); if (v === 0) return out; let s = 0; if (v < 0) { s = 0x80; v = -v; } let e = 0; while (v >= 1) { v /= 16; e++; } while (v < 1 / 16) { v *= 16; e--; }
out[0] = s | (e + 64); let m = Math.round(v * 72057594037927936); if (m >= 72057594037927936) { m = Math.round(m / 16); out[0]++; } for (let i = 7; i >= 1; i--) { out[i] = m % 256; m = Math.floor(m / 256); } return out;
}
Database unit
64-bit
User units per database unit and meters per database unit, read from the file.
two 8-byte reals from the UNITS record
Both are read with the real-number routine above. Usual values such as 0.001 and 1e-9 have no exact form in base 16 or in binary, so they arrive as the nearest numbers the file and a 64-bit number can hold.
Coordinates, widths, layers and array counts as the file stores them.
coordinates and widths: 32-bit signed; layers and counts: 16-bit signed
DataView.setInt32 and setInt16 convert by dropping the fraction and wrapping modulo 2³² or 2¹⁶. A coordinate that is not a whole number loses its fraction, and one outside the range wraps, both without a message.
gds.js
lines 65 to 66
const i2 = (...v) => { const b = new Uint8Array(v.length * 2), dv = new DataView(b.buffer); v.forEach((x, i) => dv.setInt16(i * 2, x)); return b; }, i4 = (v) => { const b = new Uint8Array(v.length * 4), dv = new DataView(b.buffer); v.forEach((x, i) => dv.setInt32(i * 4, x)); return b; };
const s = (t) => { const b = new Uint8Array(t.length % 2 ? t.length + 1 : t.length); for (let i = 0; i < t.length; i++) b[i] = t.charCodeAt(i); return b; }, r8 = (v) => toReal8(v);
Placement transform
64-bit
A referenced cell's points reflected, magnified, rotated and moved.
(x, y) → mag × (x, ±y), rotated by the angle in degrees, then translated
The angle in degrees is multiplied by π ÷ 180. cos 90° comes out as 6.1e-17, not 0, so a cell placed at a right angle gets coordinates that differ from whole database units by about that times the coordinate.
gds.js
lines 86 to 89
const xf = (pts, e, ox, oy) => { // STRANS bit 0x8000 reflects about the x axis, then magnification, then rotation, then translation
const c = Math.cos(e.angle * Math.PI / 180), s = Math.sin(e.angle * Math.PI / 180), refl = e.strans & 0x8000 ? -1 : 1;
return pts.map(([x, y]) => { const px = x * e.mag, py = y * e.mag * refl; return [px * c - py * s + ox, px * s + py * c + oy]; });
};
Nested placements
64-bit
The combined transform of a cell placed inside a placed cell.
offset = ± half width along the corner bisector ÷ max(0.2, cos of the half angle)
Each point is offset half the width to both sides. At a corner the miter length is capped at 5 times the half width, so sharp corners are cut short. Round ends (path type 1) are drawn with square extensions, as the comment above the function says.
gds.js
lines 97 to 107
const pathPoly = (e) => {
const hw = Math.abs(e.width) / 2, pts = e.pts; if (pts.length < 2 || hw === 0) return null; const left = [], right = [];
for (let i = 0; i < pts.length; i++) {
const p = pts[i]; let dx, dy; const ext = e.pathtype === 2 || e.pathtype === 1;
const a = pts[Math.max(0, i - 1)], b = pts[Math.min(pts.length - 1, i + 1)]; dx = b[0] - a[0]; dy = b[1] - a[1]; const l = Math.hypot(dx, dy) || 1; dx /= l; dy /= l;
let px = p[0], py = p[1]; if (ext && (i === 0 || i === pts.length - 1)) { const sgn = i === 0 ? -1 : 1; px += sgn * dx * hw; py += sgn * dy * hw; }
let nx = -dy, ny = dx; if (i > 0 && i < pts.length - 1) { const d1 = [pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]], d2 = [pts[i + 1][0] - pts[i][0], pts[i + 1][1] - pts[i][1]], l1 = Math.hypot(...d1) || 1, l2 = Math.hypot(...d2) || 1, n1 = [-d1[1] / l1, d1[0] / l1], n2 = [-d2[1] / l2, d2[0] / l2], sx = n1[0] + n2[0], sy = n1[1] + n2[1], sl = Math.hypot(sx, sy) || 1, cos = (n1[0] * sx + n1[1] * sy) / sl; nx = sx / sl / Math.max(0.2, cos); ny = sy / sl / Math.max(0.2, cos); }
left.push([px + nx * hw, py + ny * hw]); right.push([px - nx * hw, py - ny * hw]);
}
return [...left, ...right.reverse()];
};
Database units to user units
64-bit
Flattened coordinates in the file's user unit, usually micrometers.
x_user = x_db × userPerDb
One multiplication by the unit read from the file. With the usual 0.001 the product is not always the nearest number to the decimal value; the difference is one rounding step.
gds.js
lines 129 to 130
const k = lib.userPerDb; // database units → user units
for (const s of shapes) s.poly = s.poly.map((p) => [p[0] * k, p[1] * k]); for (const t of texts) t.at = [t.at[0] * k, t.at[1] * k];
Shapes and area per layer
64-bit
How many shapes each layer has and their total area.
area = Σ |shoelace area|
Each polygon's area as drawn, overlaps counted twice. The shoelace sum uses absolute coordinates: for a 0.1-unit shape 5000 units from the origin the relative error is near 1e-16 × (5000 ÷ 0.1)², about 3e-7.
gds.js
lines 140 to 144
/** Count shapes and area per named layer: [{name, layer, datatype, shapes, area}] (area in user units squared, polygons as drawn). Unmapped layers are listed too. */
export function layerSummary(shapes, map) {
const t = new Map(); for (const s of shapes) { const k = s.layer + '/' + s.datatype; let r = t.get(k); if (!r) t.set(k, r = { name: layerName(map, s.layer, s.datatype), layer: s.layer, datatype: s.datatype, shapes: 0, area: 0, mapped: map.layers.some((x) => x.gds[0] === s.layer && x.gds[1] === s.datatype) }); r.shapes++; r.area += Math.abs(polyArea(s.poly)); }
return [...t.values()].sort((a, b) => a.layer - b.layer || a.datatype - b.datatype);
}
Width, spacing and area checks
64-bit
Shapes that are narrower, closer or smaller than a rule allows.
The comparisons use the full 64-bit values with an absolute allowance of 1e-12 user units. The width and spacing stored in the problem as found are rounded to 4 decimals first; the area found is stored unrounded. Shapes are checked as drawn, not merged.
gds.js
lines 146 to 173
/**
* Basic layout DRC on flattened shapes, per layer: minWidth, minSpace, minArea (user units). rules: [{layer: 'NAME', minWidth?, minSpace?, minArea?}], map names layers.
* Shapes are checked AS DRAWN (not merged): overlapping or touching shapes are treated as one for spacing (no violation) but each piece is width-checked on its own,
* so a wide region built from narrow overlapping pieces reports width; merge them first. Width = two antiparallel edges of one polygon that face each other across its
* inside, closer than minWidth. Notches inside one polygon are not spacing-checked.
*/
export function layoutDrc(shapes, map, rules) {
const out = [];
for (const r of rules) {
const m = map.layers.find((x) => x.name === r.layer); if (!m) { out.push(problem('layout.rule-layer', 'Rule for ' + r.layer + ' names a layer the map does not have.')); continue; }
const polys = shapes.filter((s) => s.layer === m.gds[0] && s.datatype === m.gds[1]).map((s) => s.poly), boxes = polys.map(bbox);
polys.forEach((P, i) => {
const a = Math.abs(polyArea(P));
if (r.minArea && a < r.minArea - 1e-12) out.push(problem('layout.min-area', r.layer + ' shape at (' + P[0].map((v) => +v.toFixed(3)).join(', ') + ') has area ' + +a.toFixed(4) + '; the minimum is ' + r.minArea + '.', { rule: 'minArea', layer: r.layer, found: a, req: r.minArea, at: P[0] }));
if (r.minWidth) { const w = narrowest(P, r.minWidth); if (w !== null) out.push(problem('layout.min-width', r.layer + ' shape at (' + P[0].map((v) => +v.toFixed(3)).join(', ') + ') is ' + +w.toFixed(4) + ' wide; the minimum is ' + r.minWidth + '.', { rule: 'minWidth', layer: r.layer, found: +w.toFixed(4), req: r.minWidth, at: P[0] })); }
});
if (r.minSpace) {
const cell = Math.max(r.minSpace * 4, 1), grid = new Map(), seen = new Set();
boxes.forEach((b, i) => { for (let x = Math.floor((b[0] - r.minSpace) / cell); x <= Math.floor((b[2] + r.minSpace) / cell); x++) for (let y = Math.floor((b[1] - r.minSpace) / cell); y <= Math.floor((b[3] + r.minSpace) / cell); y++) { const k = x + ',' + y; (grid.get(k) || grid.set(k, []).get(k)).push(i); } });
for (const l of grid.values()) for (let a = 0; a < l.length; a++) for (let b = a + 1; b < l.length; b++) {
const i = l[a], j = l[b], k = i + ':' + j; if (seen.has(k)) continue; seen.add(k);
const A = boxes[i], B = boxes[j]; if (A[0] - B[2] > r.minSpace || B[0] - A[2] > r.minSpace || A[1] - B[3] > r.minSpace || B[1] - A[3] > r.minSpace) continue;
const d = polyDist(polys[i], polys[j]); if (d > 0 && d < r.minSpace - 1e-12) out.push(problem('layout.min-space', r.layer + ' shapes are ' + +d.toFixed(4) + ' apart near (' + polys[i][0].map((v) => +v.toFixed(3)).join(', ') + '); the minimum is ' + r.minSpace + '.', { rule: 'minSpace', layer: r.layer, found: +d.toFixed(4), req: r.minSpace, at: polys[i][0] }));
}
}
}
return out;
}
Narrowest width of a shape
64-bit
The smallest distance between two edges that face each other across the inside.
distance from one edge's line to the other, for antiparallel edges that overlap
Edges count as antiparallel when the normalized cross product is at most 1e-6 in size and the dot product is not positive. Edges shorter than 1e-9 and overlaps of 1e-9 or less are skipped; these are absolute, in user units.
gds.js
lines 175 to 187
function narrowest(P, minW) { // smallest facing-edge distance below minW, else null
const E = polyEdges(P); let best = null; const eps = 1e-9;
for (let i = 0; i < E.length; i++) for (let j = i + 1; j < E.length; j++) {
const [a, b] = E[i], [c, d] = E[j], u = [b[0] - a[0], b[1] - a[1]], v = [d[0] - c[0], d[1] - c[1]], lu = Math.hypot(...u), lv = Math.hypot(...v); if (lu < eps || lv < eps) continue;
const cr = (u[0] * v[1] - u[1] * v[0]) / (lu * lv), dt = (u[0] * v[0] + u[1] * v[1]) / (lu * lv); if (Math.abs(cr) > 1e-6 || dt > 0) continue; // antiparallel only
const n = [-u[1] / lu, u[0] / lu], dist = Math.abs((c[0] - a[0]) * n[0] + (c[1] - a[1]) * n[1]); if (dist >= minW - 1e-12 || dist < eps) continue;
// overlap of the projections along the edge direction
const t = (p) => ((p[0] - a[0]) * u[0] + (p[1] - a[1]) * u[1]) / lu, lo = Math.max(0, Math.min(t(c), t(d))), hi = Math.min(lu, Math.max(t(c), t(d))); if (hi - lo <= eps) continue;
const mid = (lo + hi) / 2, pa = [a[0] + u[0] / lu * mid, a[1] + u[1] / lu * mid], side = (c[0] - a[0]) * n[0] + (c[1] - a[1]) * n[1] > 0 ? 1 : -1, pm = [pa[0] + n[0] * side * dist / 2, pa[1] + n[1] * side * dist / 2];
if (pointInPoly(pm, P)) best = best === null ? dist : Math.min(best, dist);
}
return best;
}
In detail
Method
Excess-64 base-16 reals; integer coordinate records; reflection, magnification, rotation and translation composed down the hierarchy; shoelace area; facing antiparallel edges for width; edge-to-edge distance on a grid of cells for spacing.
Number types
64-bit numbers; 16-bit and 32-bit integers in the file (DataView).
Tolerances
Area, width and spacing limits compared with a 1e-12 allowance (absolute, user units). Width check: edges shorter than 1e-9 skipped, antiparallel when the normalized cross product is at most 1e-6, overlap above 1e-9.
Limits
The writer stores coordinates with setInt32, which drops any fraction and wraps beyond ±2³¹ without a check. Rotations use cos and sin of degrees, so a 90° placement leaves coordinates a rounding step off the grid. Path corners are mitered up to 5 times the half-width; round ends are drawn as square extensions.
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.