AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
place pack ignores sides: no spot for a bottom part under a top part (two-sided molecule)
adom-aiflow 0.2.57, place pack on a two-sided board
place pack finds no spot for a bottom-side part under a top-side part, although KiCad's own courtyard check (and place check) treats the two sides separately. On a two-sided board it refuses exactly the placements that make the board small.
Board: Ghost Hand rev 2 (john/ghost-hand), 30 x 22 mm molecule. The top keeps the five ICs and C15; 47 parts of 1.2 mm or less (0402/0603 passives, three SOT-23 FETs, a SOD-123F TVS) are on B.Cu (flipped at build), wished directly under the IC each one serves.
Evidence (scratch run, after the ICs were packed and landed):
OK: legal spots for 34 of 47 parts (no spot for C5, Q1, Q2, Q3, R1, R17, R2, R21, R28, R6, R7, R8, R9; left where they are now):
Q1: NO SPOT within 4.0 mm of (103.6, 94.8)
...
Every refused part sits under a top-side IC footprint. place check --moves also prints a "conservative bounding-box advisory" listing top/bottom pairs as courtyard overlaps (C11 U5, C13 U5, C14 U5, C15 Q2, ...), then reports the same moved board DRC-clean with all courtyards clear, which is KiCad's per-side verdict.
What I did instead: a small side-aware packer in KiCad's pcbnew (attached logic, below) that keeps one occupancy per side (through-hole footprints and every drilled pad count on both sides, drilled pads with 0.25 mm clearance because a USB-C shell tab sticks out past its courtyard). It placed all 47; place check passed; place land landed them (bottom parts stayed bottom).
Suggestion: in place pack (and the bounding-box advisory), test a candidate only against courtyards on the same side (F.CrtYd vs B.CrtYd), plus the copper of drilled pads on both sides. A test: a 0402 on B.Cu wished at the centre of a QFN on F.Cu gets its spot at 0.00 mm from the wish.
The packer used on this board (KiCad 10 pcbnew Python):
"""Side-aware legal-spot packer (rev 2, two-sided board). For each wished part, in wish order, search a 0.1 mm
spiral up to R mm around its wish (and the wish rotation, then +90) for a spot whose courtyard box does not
overlap any courtyard box ON THE SAME SIDE (through-hole footprints count on both sides) and stays inside the
outline by EDGE mm. Commits each spot before the next part. Writes AI Flow moves.json (KiCad mm).
python.exe pack_side.py <board.kicad_pcb> <wish.json> <out moves.json> [R=4.0]"""
import pcbnew, json, sys, math
b = pcbnew.LoadBoard(sys.argv[1]); wish = json.load(open(sys.argv[2])); R = float(sys.argv[4]) if len(sys.argv) > 4 else 4.0
mm, tomm = pcbnew.FromMM, pcbnew.ToMM
EDGE, GAP = 0.3, 0.05
ol = b.GetBoardEdgesBoundingBox(); X0, Y0, X1, Y1 = tomm(ol.GetLeft()), tomm(ol.GetTop()), tomm(ol.GetRight()), tomm(ol.GetBottom())
fps = {f.GetReference(): f for f in b.GetFootprints()}
def cy_box(f):
s = f.GetCourtyard(f.GetSide() if hasattr(f, "GetSide") else (pcbnew.B_CrtYd if f.IsFlipped() else pcbnew.F_CrtYd))
if s.OutlineCount() == 0:
bb = f.GetBoundingBox(False)
else:
bb = s.BBox()
return [tomm(bb.GetLeft()), tomm(bb.GetTop()), tomm(bb.GetRight()), tomm(bb.GetBottom())]
def courtyard(f):
layer = pcbnew.B_CrtYd if f.IsFlipped() else pcbnew.F_CrtYd
s = f.GetCourtyard(layer)
bb = s.BBox() if s.OutlineCount() else f.GetBoundingBox(False)
return [tomm(bb.GetLeft()), tomm(bb.GetTop()), tomm(bb.GetRight()), tomm(bb.GetBottom())]
def sides(f):
th = any(p.GetAttribute() in (pcbnew.PAD_ATTRIB_PTH, pcbnew.PAD_ATTRIB_NPTH) for p in f.Pads())
return {"B", "F"} if th else ({"B"} if f.IsFlipped() else {"F"})
def inside(bx): return X0 + EDGE <= bx[0] and Y0 + EDGE <= bx[1] and bx[2] <= X1 - EDGE and bx[3] <= Y1 - EDGE
def hit(a, c): return not (a[2] + GAP <= c[0] or c[2] + GAP <= a[0] or a[3] + GAP <= c[1] or c[3] + GAP <= a[1])
moves, placed = {}, set(fps) - set(wish)
HOLE_CLR = 0.25
def th_pads(): # every drilled pad is on both sides; its copper can stick out past its footprint's courtyard
out = []
for f in fps.values():
for p in f.Pads():
if p.GetAttribute() in (pcbnew.PAD_ATTRIB_PTH, pcbnew.PAD_ATTRIB_NPTH):
bb = p.GetBoundingBox()
out.append(([tomm(bb.GetLeft()) - HOLE_CLR, tomm(bb.GetTop()) - HOLE_CLR, tomm(bb.GetRight()) + HOLE_CLR, tomm(bb.GetBottom()) + HOLE_CLR], {"B", "F"}))
return out
occupied = lambda: [(courtyard(fps[r]), sides(fps[r])) for r in placed if r in fps and inside(courtyard(fps[r]))] + th_pads()
spiral = sorted({(round(i * 0.1, 2), round(j * 0.1, 2)) for i in range(-int(R * 10), int(R * 10) + 1) for j in range(-int(R * 10), int(R * 10) + 1) if math.hypot(i, j) <= R * 10}, key=lambda d: math.hypot(*d))
for ref, (wx, wy, rots) in wish.items():
f = fps[ref]; sd = sides(f); occ = [c for c, s in occupied() if s & sd]; ok = False
for rot in list(rots) + [r + 90 for r in rots]:
f.SetOrientationDegrees(rot)
for dx, dy in spiral:
f.SetPosition(pcbnew.VECTOR2I(mm(wx + dx), mm(wy + dy)))
c = courtyard(f)
if inside(c) and not any(hit(c, o) for o in occ):
moves[ref] = {"x": round(wx + dx, 3), "y": round(wy + dy, 3), "rotation": float(rot % 360)}
placed.add(ref); ok = True; break
if ok: break
print(ref, "->", moves.get(ref, "NO SPOT"))
json.dump(moves, open(sys.argv[3], "w"), indent=1)
print("placed", len(moves), "of", len(wish))