137 行
7.1 KiB
Python
137 行
7.1 KiB
Python
"""Extend existing Buck local copper through DRC-gated candidate routing."""
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
|
|
import pcbnew as pcb
|
|
|
|
import route_local_r1 as base
|
|
from schematic_maintenance import ROOT
|
|
from verify_pcb_layout import routing
|
|
|
|
OUT = ROOT / 'out/local-routing-r2'
|
|
BOARD = base.BOARD
|
|
|
|
|
|
def digest(path):
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def paths(a, b, axis):
|
|
# Escape the IC away from its adjacent pins, then use a 45-degree bend.
|
|
for escape in (.7, .9, 1.1, 1.4):
|
|
sx = 1 if b.x >= a.x else -1
|
|
sy = 1 if b.y >= a.y else -1
|
|
c = pcb.VECTOR2I(a.x + (sx * pcb.FromMM(escape) if axis == 'x' else 0),
|
|
a.y + (sy * pcb.FromMM(escape) if axis == 'y' else 0))
|
|
dx, dy = b.x-c.x, b.y-c.y
|
|
step = min(abs(dx), abs(dy))
|
|
for diagonal_first in (True, False):
|
|
d = (pcb.VECTOR2I(c.x+(step if dx >= 0 else -step), c.y+(step if dy >= 0 else -step))
|
|
if diagonal_first else
|
|
pcb.VECTOR2I(b.x-(step if dx >= 0 else -step), b.y-(step if dy >= 0 else -step)))
|
|
yield [a, c, d, b]
|
|
|
|
|
|
def main():
|
|
assert not (OUT / 'verification.json').exists(), 'Completed checkpoint exists'
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
base.OUT = OUT
|
|
rules_path = ROOT / 'pcb-routing-rules.json'
|
|
hashes = {p: digest(p) for p in (BOARD, rules_path)}
|
|
backup = OUT / 'before'
|
|
assert not backup.exists(), 'Existing attempt requires review before retry'
|
|
backup.mkdir()
|
|
for p in hashes:
|
|
shutil.copy2(p, backup / p.name)
|
|
contract = json.loads(rules_path.read_text(encoding='utf-8'))
|
|
board = pcb.LoadBoard(str(BOARD))
|
|
initial = base.unconnected_count(board)
|
|
original_tracks = {t.m_Uuid.AsString() for t in board.GetTracks()}
|
|
parts = {f.GetReference(): f for f in board.GetFootprints()}
|
|
candidate = BOARD.with_name('PhoneRail-Board1-local-r2-candidate.kicad_pcb')
|
|
temporary = [candidate.with_suffix(s) for s in ('.kicad_pcb', '.kicad_pro', '.kicad_dru', '.kicad_prl')]
|
|
assert all(not p.exists() for p in temporary)
|
|
accepted, blocked, reviews = [], [], []
|
|
try:
|
|
for suffix in ('.kicad_pro', '.kicad_dru'):
|
|
shutil.copy2(BOARD.with_suffix(suffix), candidate.with_suffix(suffix))
|
|
pcb.SaveBoard(str(candidate), board)
|
|
assert not base.drc(candidate, 'baseline-drc')['violations']
|
|
for ch in range(1, 6):
|
|
links = [
|
|
('comp_series', f'R{ch}06', '2', f'C{ch}10', '1', 'y'),
|
|
('comp_drive', f'U{ch}', '8', f'R{ch}06', '1', 'y'),
|
|
('comp_hf', f'U{ch}', '8', f'C{ch}11', '1', 'y'),
|
|
('soft_start', f'U{ch}', '9', f'C{ch}09', '1', 'x'),
|
|
('feedback_divider', f'R{ch}01', '2', f'R{ch}02', '1', 'x'),
|
|
]
|
|
for function, ref, pin, dest, dest_pin, axis in links:
|
|
source = next(p for p in parts[ref].Pads() if p.GetNumber() == pin)
|
|
target = next(p for p in parts[dest].Pads() if p.GetNumber() == dest_pin)
|
|
assert source.GetNetCode() > 0 and source.GetNetCode() == target.GetNetCode()
|
|
assert source.IsOnLayer(pcb.F_Cu) and target.IsOnLayer(pcb.F_Cu)
|
|
key = f'CH{ch}-{function}'
|
|
failures = []
|
|
for variant, points in enumerate(paths(source.GetPosition(), target.GetPosition(), axis)):
|
|
tracks = []
|
|
for a, b in zip(points, points[1:]):
|
|
if a == b:
|
|
continue
|
|
t = pcb.PCB_TRACK(board)
|
|
t.SetStart(a)
|
|
t.SetEnd(b)
|
|
t.SetLayer(pcb.F_Cu)
|
|
t.SetWidth(pcb.FromMM(.2))
|
|
t.SetNet(source.GetNet())
|
|
board.Add(t)
|
|
tracks.append(t)
|
|
ids = [t.m_Uuid.AsString() for t in tracks]
|
|
length = sum(pcb.ToMM(t.GetLength()) for t in tracks)
|
|
assert length < 8, (key, length)
|
|
contract['direction']['exceptions'].extend(ids)
|
|
pcb.SaveBoard(str(candidate), board)
|
|
report = base.drc(candidate, f'{key}-v{variant}')
|
|
if not report['violations'] and not routing(board, contract, 1)['errors']:
|
|
accepted.append({'connection': key, 'from': [ref, pin], 'to': [dest, dest_pin],
|
|
'net': source.GetNetname(), 'width_mm': .2, 'layer': 'F.Cu',
|
|
'length_mm': round(length, 5), 'track_uuids': ids})
|
|
reviews.append({'connection': key, 'uuids': ids, 'length_mm': round(length, 5),
|
|
'reason': 'Buck local compensation, soft-start or divider connection; same-face short path avoids signal vias; ground return remains pending'})
|
|
print('ROUTED', key, flush=True)
|
|
break
|
|
failures.append(sorted({v['type'] for v in report['violations']}))
|
|
for t in tracks:
|
|
board.Remove(t)
|
|
contract['direction']['exceptions'] = [u for u in contract['direction']['exceptions'] if u not in ids]
|
|
else:
|
|
blocked.append({'connection': key, 'candidate_violations': failures})
|
|
print('BLOCKED', key, flush=True)
|
|
assert accepted, 'No acceptable routes'
|
|
pcb.SaveBoard(str(candidate), board)
|
|
final = base.drc(candidate, 'final-drc')
|
|
reloaded = pcb.LoadBoard(str(candidate))
|
|
remaining = base.unconnected_count(reloaded)
|
|
assert initial - remaining == len(accepted), (initial, remaining, len(accepted))
|
|
assert original_tracks <= {t.m_Uuid.AsString() for t in reloaded.GetTracks()}
|
|
assert not final['violations'] and not routing(reloaded, contract, 1)['errors']
|
|
assert all(digest(p) == h for p, h in hashes.items()), 'Source changed during review'
|
|
contract['direction'].setdefault('exception_reviews', []).extend(reviews)
|
|
shutil.copy2(candidate, BOARD)
|
|
rules_path.write_text(json.dumps(contract, indent=2, ensure_ascii=False)+'\n', encoding='utf-8')
|
|
result = {'status': 'PARTIAL_LOCAL_ROUTING_NOT_RELEASED', 'accepted': accepted, 'blocked': blocked,
|
|
'unconnected_before': initial, 'unconnected_after': remaining,
|
|
'native_drc_violations': 0, 'direction_errors': 0,
|
|
'track_segments': len(list(reloaded.GetTracks())), 'vias': 0,
|
|
'reviewed_direction_exceptions': reviews, 'board_sha256': digest(BOARD),
|
|
'limits': 'Local signal copper only; quiet ground, power loops, Kelvin and reference planes remain incomplete'}
|
|
(OUT / 'verification.json').write_text(json.dumps(result, indent=2)+'\n', encoding='utf-8')
|
|
print('COMPLETE', len(accepted), 'connections;', remaining, 'remaining', flush=True)
|
|
finally:
|
|
for path in temporary:
|
|
path.unlink(missing_ok=True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|