main
ae007 Comparison with the Rice Eclipse AFS student board: same-rules DRC/ERC, same-load Adom Fields current and thermal scree… 3b8a4e2 7d ago
"""Side-by-side metrics of two KiCad boards (read-only): outline, layers, stack-up, parts, passive sizes, tracks by width
and layer, vias, zones, nets, and the copper on chosen power nets. Used for the AFS_Adom vs Rice Eclipse comparison."""
import json, math, re, sys, os
from collections import Counter, defaultdict
sys.path.insert(0, os.path.dirname(__file__))
from silk import parse_sexp, kids, kid

def metrics(path):
    t = parse_sexp(open(path, encoding='utf-8').read())[0]
    m = dict(file=os.path.basename(path))
    lay = kid(t, 'layers'); m['copper_layers'] = [l[1] for l in lay[1:] if isinstance(l, list) and l[1].endswith('.Cu')]
    setup = kid(t, 'setup'); stk = kid(setup, 'stackup') if setup else None
    m['thickness'] = None
    gen = kid(t, 'general')
    if gen and kid(gen, 'thickness'): m['thickness'] = float(kid(gen, 'thickness')[1])
    xs, ys = [], []
    for n in t[1:]:
        if isinstance(n, list) and n[0] in ('gr_line', 'gr_arc', 'gr_rect', 'gr_poly', 'gr_circle') and kid(n, 'layer') and kid(n, 'layer')[1] == 'Edge.Cuts':
            for k in ('start', 'end', 'mid', 'center'):
                p = kid(n, k)
                if p: xs.append(float(p[1])); ys.append(float(p[2]))
            pts = kid(n, 'pts')
            if pts:
                for xy in kids(pts, 'xy'): xs.append(float(xy[1])); ys.append(float(xy[2]))
    m['outline_mm'] = [round(max(xs) - min(xs), 2), round(max(ys) - min(ys), 2)] if xs else None
    fps = kids(t, 'footprint'); m['footprints'] = len(fps)
    sides = Counter(kid(f, 'layer')[1] for f in fps); m['sides'] = dict(sides)
    pk = Counter()
    for f in fps:
        name = f[1]
        mm = re.search(r'_(0201|0402|0603|0805|1206|1210|2512)_', name)
        if mm and (name.startswith('Resistor') or name.startswith('Capacitor') or ':R_' in name or ':C_' in name or 'LED' in name): pk[mm.group(1)] += 1
    m['passive_sizes'] = dict(pk)
    m['values'] = Counter(re.search(r'\(property "Value" "([^"]*)"', '') and '' for _ in [])  # placeholder
    segs = kids(t, 'segment'); arcs = kids(t, 'arc')
    w = Counter(); L = defaultdict(float); nets_len = defaultdict(float); nets_minw = {}
    for s in segs:
        a, b = kid(s, 'start'), kid(s, 'end'); ln = math.dist((float(a[1]), float(a[2])), (float(b[1]), float(b[2])))
        wd = float(kid(s, 'width')[1]); ly = kid(s, 'layer')[1]; net = kid(s, 'net'); nn = net[-1] if net else ''
        w[round(wd, 3)] += 1; L[ly] += ln; nets_len[nn] += ln; nets_minw[nn] = min(nets_minw.get(nn, 9), wd)
    m['segments'] = len(segs) + len(arcs); m['track_mm_by_layer'] = {k: round(v) for k, v in L.items()}; m['track_mm_total'] = round(sum(L.values()))
    m['track_widths'] = dict(sorted(w.items()))
    vias = kids(t, 'via'); m['vias'] = len(vias); m['via_sizes'] = dict(Counter('%s/%s' % (kid(v, 'size')[1], kid(v, 'drill')[1]) for v in vias))
    zones = kids(t, 'zone'); zl = []
    for z in zones:
        net = kid(z, 'net_name'); layers = kid(z, 'layers') or kid(z, 'layer'); fill_area = 0.0
        for fp in kids(z, 'filled_polygon'):
            pts = [(float(xy[1]), float(xy[2])) for xy in kids(kid(fp, 'pts'), 'xy')]
            fill_area += abs(sum(pts[i][0] * pts[i - 1][1] - pts[i - 1][0] * pts[i][1] for i in range(len(pts)))) / 2
        zl.append((net[1] if net else '', ' '.join(layers[1:]) if layers else '', round(fill_area)))
    m['zones'] = len(zones); m['zone_list'] = zl
    m['nets'] = len({n[-1] for s in segs for n in [kid(s, 'net')] if n})
    m['nets_len'] = nets_len; m['nets_minw'] = nets_minw
    return m, t

if __name__ == '__main__':
    out = {}
    for tag, p in (('afs', sys.argv[1]), ('eclipse', sys.argv[2])):
        m, t = metrics(p); out[tag] = m
        print('==', tag, {k: v for k, v in m.items() if k not in ('nets_len', 'nets_minw', 'zone_list', 'values')})
        print('   zones:', m['zone_list'][:40])
    json.dump({k: {kk: (dict(vv) if isinstance(vv, defaultdict) else vv) for kk, vv in v.items() if kk != 'values'} for k, v in out.items()}, open(sys.argv[3], 'w'), indent=1, default=str)