Pad to pillar alignment¶
A die’s pads have to land on the Cu pillars or bumps actually drawn on the
interposer. Nothing in the geometric DRC deck checks that: it sees polygons on
fab layers and chiplet boundaries from a manifest, not the correspondence
between a die’s pad named VDD and the pillar that is supposed to carry it.
The gap matters because the drawn pillar positions are not simply the pad positions transformed by the placement. The producer that generates the pillars runs a collision auto-resolver, which may shift individual bumps to clear neighbouring geometry. A pillar that has moved far enough is an open circuit that looks perfectly clean in every geometric check.
checks/pads_vs_pillars.py closes that gap. It is a manifest-level check: it
transforms each die’s pad centres through that die’s .chiplet placement,
and compares the result against the as-drawn pillar centres recorded in the
assembly’s pillar manifest.
Note
The manifest’s x_um / y_um are authoritative for this check: they
are what the producer drew, after auto-resolve. The assembly GDS remains the
fabrication ground truth. A consumer must never prefer a re-derived pillar
position over the manifest. See Pillar manifest.
Inputs¶
Flag |
Input |
|---|---|
|
The finalised |
|
The pillar manifest |
|
A pad source for one die: a |
|
A pad source for one die, extracted from that die’s |
Each die takes at most one pad source; supplying both for the same reference is a usage error, as is repeating either flag for one reference.
The manifest is validated after the .chiplet is loaded, and before any pad
source is read or any matching runs. schema must be
adk-pillar-manifest and version must be exactly 1.0.0: an exact
string match, never a range. units must be um, every coordinate must be
a finite number, and diameter_um must be positive. Non-finite coordinates
are rejected at read time on purpose, because a comparison against NaN is
false and would make every distance check pass.
Extracting pads from a GDS¶
With --gds-pads the pads are the merged pad_drawing polygons of the
die’s layout, one pad per merged polygon, each centre being the polygon’s
bounding-box centre in micrometres relative to the GDS origin. Names come from
pad_text labels: a label contained in the pad polygon wins, the nearest one
when several are contained, and the nearest label overall otherwise. A die with
no labels yields unnamed pads, which is legal and shifts the matching to the
fallback strategy.
Layer numbers are read from config/chiplet_pads.json and are never
hardcoded. A die that carries no polygons on the pad_drawing layer is an
error telling you to supply --pins instead: it is probably not a black-box
pads-only chiplet.
The layout path is resolved the same way every ADK reader resolves paths.
${VAR} ecosystem-root references expand through the environment first and
then a sibling-checkout walk, and relative paths resolve against the
.chiplet file’s own directory.
The transform¶
Pad coordinates are die-local GDS micrometres, y-up, relative to the die GDS
origin. The die’s .chiplet position is where that origin lands in the
canonical interposer GDS-bbox-corner frame, which is the same frame the pillar
manifest coordinates live in. Each pad p therefore maps to:
with R a counter-clockwise rotation in that y-up frame and
M = diag(-1, 1) for a flip_chip die, identity for face_up.
Three details of that composition are easy to get wrong:
The mirror is applied before the rotation. The assembly generator places a flipped die as
translate * R(rotation.z) * mirror-x, and the pillar producer consumes footprint-frame pin lists that already carry the same mirror. This check reproduces that order.Arbitrary rotation angles are supported here, unlike the 3Dblox export which is restricted to quarter turns by its target format.
face_downis a hard error. It is not a canonical orientation token, so there is no mirror convention to apply; useflip_chip. This is the same policy as OpenROAD 3Dblox export.
Dies must declare anchor: gds_origin. Both bbox_center and an absent
anchor are hard errors, for the reasons
Coordinate frames sets out; getting it wrong shifts every pad
by the bbox-corner offset, which can turn a real match into an open with no other
signal. Every die written by gds_to_kicad and by the KiCad plugin writer
already emits the field.
Matching¶
Matching runs per die, in three passes.
1. By pin name. Entries whose pad name and pillar pin_name are both
non-empty are grouped by name and paired nearest-first inside each group, so
duplicate names pair deterministically. A pair beyond tolerance is
MISALIGNED. Two conflicting names never cross-match: they surface as a
leftover pad and a leftover pillar rather than as a plausible-looking pairing.
2. Nearest-unique fallback. Remaining entries where at least one side is unnamed are paired greedily by distance, within tolerance. This is what makes the check usable on dies whose pad labels were not preserved.
3. Leftovers. Anything still unpaired becomes PAD_WITHOUT_PILLAR or
PILLAR_WITHOUT_PAD.
Findings¶
Type |
Meaning |
|---|---|
|
A named pad and its same-named pillar are further apart than the tolerance. The finding carries both coordinate pairs and the distance. |
|
A pad has no matching pillar. Either the producer drew no bump for it, or the bump is far enough away that neither pass could pair them. |
|
A drawn pillar has no matching pad. The finding carries the pillar’s
|
|
The nearest-unique fallback cannot decide: two or more candidates sit at indistinguishable distance, within 1e-6 um of each other. All the involved pads and pillars are named in the finding and are removed from further matching rather than being paired arbitrarily. |
|
Only with |
Auto-resolve demotion¶
A named pair beyond tolerance is not automatically a defect. When the pillar
carries moved_by_auto_resolve: true and a recorded
auto_resolve_shift_um magnitude, the producer’s collision auto-resolver
shifted that bump away from its pad on purpose. In that case a deviation within
shift + tolerance is reported as a warning instead of a finding.
Beyond shift + tolerance the flag cannot explain the deviation and the pair
is MISALIGNED again, with the recorded shift included in the finding. The
bound matters: without it, a genuine placement bug that happens to hit a
collision-moved bump would be hidden at any distance.
A bare moved_by_auto_resolve boolean with no recorded magnitude does
not demote. It cannot bound the excuse, so it is advisory only. The ADK’s own
producer always records the magnitude alongside the flag, so real manifests are
unaffected; the rule closes an unbounded false pass for foreign or hand-edited
manifests. An unnamed moved pillar cannot be attributed to a pad at all and
still surfaces as a leftover.
Tolerance¶
--tolerance-um sets the pad-to-pillar distance tolerance; the default is
1.0 um. It is validated as a finite non-negative number before the per-die loop
runs, so a pipeline that interpolated a failed computation into the flag fails
here rather than going green. NaN in particular would make every comparison
pass.
Running the check¶
python checks/pads_vs_pillars.py \
--chiplet demo.chiplet \
--pillars build/demo_interposer.pillars.json \
--pins U1=chiplets/die_a.pins.json \
--gds-pads U2 \
--tolerance-um 1.0 \
--json build/pads_vs_pillars.json \
--strict
usage: pads_vs_pillars.py [-h] --chiplet CHIPLET --pillars PILLARS
[--pins REF=PINS_JSON] [--gds-pads REF]
[--tolerance-um TOLERANCE_UM] [--json PATH]
[--strict]
Verify die pad positions against the as-drawn pillar positions of the assembly's pillar manifest (<gds-stem>.pillars.json).
options:
-h, --help show this help message and exit
--chiplet CHIPLET finalized .chiplet file (die placements)
--pillars PILLARS pillar manifest (<gds-stem>.pillars.json)
--pins REF=PINS_JSON per-die pin list (gds_to_kicad *.pins.json), die-local
coordinates; repeatable
--gds-pads REF extract that die's pads from its .chiplet layout GDS
(pad_drawing/pad_text layers per
config/chiplet_pads.json); repeatable; needs
klayout.db
--tolerance-um TOLERANCE_UM
pad-to-pillar distance tolerance in um (default: 1.0)
--json PATH write the machine-readable report here
--strict dies with a connection method but no pad source become
findings instead of warnings
Examples:
python checks/pads_vs_pillars.py --chiplet demo.chiplet \
--pillars build/demo_interposer.pillars.json \
--pins U1=chiplets/die_a.pins.json --gds-pads U2
Warnings go to stderr prefixed with WARNING:; findings go to stdout as
<TYPE> [<ref>]: <message>; the run ends with a one-line summary:
pads_vs_pillars: 0 finding(s), 1 warning(s) across 2 checked device(s): PASSED
Warnings, not findings, are also how the check reports a pillar manifest that
references a device_ref which is not a die in the .chiplet, which
usually means the sidecar is stale.
The JSON report¶
--json PATH writes the machine-readable report. Its shape:
{
"tool": "pads_vs_pillars",
"tolerance_um": 1.0,
"strict": false,
"devices": {
"U1": {"pads": 48, "pillars": 48, "matched": 48, "findings": 0}
},
"findings": [],
"warnings": [],
"summary": {
"devices_checked": 1,
"findings": 0,
"warnings": 0,
"passed": true
},
"chiplet": "demo.chiplet",
"pillar_manifest": "build/demo_interposer.pillars.json"
}
Each finding object carries at least type, device_ref and message,
plus the coordinates, names, distance and, where relevant, the recorded
auto-resolve shift.
--strict¶
Without --strict, a die that declares a connection method but was given no
pad source produces a warning and the run can still pass. That is convenient
interactively and dangerous in CI, where “no pad source was supplied” and “the
pads are aligned” would produce the same green result. --strict turns those
into NO_PAD_SOURCE findings. Use it in CI.
Exit codes¶
Code |
Meaning |
|---|---|
|
Clean. No findings. |
|
Findings. Reached only through the deliberate return at the end of a completed run. |
|
Usage, validation or tooling error: a malformed |
The separation is structural, not incidental. The whole pipeline, including
report emission, runs inside one guard, and even an unexpected exception exits
2 with its traceback intact. Python’s default handler exits 1, which would be
indistinguishable from a MISALIGNED finding, so no unguarded exception is
allowed to reach it. CI can rely on that: exit 1 means the assembly has a
problem, exit 2 means the check could not be run.
Importable entry points¶
load_chiplet(path) -> dict
load_pillar_manifest(path) -> dict # validated, exact version pin
load_pads_config(path=None) -> dict # pad layer vocabulary
extract_gds_pads(gds_path) -> [{"name", "x_um", "y_um"}, ...]
transform_pads(component, pads) -> [{"name", "x_um", "y_um"}, ...]
match_device(ref, pads, pillars) -> (findings, matched_count)
run_check(assembly, manifest, die_pads) -> report dict
expand_path_vars(path) -> str # ${VAR} discovery expansion
run_check takes die_pads as die-local pad lists; the placement
transform is applied inside. match_device takes pads already in the global
frame. Both accept tolerance_um as a keyword argument, and match_device
accepts a warnings list; when no list is supplied, the auto-resolve
demotion does not apply and a moved pillar beyond tolerance stays a finding,
which keeps legacy importers conservative.
CheckError is the exception type for everything that maps to exit code 2.
It subclasses ValueError. As everywhere in the ADK, new options are
appended as trailing keyword arguments with defaults, so importers stay
source-compatible.