main
Colby Knox AI Flow 0.2.73: neck-down, length tuning, grounded coplanar (#52) 2a6bec0
"""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.')