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.
 
 
 
 

203 lines
8.1 KiB

"""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}