app
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.
main
John Lauer
Publish 0.2.58
e8180ab
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
"""Issues #168 and #179 end to end, with no CAD and no desktop.
#168: on a one-copper-layer board whose GND and +5V pads only the planned pours join, `gate` before
`land pours` puts the pours `pour` planned into the gate copy, so the pads they join are not read as
unconnected. A fake kicad-cli reports every pad unconnected unless the board it checks carries zones.
#179: `place pack` on a two-sided board puts a bottom part straight under a top QFN (courtyards meet
only on their own side), keeps a top part out of the QFN, and `place check`'s bounding-box advisory
does not name a top/bottom pair.
Usage: python3 tests/sides-pours-workflow.py <adom-aiflow binary>"""
import json, os, pathlib, subprocess, sys, tempfile
binary = str(pathlib.Path(sys.argv[1]).resolve())
FAKE_BRIDGE = r'''#!/usr/bin/env python3
import json
print(json.dumps({"success": True, "status": "ok", "revision": "r1"}))
'''
# DRC: every pad on GND or +5V is unconnected unless the board carries a zone of that net
FAKE_CLI = r'''#!/usr/bin/env python3
import sys, json, re
a = sys.argv[1:]
if "--help" in a: print("--refill-zones"); sys.exit(0)
out = a[a.index("--output") + 1]
text = open(a[-1]).read()
unc = []
for net in ("GND", "+5V"):
if not re.search(r'\(zone\s*\(net \d+\)\s*\(net_name "' + re.escape(net) + '"', text):
unc.append({"type": "unconnected_items", "severity": "error", "description": "Missing connection between items",
"items": [{"description": f"Pad 1 [{net}] of R1"}, {"description": f"Pad 1 [{net}] of R2"}]})
json.dump({"violations": [], "unconnected_items": unc}, open(out, "w"))
'''
ONE_LAYER = '''(kicad_pcb (version 20240108) (generator "pcbnew")
(layers (0 "F.Cu" signal) (31 "B.Cu" signal) (44 "Edge.Cuts" user))
(net 0 "") (net 1 "GND") (net 2 "+5V")
(gr_rect (start 0 0) (end 40 20) (layer "Edge.Cuts"))
(footprint "R" (layer "F.Cu") (at 5 5) (property "Reference" "R1") (fp_rect (start -1 -1) (end 1 11) (layer "F.CrtYd"))
(pad "1" smd rect (at 0 0) (size 1 1) (layers "F.Cu") (net 1 "GND"))
(pad "2" smd rect (at 0 10) (size 1 1) (layers "F.Cu") (net 2 "+5V")))
(footprint "R" (layer "F.Cu") (at 35 5) (property "Reference" "R2") (fp_rect (start -1 -1) (end 1 11) (layer "F.CrtYd"))
(pad "1" smd rect (at 0 0) (size 1 1) (layers "F.Cu") (net 1 "GND"))
(pad "2" smd rect (at 0 10) (size 1 1) (layers "F.Cu") (net 2 "+5V"))))
'''
def smd(ref, layer, x, y, h, w):
crt = layer.replace('.Cu', '.CrtYd')
return (f'\t(footprint "T:{ref}"\n\t\t(layer "{layer}")\n\t\t(at {x} {y})\n\t\t(property "Reference" "{ref}")\n'
f'\t\t(fp_rect (start {-w} {-h}) (end {w} {h}) (layer "{crt}"))\n'
f'\t\t(pad "1" smd rect (at 0 0) (size 0.5 0.5) (layers "{layer}") (net "N{ref}"))\n\t)\n')
TWO_SIDED = ('(kicad_pcb\n\t(version 20260206)\n\t(generator "test")\n\t(layers\n\t\t(0 "F.Cu" signal)\n\t\t(2 "B.Cu" signal)\n\t\t(25 "Edge.Cuts" user)\n\t)\n'
+ smd('U1', 'F.Cu', 10, 10, 2.0, 2.0) + smd('C1', 'B.Cu', 3, 3, 0.5, 0.9) + smd('C2', 'F.Cu', 3, 17, 0.5, 0.9)
+ '\t(gr_rect\n\t\t(start 0 0)\n\t\t(end 30 20)\n\t\t(layer "Edge.Cuts")\n\t)\n)\n')
passed = 0
def check(cond, what, out=''):
global passed
if not cond:
sys.exit(f'FAIL: {what}\n{out}')
passed += 1
with tempfile.TemporaryDirectory(prefix='aiflow-sides-pours-') as td:
p = pathlib.Path(td)
bindir = p / 'bin'; bindir.mkdir()
(bindir / 'adom-bridge').write_text(FAKE_BRIDGE); (bindir / 'adom-bridge').chmod(0o755)
(bindir / 'kicad-cli').write_text(FAKE_CLI); (bindir / 'kicad-cli').chmod(0o755)
env = dict(os.environ, AIFLOW_DASHBOARD='off', AIFLOW_NO_PART_SEARCH='1', AIFLOW_NO_PART_LOOKUP='1',
ADOM_AIFLOW_KICAD_CLI=str(bindir / 'kicad-cli'), PATH=f"{bindir}:{os.environ['PATH']}")
env.pop('KICAD_REMOTE_TARGET', None)
def call(run, *args):
return subprocess.run([binary, '--run', str(run), '--ai-thread', 'sides-pours-test', *args], env=env, capture_output=True, text=True, timeout=300)
# ---- issue #168: pour-only power nets on one copper layer
(p / 'one.kicad_pcb').write_text(ONE_LAYER)
(p / 'spec.json').write_text(json.dumps({"board": "one-layer pours", "clearance": 0.2, "pours": [
{"net": "GND", "layers": ["F.Cu"], "polygon": [[1, 1], [39, 1], [39, 9], [1, 9]]},
{"net": "+5V", "layers": ["F.Cu"], "polygon": [[1, 11], [39, 11], [39, 19], [1, 19]]}]}))
(p / 'plan-ai.json').write_text(json.dumps({"engine": "ai", "nets": []}))
run = p / 'run'
r = call(run, 'start', '--board', str(p / 'one.kicad_pcb'), '--spec', str(p / 'spec.json'), '--engine', 'test', '--target', 'fakebox', '--remote-board', 'C:/boards/one.kicad_pcb')
check(r.returncode == 0, 'start', r.stdout + r.stderr)
r = call(run, 'gate', '--plan', str(p / 'plan-ai.json'))
out = r.stdout + r.stderr
check(r.returncode == 0 and '2 unconnected' in out, 'before pour: the gate reads the pour-only pads as unconnected (a finding)', out)
r = call(run, 'pour')
check(r.returncode == 0 and '2 zones' in r.stdout + r.stderr, 'pour plans the two zones', r.stdout + r.stderr)
r = call(run, 'gate', '--plan', str(p / 'plan-ai.json'))
out = r.stdout + r.stderr
check(r.returncode == 0 and 'gate passed' in out and '0 unconnected' in out, 'after pour: the planned pours join the pads in the gate copy', out)
g = json.loads((run / 'gate.json').read_text())
check(g.get('plannedPours', {}).get('zones') == 2 and g['plannedPours']['nets'] == ['+5V', 'GND'], 'gate.json names the planned pours', json.dumps(g.get('plannedPours')))
routed = (run / 'board-routed.kicad_pcb').read_text()
check(routed.count('(zone') == 2, 'the gate copy carries the two zones', routed[-800:])
check((p / 'one.kicad_pcb').read_text() == ONE_LAYER, 'the source board is untouched')
# ---- issue #179: a bottom part under a top part
(p / 'two.kicad_pcb').write_text(TWO_SIDED)
(p / 'spec2.json').write_text(json.dumps({"board": "two-sided", "clearance": 0.2}))
run2 = p / 'run2'
r = call(run2, 'start', '--board', str(p / 'two.kicad_pcb'), '--spec', str(p / 'spec2.json'), '--engine', 'test', '--target', 'fakebox', '--remote-board', 'C:/boards/two.kicad_pcb')
check(r.returncode == 0, 'start two-sided', r.stdout + r.stderr)
(p / 'wish.json').write_text(json.dumps([{"ref": "C1", "x": 10, "y": 10, "rot": [0], "radius": 2.0}, {"ref": "C2", "x": 10, "y": 10, "rot": [0], "radius": 4.0}]))
r = call(run2, 'place', 'pack', '--wish', str(p / 'wish.json'), '--out', str(p / 'moves.json'))
out = r.stdout + r.stderr
check(r.returncode == 0 and 'legal spots for 2 of 2' in out, 'pack places both parts', out)
moves = json.loads((p / 'moves.json').read_text())
check((moves['C1']['x'], moves['C1']['y']) == (10, 10), 'the bottom part sits at its wish under the QFN', json.dumps(moves))
check(abs(moves['C2']['x'] - 10) > 1.0 or abs(moves['C2']['y'] - 10) > 1.0, 'the top part leaves the QFN courtyard', json.dumps(moves))
r = call(run2, 'place', 'check', '--moves', str(p / 'moves.json'))
out = r.stdout + r.stderr
check(r.returncode == 0 and 'C1 U1' not in out, 'the advisory does not name the top/bottom pair', out)
print(f'OK sides-pours-workflow: {passed} checks passed')