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
"""Comparison drawings for the AFS_Adom vs Rice Eclipse page: (1) both boards' copper drawn from their .kicad_pcb files at
the same mm scale; (2) both Adom Fields still-air temperature maps at the same scale and colour range, hot parts labelled."""
import json, math, sys, os
from PIL import Image, ImageDraw, ImageFont
sys.path.insert(0, os.path.dirname(__file__))
from silk import parse_sexp, kids, kid
S = '/tmp/claude-1001/-home-adom-project/8819fb5f-c2c6-4524-9528-99428cd6b88e/scratchpad'
FB = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 34); FS = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 24)
COL = {'F.Cu': (232, 72, 60), 'B.Cu': (70, 140, 240)}
SC = 9.0  # px per mm, both boards

def draw_board(path):
    t = parse_sexp(open(path, encoding='utf-8').read())[0]
    nt = {n[1]: n[2] for n in kids(t, 'net') if len(n) > 2}
    xs, ys = [], []
    for n in t[1:]:
        if isinstance(n, list) and n[0] in ('gr_line', 'gr_arc', 'gr_rect') and kid(n, 'layer') and kid(n, 'layer')[1] == 'Edge.Cuts':
            for k in ('start', 'end', 'mid'):
                p = kid(n, k)
                if p: xs.append(float(p[1])); ys.append(float(p[2]))
    X0, Y0, X1, Y1 = min(xs), min(ys), max(xs), max(ys)
    W, H = int((X1 - X0) * SC) + 40, int((Y1 - Y0) * SC) + 40
    im = Image.new('RGBA', (W, H), (0, 0, 0, 0)); d = ImageDraw.Draw(im)
    P = lambda x, y: (20 + (x - X0) * SC, 20 + (y - Y0) * SC)
    d.rounded_rectangle([P(X0, Y0), P(X1, Y1)], radius=int(1.5 * SC), fill=(18, 52, 32, 255), outline=(200, 205, 200, 255), width=3)
    ov = Image.new('RGBA', (W, H), (0, 0, 0, 0)); od = ImageDraw.Draw(ov)
    for z in kids(t, 'zone'):
        for fp in kids(z, 'filled_polygon'):
            lay = kid(fp, 'layer')[1]
            if lay in COL:
                c = COL[lay]; od.polygon([P(float(xy[1]), float(xy[2])) for xy in kids(kid(fp, 'pts'), 'xy')], fill=(c[0], c[1], c[2], 55))
    im = Image.alpha_composite(im, ov); d = ImageDraw.Draw(im)
    for s in kids(t, 'segment'):
        a, b = kid(s, 'start'), kid(s, 'end'); lay = kid(s, 'layer')[1]
        if lay in COL: d.line([P(float(a[1]), float(a[2])), P(float(b[1]), float(b[2]))], fill=COL[lay] + (255,), width=max(1, int(float(kid(s, 'width')[1]) * SC)))
    for f in kids(t, 'footprint'):
        at = kid(f, 'at'); cx, cy = float(at[1]), float(at[2]); rot = float(at[3]) if len(at) > 3 else 0
        c_, s_ = math.cos(math.radians(rot)), math.sin(math.radians(rot)); back = kid(f, 'layer')[1] == 'B.Cu'
        for p in kids(f, 'pad'):
            pa = kid(p, 'at'); px, py = float(pa[1]), float(pa[2]); sz = kid(p, 'size'); w, h = float(sz[1]), float(sz[2])
            pang = float(pa[3]) if len(pa) > 3 else rot
            if (round(pang) // 90) % 2: w, h = h, w
            ax, ay = cx + px * c_ + py * s_, cy - px * s_ + py * c_
            d.rectangle([P(ax - w / 2, ay - h / 2), P(ax + w / 2, ay + h / 2)], fill=(150, 170, 220, 255) if back else (214, 168, 70, 255))
    for v in kids(t, 'via'):
        a = kid(v, 'at'); r = float(kid(v, 'size')[1]) / 2 * SC; x, y = P(float(a[1]), float(a[2])); d.ellipse([x - r, y - r, x + r, y + r], fill=(235, 235, 235, 255))
    return im, (X1 - X0, Y1 - Y0)

def boards():
    a, da = draw_board(S + '/AFS_Adom/AFS_Adom.kicad_pcb'); b, db = draw_board(S + '/eclipse/Version2.kicad_pcb')
    W = a.width + b.width + 420; H = max(a.height, b.height) + 260
    c = Image.new('RGB', (W, H), (12, 14, 20)); d = ImageDraw.Draw(c)
    d.text((60, 30), 'Same scale: copper on both boards', font=FB, fill=(240, 240, 240))
    d.text((60, 76), 'red = top copper, blue = bottom copper, gold = top pads, lavender = bottom-side parts, white = vias', font=FS, fill=(170, 180, 190))
    c.paste(a, (60, 160), a); c.paste(b, (120 + a.width, 160), b)
    d.text((60, 170 + a.height), 'AFS_Adom (AI)  %.0f x %.0f mm, parts on one side' % da, font=FS, fill=(230, 230, 230))
    d.text((120 + a.width, 170 + b.height), 'Rice Eclipse (students)  %.0f x %.0f mm, parts on both sides' % db, font=FS, fill=(230, 230, 230))
    c.save(S + '/eclipse/compare-boards.png'); print('boards', c.size)

def thermal():
    tiles = []
    for tag, fd, title, pcb in (('afs', S + '/AFS_Adom/aiflow/run/fields', 'AFS_Adom (AI)', S + '/AFS_Adom/AFS_Adom.kicad_pcb'), ('ecl', S + '/eclipse/fields', 'Rice Eclipse (students)', S + '/eclipse/Version2.kicad_pcb')):
        tp = parse_sexp(open(pcb, encoding='utf-8').read())[0]
        pos = {[q[2] for q in kids(f_, 'property') if q[1] == 'Reference'][0]: (float(kid(f_, 'at')[1]), float(kid(f_, 'at')[2])) for f_ in kids(tp, 'footprint')}
        f = json.load(open(fd + '/fields.json')); b = f['board']
        im = Image.open(fd + '/tex/T-F_Cu.raw.png'); W0, H0 = im.size; px = im.load()
        out = Image.new('RGB', (W0, H0), (12, 14, 20)); po = out.load()
        mask = Image.open(fd + '/tex/T-F_Cu.png').convert('RGBA').load()
        vals = [px[x, y] for y in range(H0) for x in range(W0)]; vmax = max(vals) or 1
        smax = max(c['riseC'] for c in f['chips']); trise = f['scales']['tMax']
        for y in range(H0):
            for x in range(W0):
                if mask[x, y][3] == 0: continue
                tC = px[x, y] / vmax * trise; u = max(0.0, min(1.0, tC / 60.0))
                po[x, y] = (int(20 + 235 * u), int(30 + 170 * u * u), int(110 * (1 - u) + 20))
        k = SC / (1 / b['cellMm']); out = out.resize((int(W0 * b['cellMm'] * SC), int(H0 * b['cellMm'] * SC)), Image.BILINEAR)
        d = ImageDraw.Draw(out)
        for i, ch in enumerate(sorted(f['chips'], key=lambda c: pos[c['reference']][0])):
            cx, cy = pos[ch['reference']]; x = (cx - b['x0']) * SC; y = (cy - b['y0']) * SC; lab = '%s +%.0f C' % (ch['reference'], ch['riseC'])
            d.ellipse([x - 6, y - 6, x + 6, y + 6], outline=(255, 255, 255), width=2)
            tw = d.textlength(lab, font=FS); dy = -40 if i % 2 else 12
            d.text((min(max(4, x - tw / 2), out.width - tw - 4), y + dy), lab, font=FS, fill=(255, 255, 255))
        tiles.append((out, title))
    W = sum(t.width for t, _ in tiles) + 180; H = max(t.height for t, _ in tiles) + 300
    c = Image.new('RGB', (W, H), (12, 14, 20)); d = ImageDraw.Draw(c)
    d.text((60, 30), 'Temperature rise, same loads and scale (simulated)', font=FB, fill=(240, 240, 240))
    d.text((60, 76), 'Adom Fields, still air. 4 A servo rail, 1 A on 3.3 V, 1.1 A on 5 V; bucks 0.35 / 0.30 W, MCU 0.6 W.', font=FS, fill=(170, 180, 190))
    x = 60
    for t, title in tiles:
        c.paste(t, (x, 160)); d.text((x, 170 + t.height), title, font=FS, fill=(230, 230, 230)); x += t.width + 60
    for i in range(400):
        u = i / 399; d.line([(60 + i, H - 50), (60 + i, H - 30)], fill=(int(20 + 235 * u), int(30 + 170 * u * u), int(110 * (1 - u) + 20)))
    d.text((60, H - 80), '0 C', font=FS, fill=(200, 200, 200)); d.text((420, H - 80), '60 C', font=FS, fill=(200, 200, 200))
    c.save(S + '/eclipse/compare-thermal.png'); print('thermal', c.size)

boards(); thermal()