Fault Simulation
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

104 lines
4.8 KiB

#!/usr/bin/env -S uv run
import argparse
import subprocess
from pathlib import Path
import numpy as np
from kyupy import verilog, bench, log, logic, batchrange, atalanta, stil
from kyupy.techlib import techlib_by_name, KYUPY
from fsim.static import LineRoles, FaultSet, CircuitPartition
from fsim.simple import SAFSimSimple
from fsim.incremental import SAFSimIncremental
from fsim.ppsfp import SAFSimPPSFP
def main():
algorithms = {'simple': SAFSimSimple, 'incr': SAFSimIncremental, 'ppsfp': SAFSimPPSFP}
parser = argparse.ArgumentParser(description='A basic stuck-at fault simulator.')
parser.add_argument('-t', '--tlib', default='SKY130', help=f'Techlib for verilog circuit. Default: SKY130, available: {sorted(techlib_by_name.keys())}.')
parser.add_argument('-p', '--patterns', default='1024', help='Pattern file or number of random patterns to simulate. Default: 1024.')
parser.add_argument('-a', '--algorithm', default='simple', help=f'Fault simulation algorithm. Default: simple, available: {sorted(algorithms.keys())}.')
parser.add_argument('-o', '--output', default=None, help='Write fault list to given file')
parser.add_argument('-k', '--partitions', default=1, help='Partition circuit into partitions')
parser.add_argument('--seed', type=int, default=42, help='Random seed for reproducibility. Default: 42.')
parser.add_argument('circuit', help='Gate-level verilog, bench, or nix package to import. See available packages: "nix flake show github:s-holst/benchmark-circuits".')
args = parser.parse_args()
if int(args.partitions) > 1:
assert args.algorithm == 'ppsfp', "partitioning is only supported for PPSFP."
if not (circuit_path := Path(args.circuit)).exists(): # fallback to published nix package.
nix_cmd = f"nix build github:s-holst/benchmark-circuits#{args.circuit} --print-out-paths --no-link"
benchmark_path = Path(subprocess.check_output(nix_cmd.split(), text=True).strip())
circuit_path = next(benchmark_path.glob("*/nl/*.nl.v"))
log.info(f'loading {circuit_path} ...')
if circuit_path.name.endswith('.bench'):
args.tlib = techlib_by_name['KYUPY']
c = bench.load(circuit_path)
else:
args.tlib = techlib_by_name[args.tlib]
c = verilog.load(circuit_path, tlib=args.tlib)
stats = {k.replace('__',''): v for k, v in c.stats(args.tlib).items() if k.startswith('__') or k.endswith('put')}
log.info(f'circuit {stats=}')
log.info(f'line role stats={LineRoles(c, args.tlib).stats}')
c_resolved = c.copy()
c_resolved.resolve_tlib_cells(args.tlib)
fs = FaultSet(c, args.tlib, c_resolved)
log.info(f'fault sites: {len(fs.fault_sites)}')
log.info(f'uncollapsed stuck-at fault count: {len(fs.saf_set)}')
log.info(f'collapsed stuck-at fault count: {len(fs.saf_equiv_classes)}')
partitions = CircuitPartition(c_resolved, int(args.partitions))
partitions.print_stats()
rng = np.random.default_rng(args.seed)
if not (tests_path := Path(args.patterns)).exists(): # fallback to random patterns.
patterns = rng.choice(
[logic.ZERO, logic.ONE],
size=(len(c_resolved.s_nodes(KYUPY)), int(args.patterns)),
).astype(np.uint8)
else:
log.info(f'loading {tests_path} ...')
if tests_path.name.endswith('.stil'):
patterns = stil.load(tests_path).tests(c_resolved)
else:
patterns = atalanta.load(tests_path).tests(c_resolved)
saf_collapsed = np.array(list(fs.saf_equiv_classes.keys()), dtype=np.uint32)
rng.shuffle(saf_collapsed)
if int(args.partitions) > 1:
safsim = algorithms[args.algorithm](c_resolved, min(patterns.shape[1], 10240), partitions.partitions)
else:
safsim = algorithms[args.algorithm](c_resolved, min(patterns.shape[1], 10240))
log.info(f'{args.algorithm} {safsim.sim=}')
fclasses = safsim.classify_faults(saf_collapsed, patterns)
log.info(f'{safsim.timers=}')
sim_performance = stats['comb'] * len(saf_collapsed) * patterns.shape[1] / safsim.timers['sim'].s
log.info(f'fsim performance: {sim_performance:.2e} gfp/s')
log.info(f'detected by simulation (collapsed): {len(fclasses["DS"])}/{len(saf_collapsed)} - {len(fclasses["DS"])/len(saf_collapsed)*100:.2f}%')
if args.output is not None:
out_path = Path(args.output)
with open(out_path, 'w') as f:
log.info(f'Writing {out_path.absolute()} ...')
for flt_rep in fclasses["NO"]:
for flt in fs.saf_equiv_classes[flt_rep]:
f.write(f'{fs.fault_type_str(flt)}\tNO\t{fs.fault_site_str(c, args.tlib, flt)}\n')
for flt_rep in fclasses["DS"]:
for flt in fs.saf_equiv_classes[flt_rep]:
f.write(f'{fs.fault_type_str(flt)}\tDS\t{fs.fault_site_str(c, args.tlib, flt)}\n')
if __name__ == "__main__":
main()