tests for safsim and wip ppsfp implementation
This commit is contained in:
1 parent
e8348f26c0
commit
8ef19cf053
3 files changed
+294
-2
No files matched your search
@@ -12,13 +12,17 @@ from kyupy.techlib import techlib_by_name, KYUPY
|
|||||||
from fsim.static import LineRoles, FaultSet
|
from fsim.static import LineRoles, FaultSet
|
||||||
from fsim.simple import SAFSimSimple
|
from fsim.simple import SAFSimSimple
|
||||||
from fsim.incremental import SAFSimIncremental
|
from fsim.incremental import SAFSimIncremental
|
||||||
|
from fsim.ppsfp import SAFSimPPSFP
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
|
algorithms = {'simple': SAFSimSimple, 'incr': SAFSimIncremental, 'ppsfp': SAFSimPPSFP}
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='A basic stuck-at fault simulator.')
|
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('-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('-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='')
|
parser.add_argument('-o', '--output', default=None, help='')
|
||||||
parser.add_argument('--seed', type=int, default=42, help='Random seed for reproducibility. Default: 42.')
|
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".')
|
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".')
|
||||||
@@ -76,8 +80,8 @@ def main():
|
|||||||
saf_collapsed = np.array(list(fs.saf_equiv_classes.keys()), dtype=np.uint32)
|
saf_collapsed = np.array(list(fs.saf_equiv_classes.keys()), dtype=np.uint32)
|
||||||
rng.shuffle(saf_collapsed)
|
rng.shuffle(saf_collapsed)
|
||||||
|
|
||||||
safsim = SAFSimSimple(c_resolved, min(patterns.shape[1], 10240))
|
safsim = algorithms[args.algorithm](c_resolved, min(patterns.shape[1], 10240))
|
||||||
log.info(f'{safsim.sim=}')
|
log.info(f'{args.algorithm} {safsim.sim=}')
|
||||||
|
|
||||||
fclasses = safsim.classify_faults(saf_collapsed, patterns)
|
fclasses = safsim.classify_faults(saf_collapsed, patterns)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import time
|
||||||
|
from collections.abc import Collection
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from kyupy import log, batchrange, cdiv, Timers, logic
|
||||||
|
from kyupy.circuit import Circuit, Node
|
||||||
|
from kyupy.logic_sim import LogicSim2V
|
||||||
|
from kyupy.techlib import KYUPY
|
||||||
|
|
||||||
|
class SAFSimPPSFP:
|
||||||
|
"""A stuck-at fault simulator that uses explicit simulations only at roots of fan-out free regions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, circuit_resolved: Circuit, batch_size: int):
|
||||||
|
self.sim = LogicSim2V(circuit_resolved, sims=batch_size)
|
||||||
|
self.timers = Timers()
|
||||||
|
|
||||||
|
self.ffr_stem2idx = {}
|
||||||
|
self.lines2ffr_stem: list[Node|None] = [None] * len(circuit_resolved.lines)
|
||||||
|
for stem, nodes in circuit_resolved.fanout_free_regions(KYUPY):
|
||||||
|
self.ffr_stem2idx[stem] = len(self.ffr_stem2idx)
|
||||||
|
for n in nodes + [stem]:
|
||||||
|
for il in n.ins.without_nones():
|
||||||
|
self.lines2ffr_stem[il.index] = stem
|
||||||
|
|
||||||
|
def classify_faults(self, faults: Collection[int], patterns: np.ndarray):
|
||||||
|
|
||||||
|
with self.timers['startup']:
|
||||||
|
self.sim.c_prop() # trigger jit
|
||||||
|
c_golden = np.zeros_like(self.sim.c)
|
||||||
|
fclass_DS = set()
|
||||||
|
nbatches = cdiv(patterns.shape[1], self.sim.sims)
|
||||||
|
nfaults = len(faults)
|
||||||
|
ffr_obs = np.zeros((len(self.ffr_stem2idx), self.sim.c.shape[2]), dtype=np.uint8)
|
||||||
|
ffr_obs_valid = np.zeros(len(self.ffr_stem2idx), dtype=np.uint8)
|
||||||
|
obs_mask = np.packbits(np.full(patterns.shape[1], 1, dtype=np.uint8), bitorder='little')
|
||||||
|
fault_to_stem_obs = obs_mask.copy()
|
||||||
|
fault_act = obs_mask.copy()
|
||||||
|
|
||||||
|
with self.timers['sim'], log.progress() as p:
|
||||||
|
for bidx, (bo, bs) in enumerate(batchrange(patterns.shape[1], self.sim.sims)):
|
||||||
|
self.sim.s_assign[:, :bs] = patterns[:, bo:bo+bs]
|
||||||
|
self.sim.s_to_c()
|
||||||
|
self.sim.c_dirty[...] = 1
|
||||||
|
with self.timers['sim_full_prop']:
|
||||||
|
self.sim.c_prop()
|
||||||
|
c_golden[...] = self.sim.c
|
||||||
|
c_golden_poppo = c_golden[self.sim.poppo_c_locs]
|
||||||
|
ffr_obs_valid[...] = 0
|
||||||
|
|
||||||
|
for fidx, fault in enumerate(faults):
|
||||||
|
fault_site = fault//2
|
||||||
|
fault_polarity = fault&1
|
||||||
|
ffr_stem = self.lines2ffr_stem[fault_site]
|
||||||
|
assert ffr_stem is not None
|
||||||
|
ffr_idx = self.ffr_stem2idx[ffr_stem]
|
||||||
|
|
||||||
|
# compute FFR observability vector ffr_obs[ffr_idx] if necessary
|
||||||
|
if not ffr_obs_valid[ffr_idx]:
|
||||||
|
if len(ffr_stem.outs) > 1:
|
||||||
|
self.sim.c_dirty[...] = 0
|
||||||
|
with self.timers['sim_ffr_prop']:
|
||||||
|
self.sim.c_prop(fault_line=ffr_stem.ins[0].index, fault_model=2)
|
||||||
|
with self.timers['sim_ffr_obs']:
|
||||||
|
ffr_obs[ffr_idx] = np.bitwise_or.reduce(c_golden_poppo ^ self.sim.c[self.sim.poppo_c_locs], axis=0) & obs_mask
|
||||||
|
with self.timers['sim_ffr_reset']:
|
||||||
|
self.sim.c[...] = c_golden # clear fault
|
||||||
|
else: # primary output, completely observable
|
||||||
|
ffr_obs[ffr_idx] = obs_mask
|
||||||
|
ffr_obs_valid[ffr_idx] = 1
|
||||||
|
|
||||||
|
# FIXME: compute observability of fault location at FFR stem
|
||||||
|
fault_to_stem_obs[...] = obs_mask
|
||||||
|
|
||||||
|
# compute fault activation
|
||||||
|
fault_act[...] = self.sim.c[self.sim.c_locs[fault_site]] & obs_mask
|
||||||
|
if fault_polarity: # stuck-at 1?
|
||||||
|
fault_act ^= obs_mask # activated iff signal is 0
|
||||||
|
|
||||||
|
if np.bitwise_or.reduce(ffr_obs[ffr_idx] & fault_to_stem_obs & fault_act) != 0:
|
||||||
|
fclass_DS.add(fault)
|
||||||
|
p.update(((fidx+1) / nfaults) * ((bidx+1) / nbatches))
|
||||||
|
|
||||||
|
return {'DS': fclass_DS, 'NO': set(faults) - fclass_DS}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
"""Unit tests for the stuck-at fault simulators (the SAF* classes).
|
||||||
|
|
||||||
|
Each fault is identified by ``line.index * 2 + polarity`` where polarity 0 is
|
||||||
|
stuck-at-0 and polarity 1 is stuck-at-1. ``classify_faults`` returns a dict
|
||||||
|
with the detected-by-simulation set under ``'DS'`` and the not-observed set
|
||||||
|
under ``'NO'``.
|
||||||
|
|
||||||
|
The three simulators are exercised side-by-side:
|
||||||
|
* ``SAFSimSimple`` - brute force, exact.
|
||||||
|
* ``SAFSimIncremental`` - incremental cone re-simulation, exact.
|
||||||
|
* ``SAFSimPPSFP`` - FFR-based, deliberately *optimistic*: its stem
|
||||||
|
observability is hard-wired to "always observable" (see the FIXME in
|
||||||
|
``ppsfp.py``), so its DS set is a super-set of the exact DS set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from kyupy import bench, logic
|
||||||
|
from kyupy.techlib import KYUPY
|
||||||
|
from fsim.static import FaultSet
|
||||||
|
from fsim.simple import SAFSimSimple
|
||||||
|
from fsim.incremental import SAFSimIncremental
|
||||||
|
from fsim.ppsfp import SAFSimPPSFP
|
||||||
|
|
||||||
|
|
||||||
|
# The two exact simulators must always agree; PPSFP only over-approximates.
|
||||||
|
ALL_ALGS = [SAFSimSimple, SAFSimIncremental, SAFSimPPSFP]
|
||||||
|
|
||||||
|
|
||||||
|
def make_patterns(*specs):
|
||||||
|
"""Build a 2D pattern array from per-pattern strings via ``logic.mvarray``.
|
||||||
|
|
||||||
|
Each string holds one character per s-node, ordered as the inputs and
|
||||||
|
outputs are defined in the netlist (output positions are don't-cares here,
|
||||||
|
overwritten by simulation). The result has shape ``(n_s_nodes, n_patterns)``
|
||||||
|
as expected by the simulators; a lone pattern is reshaped to a single column.
|
||||||
|
"""
|
||||||
|
pat = logic.mvarray(*specs)
|
||||||
|
return pat if pat.ndim == 2 else pat.reshape(-1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def classify(alg, circuit, faults, patterns):
|
||||||
|
# PPSFP requires the batch size to match the pattern count (as main.py does).
|
||||||
|
sim = alg(circuit, patterns.shape[1])
|
||||||
|
return sim.classify_faults(list(faults), patterns)
|
||||||
|
|
||||||
|
|
||||||
|
def saf(line, polarity):
|
||||||
|
return line.index * 2 + polarity
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Inline single-gate circuits with hand-computed expectations.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('alg', ALL_ALGS)
|
||||||
|
def test_inv_single_pattern(alg):
|
||||||
|
# s-nodes are 'i','o'. i=0 => golden o=1. Detect faults that flip o off 1.
|
||||||
|
c = bench.parse('input(i) output(o) o=INV(i)')
|
||||||
|
oline = c.forks['o'].ins[0]
|
||||||
|
iline = oline.driver.ins[0]
|
||||||
|
pat = make_patterns('0-') # i=0 (o position is a don't-care)
|
||||||
|
faults = [saf(oline, 0), saf(oline, 1), saf(iline, 0), saf(iline, 1)]
|
||||||
|
r = classify(alg, c, faults, pat)
|
||||||
|
# o s-a-0 forces 0 (!=1) -> detected; i s-a-1 forces i=1 -> o=0 -> detected.
|
||||||
|
assert r['DS'] == {saf(oline, 0), saf(iline, 1)}
|
||||||
|
# o s-a-1 already matches golden; i s-a-0 keeps i=0 -> nothing changes.
|
||||||
|
assert r['NO'] == {saf(oline, 1), saf(iline, 0)}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('alg', ALL_ALGS)
|
||||||
|
def test_inv_both_polarities_detected(alg):
|
||||||
|
# Applying both i=0 and i=1 sensitises every fault of the inverter.
|
||||||
|
c = bench.parse('input(i) output(o) o=INV(i)')
|
||||||
|
oline = c.forks['o'].ins[0]
|
||||||
|
iline = oline.driver.ins[0]
|
||||||
|
pat = make_patterns('00', '10') # i=0 and i=1
|
||||||
|
faults = [saf(oline, 0), saf(oline, 1), saf(iline, 0), saf(iline, 1)]
|
||||||
|
r = classify(alg, c, faults, pat)
|
||||||
|
assert r['DS'] == set(faults)
|
||||||
|
assert r['NO'] == set()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('alg', ALL_ALGS)
|
||||||
|
def test_and2_output(alg):
|
||||||
|
# s-nodes are 'i0','i1','o'.
|
||||||
|
c = bench.parse('input(i0,i1) output(o) o=AND2(i0,i1)')
|
||||||
|
oline = c.forks['o'].ins[0]
|
||||||
|
o_sa0, o_sa1 = saf(oline, 0), saf(oline, 1)
|
||||||
|
|
||||||
|
# i0=i1=1 => golden o=1: only o s-a-0 is observable.
|
||||||
|
r = classify(alg, c, [o_sa0, o_sa1], make_patterns('110'))
|
||||||
|
assert r['DS'] == {o_sa0}
|
||||||
|
assert r['NO'] == {o_sa1}
|
||||||
|
|
||||||
|
# i0=0,i1=1 => golden o=0: only o s-a-1 is observable.
|
||||||
|
r = classify(alg, c, [o_sa0, o_sa1], make_patterns('010'))
|
||||||
|
assert r['DS'] == {o_sa1}
|
||||||
|
assert r['NO'] == {o_sa0}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('alg', ALL_ALGS)
|
||||||
|
def test_two_level_nand_propagation(alg):
|
||||||
|
# o = NAND2(NAND2(a,b), c); s-nodes 'a','b','c','o'. a=b=c=1: n=0, o=1.
|
||||||
|
c = bench.parse('input(a,b,c) output(o) n=NAND2(a,b) o=NAND2(n,c)')
|
||||||
|
nline = c.forks['n'].ins[0]
|
||||||
|
oline = c.forks['o'].ins[0]
|
||||||
|
faults = [saf(nline, 0), saf(nline, 1), saf(oline, 0), saf(oline, 1)]
|
||||||
|
r = classify(alg, c, faults, make_patterns('1110'))
|
||||||
|
# n already 0 (s-a-0 invisible); n s-a-1 -> o=0; o s-a-0 visible; o already 1.
|
||||||
|
assert r['DS'] == {saf(nline, 1), saf(oline, 0)}
|
||||||
|
assert r['NO'] == {saf(nline, 0), saf(oline, 1)}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# c17 fixture: small, fully testable combinational benchmark.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def _c17_exhaustive_patterns():
|
||||||
|
"""All 2**5 input combinations of c17.
|
||||||
|
|
||||||
|
s-nodes are inputs 1,2,3,6,7 then outputs 22,23 (netlist order); the two
|
||||||
|
output positions are don't-cares appended after the five input bits.
|
||||||
|
"""
|
||||||
|
return make_patterns(*[''.join(combo) + '--'
|
||||||
|
for combo in itertools.product('01', repeat=5)])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('alg', ALL_ALGS)
|
||||||
|
def test_c17_fully_detected(alg, c17_bench, c17_resolved):
|
||||||
|
# c17 has no redundant faults, so exhaustive patterns detect every fault.
|
||||||
|
fs = FaultSet(c17_bench, KYUPY, c17_resolved)
|
||||||
|
faults = list(fs.saf_equiv_classes.keys())
|
||||||
|
pat = _c17_exhaustive_patterns()
|
||||||
|
r = classify(alg, c17_resolved, faults, pat)
|
||||||
|
assert r['DS'] == set(faults)
|
||||||
|
assert r['NO'] == set()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('alg', ALL_ALGS)
|
||||||
|
def test_c17_output_fault_class(alg, c17_resolved):
|
||||||
|
# All-zero inputs: output 22 settles to 0, so only its s-a-1 is observable.
|
||||||
|
o22 = c17_resolved.forks['22'].ins[0]
|
||||||
|
pat = make_patterns('00000--') # all inputs 0
|
||||||
|
r = classify(alg, c17_resolved, [saf(o22, 0), saf(o22, 1)], pat)
|
||||||
|
assert r['DS'] == {saf(o22, 1)}
|
||||||
|
assert r['NO'] == {saf(o22, 0)}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# s27 fixture: exposes the exact-vs-optimistic difference between simulators.
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
def _s27_setup(s27_bench, s27_resolved):
|
||||||
|
fs = FaultSet(s27_bench, KYUPY, s27_resolved)
|
||||||
|
faults = list(fs.saf_equiv_classes.keys())
|
||||||
|
rng = np.random.default_rng(1)
|
||||||
|
pat = rng.choice([logic.ZERO, logic.ONE],
|
||||||
|
size=(len(s27_resolved.s_nodes(KYUPY)), 64)).astype(np.uint8)
|
||||||
|
return faults, pat
|
||||||
|
|
||||||
|
|
||||||
|
def test_s27_exact_simulators_agree(s27_bench, s27_resolved):
|
||||||
|
faults, pat = _s27_setup(s27_bench, s27_resolved)
|
||||||
|
assert len(faults) == 32 # collapsed fault count, see test_fault_set.test_s27
|
||||||
|
|
||||||
|
simple = classify(SAFSimSimple, s27_resolved, faults, pat)
|
||||||
|
incr = classify(SAFSimIncremental, s27_resolved, faults, pat)
|
||||||
|
|
||||||
|
# The two exact simulators must classify every fault identically.
|
||||||
|
assert simple['DS'] == incr['DS']
|
||||||
|
assert simple['NO'] == incr['NO']
|
||||||
|
# This deterministic pattern set leaves exactly one fault unobserved.
|
||||||
|
assert len(simple['DS']) == 31
|
||||||
|
assert len(simple['NO']) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_s27_ppsfp_over_approximates(s27_bench, s27_resolved):
|
||||||
|
faults, pat = _s27_setup(s27_bench, s27_resolved)
|
||||||
|
|
||||||
|
exact = classify(SAFSimSimple, s27_resolved, faults, pat)
|
||||||
|
ppsfp = classify(SAFSimPPSFP, s27_resolved, faults, pat)
|
||||||
|
|
||||||
|
# PPSFP's optimistic stem observability can only add detections, never drop.
|
||||||
|
assert exact['DS'] <= ppsfp['DS']
|
||||||
|
# Here it reports the lone exact-NO fault as detected too.
|
||||||
|
assert ppsfp['DS'] == exact['DS'] | exact['NO']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('alg', ALL_ALGS)
|
||||||
|
def test_ppsfp_multibranch_stem_observability(alg):
|
||||||
|
# Stem `s` fans out to two AND gates; with sel1=0,sel2=1 the stem is
|
||||||
|
# observable only through the o2 branch (the stem's *second* fanout line).
|
||||||
|
c = bench.parse('input(a,b,sel1,sel2) output(o1,o2) '
|
||||||
|
's=NAND2(a,b) o1=AND2(s,sel1) o2=AND2(s,sel2)')
|
||||||
|
a_sa0 = saf(c.forks['s'].ins[0].driver.ins[0], 0)
|
||||||
|
# a=1,b=1,sel1=0,sel2=1: a s-a-0 flips s, propagating through o2 only.
|
||||||
|
pat = make_patterns('1101--')
|
||||||
|
r = classify(alg, c, [a_sa0], pat)
|
||||||
|
assert r['DS'] == {a_sa0}
|
||||||
Reference in new issue
Block a user