This directory holds the oracle harness that verifies klayout-rs produces
the same results as KLayout's reference C++ engine (via klayout.db).
cargo test -p klayout-validate --test parity_report -- --nocapture
prints a Markdown table summarising pass/fail counts per suite. As of
the last corpus regen (KLayout 0.30.8 + OpenROAD 26Q2): 4893 / 4893
cases pass (100.00%) across two oracles — klayout.db for layout
formats and openroad/orfs (OpenSTA / OpenDB) via Docker for Liberty,
LEF, DEF, and SPEF. Strict
vertex-level equality (no point-set or symmetric-difference fudge —
every polygon must match the reference vertex-for-vertex after
canonicalising to lowest-y/lowest-x first). The parity_report test
is the regression gate — if any case diverges, the test fails.
Suites with n/a in the report need a corpus regen (run
oracle.py <suite> to populate them — requires klayout.db install).
Suites without an oracle (LEF/DEF/Liberty/SPEF/CIF/DXF/MAG) are listed
at the bottom of the report with an explanation of why.
oracle.pyruns against a realklayout.dbinstall, exercisingdb.Trans,db.Box,db.Layoutetc. with a fixed-seed PRNG. It serializes inputs + KLayout's results into JSON and writes them tocorpus/.klayout-validateis a pure-Rust crate. Its tests load the JSON corpus and assert klayout-rs produces the same outputs — exact integer equality, no tolerance.
CI does not need klayout.db installed: the corpus is checked in and the Rust tests are self-contained. Regenerate the corpus only when:
- You add new oracle cases.
- You upgrade the reference KLayout version (and want to verify the new version against your impl).
- A test fails and you suspect the corpus is wrong (rare).
# One-time: install klayout in a venv (do NOT use system pip)
python3 -m venv .venv
.venv/bin/pip install klayout
# Regenerate everything (klayout.db oracle)
.venv/bin/python validation/oracle.py
# Or just one suite
.venv/bin/python validation/oracle.py trans
.venv/bin/python validation/oracle.py bbox
.venv/bin/python validation/oracle.py gdsTo regenerate against a pinned KLayout built inside Docker (same git ref as
validation/docker/klayout-git-ref), build the image and run the oracle with a
writable corpus dir — see validation/docker/README.md.
For formats KLayout can't read (Liberty, soon SPEF / LEF / DEF), the oracle is OpenSTA / OpenDB run inside Docker:
# One-time: pull the image (~5GB, AMD64-only — runs under emulation
# on Apple Silicon).
docker pull --platform linux/amd64 openroad/orfs:latest
# Regenerate the Liberty corpus.
python3 validation/oracle_external.py libertyCI does not need Docker — the corpus JSON is checked in.
corpus/drc_density_grid.json is a KLayout-driven Cartesian product
of tiling knobs (padding_zero / padding_ignore, with/without density,
tile_origin, tile_count, singleton frame, window vs step). It is not
the full continuous (coordinates × floats) space — expand gen_drc_density_grid()
in oracle.py when you need more coverage. Regenerate:
.venv/bin/python validation/oracle.py drc_density_gridHeavyweight integration checks (network + OpenROAD-flow) live in
validation/docker/README.md:
./validation/docker/run_benchmark.sh pdk-smoke
./validation/docker/run_benchmark.sh openroad-gcd-synthRebuild the pinned-AMD64 oracle image with:
docker build -t klayout-rs-oracle:latest validation/docker/| Suite | Cases | What's verified |
|---|---|---|
trans.json |
~1500 | Trans::apply, compose, inverse vs db.Trans |
bbox.json |
~2000 | Bbox::union, intersection, contains, apply_bbox |
gds/*.gds — reader |
14 files | KLayout writes, our reader matches canonical dump |
| writer — round-trip | 11 cases | We write, KLayout reads, dumps match our in-memory canonical |
region.json |
24 cases | Boolean ops + size vs db.Region (point-set equivalence) |
drc.json |
40 cases | DRC primitives + density_window fixtures vs db.Region checks / TilingProcessor |
drc_density_grid.json |
1536 cases | density_window combinatorial grid (same oracle as drc) |
oasis/*.oas — reader |
3 files | KLayout writes OASIS, our reader matches canonical dump |
polygon_ops.json |
60 cases | area, perimeter, bbox vs db.Polygon |
Reader fixtures cover: single box, mixed shapes (polygon/path/text), hierarchical
SREFs with rotation+mirror, regular AREF, all 8 SREF orientations, every PathCap
variant, irregular polygons, multi-layer cells, 3-level hierarchy, empty cells,
rotated/mirrored AREF, multiple text labels, mixed shapes-and-instances.
Writer fixtures cover the same surface in the opposite direction, exercised
by writing GDS via klayout-io and verifying KLayout (subprocess) reads
back something that dumps identically to our in-memory canonical view.
These are real KLayout behaviors we matched after the corpus revealed divergences. Documented here so we don't re-discover them later.
Trans.rotis a single 0..7 code combining rotation and mirror (0..3 = r0/r90/r180/r270, 4..7 = m0/m45/m90/m135). The constructor takes(rot, mirror, dx, dy)separately but the field combines them.db.Box(0, 0, 0, 0)is not empty. Empty is reserved for the default-constructeddb.Box()(sentinel left=right=bottom=top=0 combined with internal flags — it's not directly representable as a 4-tuple of coordinates). KLayout'sbox.width()on the empty sentinel returns u32-wrap garbage; ignore it.- KLayout writes
db.Boxshapes asBOUNDARYrecords in GDS (5 closed points), not asBOXrecords. On read it reclassifies axis-aligned 4-vertex polygons back toBox. Our reader does the same. - KLayout writes
LIBNAME = "LIB"by default, even when the in-memorylayout.libnameis empty. (libnamemay not even be exposed as a Python attribute —hasattr(layout, "libname")was False on 0.30.8.) We don't compare LIBNAME in the GDS test. CellInstArray.a/.bare not the GDS COLROW columns / rows in the order you'd expect. After read, KLayout assigns:a <- (XY[2] - origin) / na— the GDS "Y-direction" / num_rowsb <- (XY[1] - origin) / nb— the GDS "X-direction" / num_cols So(a, na)is the slow axis,(b, nb)is the fast one. Our validation mapsRepetition::Regular { col, row, n_cols, n_rows }through(a=row, b=col, na=n_rows, nb=n_cols)to match.- GDS UNITS encoding precision.
1e-6 / 1000.0 != 1e-9in IEEE f64 (one ULP off). The naive form makes our writer encode bytes KLayout reads as0.0009999999999999998instead of0.001. We useuser_unit * 1e-6instead, which is bit-exact fordbu = 1000. - Polygon canonicalization. KLayout normalizes hulls to clockwise
winding starting from the lowest-y/lowest-x vertex on database insert.
Our
Polygon::from_hulldoes the same so cross-tool comparisons see byte-equal vertex sequences.from_hull_rawskips this for the rare case where vertex order matters. - Path extension reporting. KLayout reports effective endpoint
extensions as
width/2forPATHTYPE 1(round) andPATHTYPE 2(extended-square), and the user-set values forPATHTYPE 4(custom). Our canonical dump derives extensions the same way for parity. - Region topology canonicalisation.
i_overlayproduces disjoint simple polygons by default. KLayout's boolean ops produce polygon-with-holes (XOR of overlapping rects = one polygon-with-hole whose hull traces the union outline and whose hole is the overlap). The validation harness'sklayoutizepass detects pairs of disjoint polygons that share two vertices and merges them by stitching outer arcs into a hull and inner arcs into a hole — producing KLayout's representation byte-for-byte. Compared with strict vertex equality. - Hole winding convention. Our
Polygonstores hulls as CW and holes as CCW so winding-number rules (FillRule::NonZero) correctly subtract holes.Polygon::add_holenormalizes input hole windings; rawpolygon.holes.push(...)skips normalization and is unsafe. db.Region.sizedmode. Mode 0 (Acute) and mode 1 (Square) chamfer corners. Mode 2 (Octagon) and mode 3 (Round) preserve sharp corners on axis-aligned input. OurSizeJoin::Mitermatches mode 2/3.- DRC width uses exact edge-pair analysis (matches KLayout).
Even-DBU thresholds work correctly —
width(p, 10)with a polygon of width exactly 10 returns empty, matching KLayout's strict less-than. Concave-corner triangles are emitted via theconcave_cornerscross-product test plus chamfer-length extension, matching KLayout's edge-pair output vertex-for-vertex. - DRC chamfer formula. For
enclosing,overlap, and concave-corner width extensions, KLayout usesround(sqrt(min² - dist²))as the parallel-axis chamfer length: the chamfer endpoint sits at perpendicular distance exactlyminfrom the inner edge endpoint. Now matches. - OASIS plist-type 4 g-delta encoding. Form-1 (raw bit 0 = 0):
octangular delta,
dir = (raw >> 1) & 7,mag = raw >> 4. Form-2 (raw bit 0 = 1): non-octangular delta,dx_sign = (raw >> 1) & 1,dx_mag = raw >> 2, then a signed-int for dy. Form-2 only valid in type 4 / type 5 lists. KLayout switches to type-4 the moment any edge is non-octangular; reader must handle both forms. - OASIS manhattan polygon implicit close. Type 0/1 lists encode
count = N - 2deltas; the last 2 edges are implicit (perpendicular, closing back to start). Reader materialises one extra delta to land on vertex N-1; the close back to vertex 0 is implicit in the hull. - OASIS PROPERTY info-byte UUUUVCNS. C[2] = 1 means propname is explicit (follows), not modal — a common mis-read of the spec. N[1] selects refnum vs name-string when C=1. V[3] = 1 means modal value-list (no values follow).
- OASIS PLACEMENT info-byte CNXYRAAF. C[7] = 1 means name is explicit; N[6] (only meaningful when C=1) selects refnum vs name-string. The two bits aren't independent "string" / "refnum" alternatives.
- DRC concave-corner extensions. At inside corners (interior angle
180°), KLayout's edge-pair check extends width violations along the longer edge by
chamfer_lenpast the projection — capturing the triangle-shaped near-corner region that erosion-based DRC misses. Detection: vertices where the cross product of incoming × outgoing edge vectors is positive (left turn on a CW polygon).
validation/
├── README.md # this file
├── oracle.py # generates the corpus from klayout.db
├── corpus/ # checked-in fixtures (regenerable)
│ ├── trans.json
│ ├── bbox.json
│ └── gds/
│ ├── single_box.{gds,json}
│ ├── mixed.{gds,json}
│ ├── hierarchy.{gds,json}
│ └── aref.{gds,json}
└── klayout-validate/ # pure-Rust validator crate
├── Cargo.toml
└── tests/
├── trans.rs
├── bbox.rs
└── gds.rs