molecule
AFS_Adom flight computer molecule
Public Unreviewedby ae007
Active fin stabilization flight computer for a sounding rocket, a clean-room AI redesign of the Rice Eclipse AFS board: RP2350B, ICM-45686, KX134-1211, MMC5983MA, MS5611, TMP117, INA219A, 8 servos, 2S LiPo, USB-C; 4-layer Adom molecule, designed end to end by Claude through KiCad and adom-aiflow
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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
"""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()