"""Synthesize the four decision variants with nosis and write circuit.json. python synth.py nosis is a pure-Python SystemVerilog to Lattice ECP5 synthesizer. Counts are LUT4s, carry cells and slices on that device. Adder trees land on the carry chain, so CCU2C rather than LUT4 is the resource that moves with the form of the decision, and `bound` records which resource limits each variant. """ import argparse import re import subprocess import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) # repo root, for `common` from common import read_artifact, write_artifact # noqa: E402 HERE = Path(__file__).resolve().parent NOSIS_ROOT = Path(r'D:\nosis') VARIANTS = { 'sum': ('person_classifier_1p', 'runtime input'), 'sum_folded': ('person_classifier_sum_folded', 'baked'), 'popcount': ('person_classifier_popcount', 'runtime inputs'), 'popcount_folded': ('person_classifier_popcount_folded', 'baked'), } def synth_one(src: Path, top: str, build: Path, nosis_root: Path): build.mkdir(parents=True, exist_ok=True) r = subprocess.run( [sys.executable, '-m', 'nosis', str(src), '--top', top, '--stats', '-o', str(build / f'{top}.json')], cwd=str(nosis_root), capture_output=True, text=True) if r.returncode != 0: raise SystemExit(f'nosis failed on {src.name}:\n' f'{r.stdout[-2000:]}{r.stderr[-2000:]}') (build / f'{top}.log').write_text(r.stdout, encoding='utf-8') def grab(pattern, cast=int): m = re.search(pattern, r.stdout) return cast(m.group(1)) if m else None def text(pattern): m = re.search(pattern, r.stdout) return m.group(1) if m else None return {'slices': grab(r'Slices:\s+(\d+)'), 'lut4': grab(r'LUTs:\s+(\d+)'), 'ccu2c': grab(r'CCU2C:\s+(\d+)'), 'ffs': grab(r'FFs:\s+(\d+)'), 'bound': text(r'Bound:\s+(\S+)'), 'critical_path_ns': grab(r'Critical path delay:\s+([\d.]+)', float), 'device': text(r'Device:\s+(\S+)')} def main(): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument('--rtl', type=Path, default=HERE / 'rtl') ap.add_argument('--build', type=Path, default=HERE / 'build') ap.add_argument('--nosis', type=Path, default=NOSIS_ROOT) ap.add_argument('--out', type=Path, default=HERE / 'circuit.json') args = ap.parse_args() accuracy = read_artifact(args.out)['accuracy'] variants = {} print(f"{'variant':>18}{'slices':>8}{'LUT4':>7}{'CCU2C':>7}{'bound':>7}{'ns':>8}") for name, (top, thresholds) in VARIANTS.items(): src = args.rtl / f'{name}.v' if not src.exists(): raise SystemExit(f'{src} missing; run rtl_gen.py first') s = synth_one(src, top, args.build, args.nosis) s.update({'rtl': f'rtl/{name}.v', 'thresholds': thresholds}) variants[name] = s print(f'{name:>18}{s["slices"]:>8}{s["lut4"]:>7}{s["ccu2c"]:>7}' f'{s["bound"]:>7}{s["critical_path_ns"]:>8.2f}', flush=True) device = next((v['device'] for v in variants.values() if v['device']), None) write_artifact(args.out, {'variants': variants, 'accuracy': accuracy}, generator='synth.py', tool='nosis', target={'family': 'ecp5', 'device': device}, inputs='40 signed INT8 feature channels at the classifier dims', note='LUT4, carry and slice counts on an ECP5, not abstract gates') print(f'\n[done] wrote {args.out}') if __name__ == '__main__': main()