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
Colby Knox
AI Flow 0.2.73: neck-down, length tuning, grounded coplanar (#52)
2a6bec0
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
"""Impedance targets end to end (issue #52): table, route sizes and couples a USB pair, analyze
impedance judges the copper, a refused layer and a split pair are reported, never claimed, and a
grounded coplanar pair is sized for its pour, which is said to be assumed until it is poured.
Usage: python3 tests/impedance-workflow.py <adom-aiflow binary>"""
import json, os, pathlib, subprocess, sys, tempfile
binary = str(pathlib.Path(sys.argv[1]).resolve())
STACK = ('(setup (stackup (layer "F.Cu" (type "copper") (thickness 0.035)) (layer "dielectric 1" (type "prepreg") (thickness 0.2104) (epsilon_r 4.4)) '
'(layer "In1.Cu" (type "copper") (thickness 0.0152)) (layer "dielectric 2" (type "core") (thickness 1.065) (epsilon_r 4.6)) '
'(layer "In2.Cu" (type "copper") (thickness 0.0152)) (layer "dielectric 3" (type "prepreg") (thickness 0.2104) (epsilon_r 4.4)) '
'(layer "B.Cu" (type "copper") (thickness 0.035))))')
def board(path, y1, post=""):
def fp(r, x, y, g):
return (f'(footprint "T:{r}" (layer "F.Cu") (at {x} {y}) (property "Reference" "{r}") '
f'(pad "1" smd rect (at 0 -0.35) (size 1.0 0.4) (layers "F.Cu") (net 1 "USB_D+")) '
f'(pad "2" smd rect (at 0 0.35) (size 1.0 0.4) (layers "F.Cu") (net 2 "USB_D-")) '
f'(pad "3" thru_hole circle (at {g} 0) (size 0.8 0.8) (drill 0.4) (layers "*.Cu") (net 3 "GND")))')
path.write_text(f'(kicad_pcb (version 20241229) (generator "test") (general (thickness 1.6)) (layers (0 "F.Cu" signal) (4 "In1.Cu" signal) (6 "In2.Cu" signal) (2 "B.Cu" signal) (25 "Edge.Cuts" user)) {STACK} '
f'(net 0 "") (net 1 "USB_D+") (net 2 "USB_D-") (net 3 "GND") (net 4 "X") {fp("J1", 4.0, 6.05, -1.5)} {fp("U1", 36.0, y1, 1.5)} {post} '
'(gr_rect (start 0 0) (end 40 20) (layer "Edge.Cuts") (stroke (width 0.1) (type solid))))')
with tempfile.TemporaryDirectory(prefix='aiflow-impedance-') as td:
p = pathlib.Path(td)
env = dict(os.environ, AIFLOW_DASHBOARD='off')
def call(run, *args):
return subprocess.run([binary, '--run', str(run), '--ai-thread', 'impedance-regression', *args], env=env, capture_output=True, text=True, timeout=120)
spec = {"clearance": 0.127, "minTrackWidth": 0.127, "track": 0.2, "copperUm": 35,
"impedanceTargets": {"USB": {"pair": ["USB_D+", "USB_D-"], "targetDiffOhms": 90}}}
(p / 'spec.json').write_text(json.dumps(spec))
b = p / 'usb.kicad_pcb'
board(b, 11.05)
# the table: both outer layers solve on the router's grid pitch, JLC's 5:1 inner layers are refused
r = call(p / 'none', 'impedance', 'table', '--board', str(b), '--spec', str(p / 'spec.json'), '--json')
assert r.returncode == 0, r.stdout + r.stderr
doc = json.loads(r.stdout[:r.stdout.index('\nOK: ')])
rows = {x['layer']: x for x in doc['rows']}
assert rows['F.Cu']['ok'] and abs(rows['F.Cu']['ohms'] - 90) < 0.5, rows['F.Cu']
assert abs(rows['F.Cu']['widthMm'] + rows['F.Cu']['gapMm'] - 0.7) < 1e-6, rows['F.Cu']
assert not rows['In1.Cu']['ok'] and '3:1' in rows['In1.Cu']['why']
assert 'route sizes USB' in r.stdout
# a fab too coarse for any outer-layer solution: route refuses before routing and names take route=ai
coarse = dict(spec, minTrackWidth=0.6)
(p / 'coarse.json').write_text(json.dumps(coarse))
run = p / 'run-coarse'
assert call(run, 'start', '--board', str(b), '--spec', str(p / 'coarse.json'), '--engine', 'test', '--prompt-time', '2026-10-27T00:00:00Z').returncode == 0
r = call(run, 'route')
assert r.returncode != 0 and 'cannot be routed at 90' in r.stdout and 'take route=ai' in r.stdout, r.stdout
# the binary alone: route sizes and couples the pair, analyze impedance passes on its copper
run = p / 'run'
assert call(run, 'start', '--board', str(b), '--spec', str(p / 'spec.json'), '--engine', 'test', '--prompt-time', '2026-10-27T00:00:00Z').returncode == 0
r = call(run, 'route')
assert r.returncode == 0, r.stdout + r.stderr
assert 'impedance USB (USB_D+ / USB_D-): routed on F.Cu at width 0.3689 mm' in r.stdout, r.stdout
rep = json.loads((run / 'plan-route-report.json').read_text())['impedance']['check'][0]
assert rep['status'] == 'pass' and rep['vias'] == [0, 0] and min(rep['coupledPercent']) > 90, rep
widths = {e['width'] for e in json.loads((run / 'plan-route.json').read_text())['nets'] if e['net'].startswith('USB')}
assert widths == {0.3689}, widths # 0.4 mm pads: the escape stubs leave at the solved width too
r = call(run, 'analyze', 'impedance')
assert r.returncode == 0 and 'impedance analysis passed' in r.stdout and 'signals.differentialPairs' in r.stdout, r.stdout
o = json.loads((run / 'run.json').read_text())['outcomes']['analyze-impedance']
assert o['pass'] and o['results'][0]['copperFrom'] == ['plan', 'plan']
g = json.loads(call(run, 'finish-guard', '--json').stdout)
assert not any('impedance' in x for x in g['remaining']), g['remaining']
# a post between the nets: the router splits the pair, says so, and analyze fails it
post = '(footprint "T:P" (layer "F.Cu") (at 20 6.05) (property "Reference" "TP1") (pad "1" thru_hole circle (at 0 0) (size 1.6 1.6) (drill 0.8) (layers "*.Cu") (net 4 "X")))'
b2 = p / 'split.kicad_pcb'
board(b2, 6.05, post)
run = p / 'run-split'
assert call(run, 'start', '--board', str(b2), '--spec', str(p / 'spec.json'), '--engine', 'test', '--prompt-time', '2026-10-27T00:00:00Z').returncode == 0
r = call(run, 'route')
assert r.returncode == 0 and 'WARNING impedance USB' in r.stdout + r.stderr and 'take route=ai' in r.stdout, r.stdout
r = call(run, 'analyze', 'impedance')
assert r.returncode != 0 and 'not coupled' in r.stdout, r.stdout
# a same-layer GND pour beside the pair (grounded coplanar): the outer rows say the pour is assumed,
# route sizes the pair for it, and analyze impedance says the pour is assumed, not measured, until
# a filled GND zone is on the layer
cpw = dict(spec, coplanarGround={"net": "GND", "gapMm": 0.15})
(p / 'cpw.json').write_text(json.dumps(cpw))
r = call(p / 'none', 'impedance', 'table', '--board', str(b), '--spec', str(p / 'cpw.json'), '--json')
assert r.returncode == 0, r.stdout + r.stderr
rows = {x['layer']: x for x in json.loads(r.stdout[:r.stdout.index('\nOK: ')])['rows']}
f = rows['F.Cu']
assert f['ok'] and f['structure'] == 'grounded coplanar' and f['coplanarGapMm'] == 0.15 and 'GND pour on F.Cu 0.15 mm' in f['assumes'], f
assert 'coplanarNote' in rows['In1.Cu'] and 'with a GND pour 0.15 mm beside it (assumed' in r.stdout and 'Grounded coplanar rows' in r.stdout, r.stdout
run = p / 'run-cpw'
assert call(run, 'start', '--board', str(b), '--spec', str(p / 'cpw.json'), '--engine', 'test', '--prompt-time', '2026-10-27T00:00:00Z').returncode == 0
r = call(run, 'route')
assert r.returncode == 0 and f"routed on F.Cu at width {f['widthMm']} mm" in r.stdout and 'assumed until it is poured' in r.stdout, r.stdout
r = call(run, 'analyze', 'impedance')
assert r.returncode == 0 and 'GND pour 0.15 mm beside it ASSUMED on F.Cu, not measured' in r.stdout and 'signals.differentialPairs' not in r.stdout, r.stdout
# adom-fields' own impedance map with an object value is refused with the right key named
(p / 'bad.json').write_text(json.dumps({"impedance": {"USB": {"pair": ["USB_D+", "USB_D-"]}}}))
r = call(p / 'none', 'impedance', 'table', '--board', str(b), '--spec', str(p / 'bad.json'))
assert r.returncode != 0 and 'impedanceTargets' in r.stdout, r.stdout
print('PASS: table per layer with refusals, route refuses an unreachable target, sizes and couples a pair on one layer, analyze impedance passes it and fails a split pair, a coplanar pair is sized for its pour and the pour is said to be assumed, Fields map kept numeric.')