import re, math, sys from collections import defaultdict def parse_pcb(path): txt=open(path).read() fps=[] for m in re.finditer(r'\(footprint "([^"]+)"(.*?)\n\t\)\n', txt, re.S): body=m.group(2) ref=re.search(r'\(property "Reference" "([^"]+)"', body).group(1) at=re.search(r'\n\t\t\(at (-?[\d.]+) (-?[\d.]+)(?: (-?[\d.]+))?\)', body) fx,fy,frot=float(at.group(1)),float(at.group(2)),float(at.group(3) or 0) layer=re.search(r'\(layer "([^"]+)"\)', body).group(1) pads=[] for p in re.finditer(r'\(pad "([^"]*)" (\w+) (\w+)\s*\(at (-?[\d.]+) (-?[\d.]+)(?: (-?[\d.]+))?\)\s*\(size ([\d.]+) ([\d.]+)\)(.*?)\n\t\t\)', body, re.S): num,ptype,shape=p.group(1),p.group(2),p.group(3) ox,oy,prot=float(p.group(4)),float(p.group(5)),float(p.group(6) or 0) sx,sy=float(p.group(7)),float(p.group(8)) rest=p.group(9) lay=re.search(r'\(layers ([^)]*)\)', rest); lays=re.findall(r'"([^"]+)"', lay.group(1)) if lay else [] net=re.search(r"\(net \"([^\"]*)\"\)", rest) th=math.radians(frot) px=fx+ox*math.cos(th)+oy*math.sin(th); py=fy-ox*math.sin(th)+oy*math.cos(th) # pad absolute rotation = prot (kicad stores absolute rotation in pad 'at' for footprint pads) -> in v8+ pad rotation is relative? In KiCad 8/9 file, pad rotation is absolute (fp rot + pad rot). We'll treat prot as absolute. pads.append(dict(ref=ref,num=num,type=ptype,shape=shape,x=px,y=py,rot=prot,sx=sx,sy=sy,layers=lays,net=net.group(1) if net else "")) fps.append(dict(ref=ref,x=fx,y=fy,rot=frot,layer=layer,pads=pads)) segs=[dict(x1=float(a),y1=float(b),x2=float(c),y2=float(d),w=float(w),layer=l,net=n) for a,b,c,d,w,l,n in re.findall(r'\(segment\s*\(start (-?[\d.]+) (-?[\d.]+)\)\s*\(end (-?[\d.]+) (-?[\d.]+)\)\s*\(width ([\d.]+)\)\s*\(layer "([^"]+)"\)\s*\(net "([^"]*)"\)', txt)] arcs=re.findall(r'\(arc\s*\(start', txt) vias=[dict(x=float(x),y=float(y),size=float(s),drill=float(d),layers=[l1,l2],net=n) for x,y,s,d,l1,l2,n in re.findall(r'\(via\s*\(at (-?[\d.]+) (-?[\d.]+)\)\s*\(size ([\d.]+)\)\s*\(drill ([\d.]+)\)\s*\(layers "([^"]+)" "([^"]+)"\)(?:\s*\([^)]*\))*?\s*\(net "([^"]*)"\)', txt)] return fps,segs,vias,len(arcs) COPPER=["F.Cu","In1.Cu","In2.Cu","B.Cu"] def pad_layers(p): out=set() for l in p["layers"]: if l=="*.Cu": out.update(COPPER) elif l.endswith(".Cu"): out.add(l) return out def point_in_pad(x,y,p,tol=0.0): # rotate point into pad frame th=math.radians(p["rot"]); dx=x-p["x"]; dy=y-p["y"] u= dx*math.cos(th)-dy*math.sin(th); w= dx*math.sin(th)+dy*math.cos(th) # kicad y is down; rotation sign: try both conventions by testing symmetric sizes; use |u|<=sx/2 || |w|<=sy/2 with swapped if rot 90 if abs(u)<=p["sx"]/2+tol and abs(w)<=p["sy"]/2+tol: return True u2= dx*math.cos(th)+dy*math.sin(th); w2=-dx*math.sin(th)+dy*math.cos(th) return abs(u2)<=p["sx"]/2+tol and abs(w2)<=p["sy"]/2+tol def seg_dist(px,py,s): x1,y1,x2,y2=s["x1"],s["y1"],s["x2"],s["y2"] dx,dy=x2-x1,y2-y1; L2=dx*dx+dy*dy t=0 if L2==0 else max(0,min(1,((px-x1)*dx+(py-y1)*dy)/L2)) cx,cy=x1+t*dx,y1+t*dy return math.hypot(px-cx,py-cy) class UF: def __init__(s): s.p={} def f(s,a): s.p.setdefault(a,a) while s.p[a]!=a: s.p[a]=s.p[s.p[a]]; a=s.p[a] return a def u(s,a,b): s.p[s.f(a)]=s.f(b) def build(net, fps, segs, vias, exclude_pads=(), exclude_vias=()): items=[] for f in fps: for p in f["pads"]: if p["net"]==net and (p["ref"],p["num"]) not in exclude_pads: items.append(("pad",p)) for s in segs: if s["net"]==net: items.append(("seg",s)) for i,v in enumerate(vias): if v["net"]==net and (round(v["x"],3),round(v["y"],3)) not in exclude_vias: items.append(("via",v)) uf=UF() n=len(items) for i in range(n): uf.f(i) for i in range(n): ki,oi=items[i] for j in range(i+1,n): kj,oj=items[j] if connected(ki,oi,kj,oj): uf.u(i,j) return items,uf def layers_of(k,o): if k=="pad": return pad_layers(o) if k=="seg": return {o["layer"]} if k=="via": return set(COPPER) if o["layers"]==["F.Cu","B.Cu"] else set(o["layers"]) def connected(ka,a,kb,b): la,lb=layers_of(ka,a),layers_of(kb,b) if not (la&lb): return False if ka=="seg" and kb=="seg": wa,wb=a["w"]/2,b["w"]/2 for (x,y) in ((a["x1"],a["y1"]),(a["x2"],a["y2"])): if seg_dist(x,y,b)<=wa+wb-0.001+0.002: return True for (x,y) in ((b["x1"],b["y1"]),(b["x2"],b["y2"])): if seg_dist(x,y,a)<=wa+wb-0.001+0.002: return True return False if ka=="seg" or kb=="seg": s,o,ko=(a,b,kb) if ka=="seg" else (b,a,ka) if ko=="via": return seg_dist(o["x"],o["y"],s)<=s["w"]/2+o["size"]/2-0.001 else: for (x,y) in ((s["x1"],s["y1"]),(s["x2"],s["y2"])): if point_in_pad(x,y,o,tol=s["w"]/2*0.0): return True # also pad center near segment return seg_dist(o["x"],o["y"],s)<=s["w"]/2+min(o["sx"],o["sy"])/2-0.001 if ka=="via" and kb=="via": return math.hypot(a["x"]-b["x"],a["y"]-b["y"])<=(a["size"]+b["size"])/2-0.001 if ka=="via" or kb=="via": v,p=(a,b) if ka=="via" else (b,a) return point_in_pad(v["x"],v["y"],p,tol=v["size"]/2-0.001) if ka=="pad" and kb=="pad": return math.hypot(a["x"]-b["x"],a["y"]-b["y"])<=(min(a["sx"],a["sy"])+min(b["sx"],b["sy"]))/2-0.001 return False def comp_of(items,uf,key): for i,(k,o) in enumerate(items): if k=="pad" and (o["ref"],o["num"])==key: return uf.f(i) return None def clip_segments_outside_rect(segs, net, x0,y0,x1,y1): """Return segs with the parts inside the rect removed (Liang-Barsky), for a given net.""" out=[] for s in segs: if s["net"]!=net: out.append(s); continue ax,ay,bx,by=s["x1"],s["y1"],s["x2"],s["y2"] ina = x0<=ax<=x1 and y0<=ay<=y1; inb = x0<=bx<=x1 and y0<=by<=y1 if ina and inb: continue if not ina and not inb: # could cross the rect; check with Liang-Barsky dx,dy=bx-ax,by-ay; t0,t1=0.0,1.0; ok=True for p,q in ((-dx,ax-x0),(dx,x1-ax),(-dy,ay-y0),(dy,y1-ay)): if p==0: if q<0: ok=False; break else: t=q/p if p<0: t0=max(t0,t) else: t1=min(t1,t) if ok and t00.001: out.append(dict(s,x1=ax,y1=ay,x2=ax+t*dx,y2=ay+t*dy)) return out def kelvin_test(fps,segs,vias,net,pad_ref,pad_num,sense_ref,sense_num): pad=[p for f in fps for p in f["pads"] if (p["ref"],p["num"])==(pad_ref,pad_num)][0] x0,y0,x1,y1=pad["x"]-pad["sx"]/2,pad["y"]-pad["sy"]/2,pad["x"]+pad["sx"]/2,pad["y"]+pad["sy"]/2 segs2=clip_segments_outside_rect(segs,net,x0,y0,x1,y1) vias2=[v for v in vias if not (v["net"]==net and x0<=v["x"]<=x1 and y0<=v["y"]<=y1)] items,uf=build(net,fps,segs2,vias2,exclude_pads=((pad_ref,pad_num),)) c=comp_of(items,uf,(sense_ref,sense_num)) pads=[o["ref"]+"."+o["num"] for i,(k,o) in enumerate(items) if k=="pad" and uf.f(i)==c] nvia=sum(1 for i,(k,o) in enumerate(items) if k=="via" and uf.f(i)==c) return pads,nvia