Browse Source

basic project setup, tools and fsim

main
stefan 3 weeks ago
commit
a74881de49
  1. 170
      .gitignore
  2. 4
      .gitmodules
  3. 1
      .python-version
  4. 34
      README.md
  5. 153
      flake.lock
  6. 35
      flake.nix
  7. 1
      kyupy
  8. 97
      main.py
  9. 33
      pyproject.toml
  10. 0
      src/fsim/__init__.py
  11. 193
      src/fsim/static.py
  12. 2488
      tests/c6288.bench
  13. 119
      tests/test_fault_set.py
  14. 62
      tests/test_line_roles.py
  15. 1950
      uv.lock

170
.gitignore vendored

@ -0,0 +1,170 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# ---> JupyterNotebooks
# gitignore template for Jupyter Notebooks
# website: http://jupyter.org/
.ipynb_checkpoints
*/.ipynb_checkpoints/*
# IPython
profile_default/
ipython_config.py
# Remove previous ipynb_checkpoints
# git rm -r .ipynb_checkpoints/
result
*nogit*

4
.gitmodules vendored

@ -0,0 +1,4 @@
[submodule "kyupy"]
path = kyupy
url = git@git.vlab.cse.kyutech.ac.jp:stefan/kyupy.git
branch = devel

1
.python-version

@ -0,0 +1 @@
3.13

34
README.md

@ -0,0 +1,34 @@
# Fault Simulation
PPSFP (Parallel-Pattern Single-Fault Propagation) style fault simulators and other utility functions for use in various projects.
These fault simulators are meant to be correct (ensured by unit-tests) and reasonably fast.
## Quick Start
This project has submodules. To ensure everything is up-to-date, run
```
git submodule update --init --recursive
```
after `git clone`, `git pull` or `git checkout`.
This project manages reproducible programming environments with:
- [uv](https://docs.astral.sh/uv/) for managing python environments.
- [nix](https://nixos.org) for managing non-python tools and benchmark designs. Follow [this guide](https://librelane.readthedocs.io/en/stable/installation/nix_installation/index.html) or [this guide](https://github.com/fossi-foundation/nix-eda/blob/main/docs/installation.md) to setup [nix-eda](https://github.com/fossi-foundation/nix-eda/tree/main) binary cache to avoid re-building EDA-related tools.
Smoke-test: `uv run pytest`
For running Jupyter Notebooks in the reproducible programming environment, install a kernelspec with this command:
```
uv run ipython kernel install --user --env VIRTUAL_ENV $(pwd)/.venv --env PATH ${PATH} --name=uv-env
```
Run a Jupyter Lab server locally:
```
uv run jupyter lab
```
Choose `uv-env` as kernel in Jupyter Lab.
`uv run main.py <circuit>` runs a naive, baseline fault simulation.
`nix develop` makes [quaigh](https://github.com/coloquinte/quaigh) available, another simulator/atpg written in rust.

153
flake.lock

@ -0,0 +1,153 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"nl2bench",
"quaigh",
"naersk",
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1752475459,
"narHash": "sha256-z6QEu4ZFuHiqdOPbYss4/Q8B0BFhacR8ts6jO/F/aOU=",
"owner": "nix-community",
"repo": "fenix",
"rev": "bf0d6f70f4c9a9cf8845f992105652173f4b617f",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"naersk": {
"inputs": {
"fenix": "fenix",
"nixpkgs": [
"nl2bench",
"quaigh",
"nixpkgs"
]
},
"locked": {
"lastModified": 1763384566,
"narHash": "sha256-r+wgI+WvNaSdxQmqaM58lVNvJYJ16zoq+tKN20cLst4=",
"owner": "nix-community",
"repo": "naersk",
"rev": "d4155d6ebb70fbe2314959842f744aa7cabbbf6a",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nix-eda": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1768614091,
"narHash": "sha256-UElQINIEz44UlEXP9eIJmjMuCIkP9fS8+WitJwyTYzw=",
"owner": "fossi-foundation",
"repo": "nix-eda",
"rev": "8d6f4ea82716b59bc6eff6b070304e1b916b2c75",
"type": "github"
},
"original": {
"owner": "fossi-foundation",
"ref": "6.0.2",
"repo": "nix-eda",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1766201043,
"narHash": "sha256-eplAP+rorKKd0gNjV3rA6+0WMzb1X1i16F5m5pASnjA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b3aad468604d3e488d627c0b43984eb60e75e782",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nl2bench": {
"inputs": {
"nix-eda": "nix-eda",
"quaigh": "quaigh"
},
"locked": {
"lastModified": 1777493983,
"narHash": "sha256-QY90b9hKxAt68xYvRbcMXbDrLTQwsx9TpG8+CGG8iK8=",
"owner": "donn",
"repo": "nl2bench",
"rev": "0c5aadc0119c6178357102f7eb8ff2bdb0dc8a12",
"type": "github"
},
"original": {
"owner": "donn",
"repo": "nl2bench",
"type": "github"
}
},
"quaigh": {
"inputs": {
"naersk": "naersk",
"nixpkgs": [
"nl2bench",
"nix-eda",
"nixpkgs"
]
},
"locked": {
"lastModified": 1768613600,
"narHash": "sha256-dcGI2FVd0SyrN5RaZXc5VrYY37pKtb/pZhNSV7MtNGE=",
"owner": "donn",
"repo": "quaigh",
"rev": "994aef4a5100ef3756032af86c51df1f932dd601",
"type": "github"
},
"original": {
"owner": "donn",
"ref": "nixos_25.11",
"repo": "quaigh",
"type": "github"
}
},
"root": {
"inputs": {
"nl2bench": "nl2bench"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1752428706,
"narHash": "sha256-EJcdxw3aXfP8Ex1Nm3s0awyH9egQvB2Gu+QEnJn2Sfg=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "591e3b7624be97e4443ea7b5542c191311aa141d",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

35
flake.nix

@ -0,0 +1,35 @@
#
# Enable this shell by calling 'nix develop' in the same directory as this file.
#
{
description = "project development shell";
inputs = {
# 'quaigh' (logic synthesis / ATPG) is not in nixpkgs. The nl2bench overlay
# builds it from crates.io via naersk and pulls in nix-eda (and its nixpkgs).
nl2bench.url = "github:donn/nl2bench";
};
outputs = { self, nl2bench }:
let
nix-eda = nl2bench.inputs.nix-eda;
nixpkgs = nix-eda.inputs.nixpkgs;
in {
devShells = nix-eda.forAllSystems (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ nix-eda.overlays.default nl2bench.overlays.default ];
};
in {
default = pkgs.mkShell {
packages = [
pkgs.uv
pkgs.quaigh
];
};
}
);
};
}

1
kyupy

@ -0,0 +1 @@
Subproject commit 449bf9a0314c43cd0dfbafb6ad98c97d9c9803ce

97
main.py

@ -0,0 +1,97 @@
#!/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
from kyupy.techlib import techlib_by_name, KYUPY
from kyupy.logic_sim import LogicSim2V
from fsim.static import LineRoles, FaultSet
def main():
parser = argparse.ArgumentParser(description='Fault Stats')
parser.add_argument('-t', '--tlib', default='SKY130', help=f'techlib of circuit. default: SKY130, available: {sorted(techlib_by_name.keys())}.')
parser.add_argument('-p', '--patterns', default=1024, help='Number of random patterns to simulate.')
parser.add_argument('--seed', type=int, default=42, help='Random seed for reproducibility (default: 42).')
parser.add_argument('circuit', help='gate-level verilog file or nix package to import. See "nix flake show github:s-holst/benchmark-circuits" for available packages.')
args = parser.parse_args()
args.patterns = int(args.patterns)
args.tlib = techlib_by_name[args.tlib]
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'):
c = bench.load(circuit_path)
args.tlib = techlib_by_name['KYUPY']
else:
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=}')
lr = LineRoles(c, args.tlib)
log.info(f'line role stats={lr.stats}')
fs = FaultSet(c, args.tlib)
log.info(f'fault sites: {len(fs.fault_sites)}')
log.info(f'collapsed stuck-at fault count: {len(fs.saf_equiv_classes)}')
c.resolve_tlib_cells(args.tlib)
ffr_stems = []
for stem, _ in c.fanout_free_regions(KYUPY):
if len(stem.outs) > 0 and stem.outs[0] is not None:
ffr_stems.append(stem.outs[0])
ffr_stems = np.array(ffr_stems, dtype=np.uint32)
log.info(f'FFR count: {len(ffr_stems)}')
sim = LogicSim2V(c, sims=min(args.patterns, 10240))
rng = np.random.default_rng(args.seed)
patterns = rng.choice(
[logic.ZERO, logic.ONE],
size=(sim.s_len, args.patterns),
).astype(np.uint8)
golden = np.zeros_like(patterns)
log.info(f'{sim=}')
sim.simulate(patterns, golden)
log.info(f'golden sim finished.')
syndrome = np.zeros_like(patterns)
injection_faults = np.array(list(fs.saf_equiv_classes.keys()), dtype=np.uint32)
rng.shuffle(injection_faults)
undetected = set()
detected = set()
with log.progress() as p:
for fidx, fault in enumerate(injection_faults):
fault_site = fault//2
fault_polarity = fault&1
p.update((fidx+1) / len(injection_faults), f'd:{len(detected)} u:{len(undetected)}')
for bo, bs in batchrange(patterns.shape[1], sim.sims):
sim.s_assign[:, :bs] = patterns[:, bo:bo+bs]
sim.s_to_c()
sim.c_prop(fault_line=fault_site, fault_model=fault_polarity)
sim.c_to_s()
syndrome[:, bo:bo+bs] = sim.s_result[:,:bs]
if np.allclose(golden, syndrome):
undetected.add(fault)
else:
detected.add(fault)
log.info(f'detected by simulation: {len(detected)}/{len(injection_faults)} - {len(detected)/len(injection_faults)*100:.1f}%')
if __name__ == "__main__":
main()

33
pyproject.toml

@ -0,0 +1,33 @@
[project]
name = "fsim"
version = "0.0.1"
description = "PPSFP (Parallel-Pattern Single-Fault Propagation) style fault simulators and other utility functions for use in various projects"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"kyupy",
"numba>=0.65.1",
"numba-cuda[cu13]>=0.30.1; sys_platform != 'darwin'",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"isort>=8.0.1",
"pytest>=9.0.1",
"ipython>=9.14.1",
"jupyter>=1.1.1",
"graphviz",
]
[tool.uv.workspace]
members = [
"kyupy",
]
[tool.uv.sources]
kyupy = { workspace = true, editable = true }

0
src/fsim/__init__.py

193
src/fsim/static.py

@ -0,0 +1,193 @@
"""A set of tools for static (structural) analysis of gate-level circuits.
"""
from collections import defaultdict
from itertools import product
import numpy as np
from kyupy.circuit import Circuit, Node
from kyupy.techlib import TechLib
class LineRoles:
"""Computes signal line roles of a circuit depending on the structural reach of each signal.
The possible roles are any combination of the following:
- 'lout' / LOGIC_OUT: there is a structural path to a primary output via zero or more combinational gates.
- 'lseq' / LOGIC_SEQ: there is a structural path to a D input of a sequential cell via zero or more combinational gates.
- 'clk' / CLOCK: there is a structural path to a clock input of a sequential cell via zero or more combinational gates.
- 'rst' / RESET: there is a structural path to an asynchronous set or reset input of a sequential cell via zero or more combinational gates.
- 'dft' / DFT: there is a structural path to an scan-in or scan-enable input of a sequential cell via zero or more combinational gates.
Signal lines that may propagate to various cells may have multiple roles.
The roles are encoded using bit positions in integers.
The corresponding masks are defines as class constants LOGIC_OUT, LOGIC_SEQ, etc.
"""
LOGIC_OUT = 1
LOGIC_SEQ = 2
CLOCK = 4
RESET = 8
DFT = 16
def __init__(self, circuit: Circuit, tlib: TechLib):
self.line2roles = np.zeros(len(circuit.lines), dtype=np.uint8)
"""Array of roles for each line index in the circuit.
"""
outputs = {n for n in circuit.io_nodes if len(n.ins) > 0}
for n in circuit.reversed_topological_order(tlib):
if tlib.is_dff(n.kind):
for il in n.ins.without_nones():
pname = tlib.pin_name(n.kind, il.reader_pin)
if pname == 'D': self.line2roles[il] = self.LOGIC_SEQ
elif pname in ('CK', 'CLK', 'CLK_N'): self.line2roles[il] = self.CLOCK
elif pname in ('SET_B', 'RESET_B', 'SETB', 'RSTB', 'RN', 'SN'): self.line2roles[il] = self.RESET
elif pname in ('SCD', 'SCE', 'SE', 'SI'): self.line2roles[il] = self.DFT
else: raise ValueError(f'Unable to classify pin name "{pname}" for "{n.kind}".')
elif n in outputs: # or len(n.outs) == 0:
for il in n.ins.without_nones():
self.line2roles[il] = self.LOGIC_OUT
else:
lc = 0
for ol in n.outs.without_nones():
lc |= self.line2roles[ol]
for il in n.ins.without_nones():
self.line2roles[il] = lc
self.roles2lines: defaultdict[int,set[int]] = defaultdict(set)
"""A dictionary that maps a role combination to a set of line indices matching that role combination.
"""
for li, lc in enumerate(self.line2roles):
self.roles2lines[lc].add(li)
@classmethod
def roles_str(cls, roles: int) -> str:
"""Returns a human-readable string for a role combination given as int.
"""
roles_list = [n for v, n in (
(cls.LOGIC_OUT, 'lout'),
(cls.LOGIC_SEQ, 'lseq'),
(cls.CLOCK, 'clk'),
(cls.RESET, 'rst'),
(cls.DFT, 'dft')) if (roles&v) != 0]
return 'none' if len(roles_list) == 0 else '|'.join(roles_list)
@property
def stats(self):
"""A dictionary mapping each role combination to the number of matching signal lines.
"""
return {self.roles_str(roles): len(self.roles2lines[roles]) for roles in range(256) if len(self.roles2lines[roles]) > 0}
class FaultSet:
"""Computes eligible fault sites and a collapsed set of stuck-at faults for a circuit.
Faults in this class are represented by integers of the form: `line_index * 2 + polarity`.
Line index defines the location in the circuit, and polarity is 0 for stuck-at-0 and 1 for stuck-at-1.
Faults sites are located only in the combinational portion of the circuit (excluding clock tree, asynchronous set/reset logic, dft/scan).
Stuck-at faults are collapsed using basic controlling-value theory of simple gates.
"""
def __init__(self, circuit: Circuit, tlib: TechLib):
lr = LineRoles(circuit, tlib)
self.fault_sites = {l.index for l in circuit.lines
if (lr.line2roles[l] & (lr.LOGIC_OUT|lr.LOGIC_SEQ)) # line propagates to output or sequential element
and (
(l.driver.kind == '__fork__' and len(l.driver.outs) > 1) # fanout branch
or (l.driver.kind == '__fork__' and len(l.driver.ins) == 0) # primary input
or (l.driver.kind != '__fork__' ) # gate output
)
}
"""Set of line indices of all eligible fault sites. A fault site is eligible iff:
fault effect can propagate via combinational gates (only) to outputs of to a flip-flop, AND
(fault site is driven by a standard cell OR it is a primary input signal OR it is a branch of a fan-out).
"""
self.saf_set = {site*2+polarity for site, polarity in product(self.fault_sites, (0, 1))}
"""Set of all stuck-at faults in the circuit. Each stuck-at fault is encoded as integer i with
i & 1 being the polarity and i // 2 the circuit line index of its location.
"""
self.saf_equiv_classes : dict[int,set[int]] = {}
r"""Dictionary mapping a representative stuck-at fault to its set of equivalent faults. The
representative is always the fault closest to the circuit's outputs. The value set of
representative f always contains at least f (:math:`f \in` saf_equiv_classes[f]).
"""
def collect_equivalent_faults(circuit: Circuit, fault: int) -> set:
site = fault//2
polarity = fault&1
driver = circuit.lines[site].driver
# Stop collapsing at fanout stems, primary inputs, and cells whose
# output faults have no equivalent single-input fault (XOR/XNOR, MUX, the two-term
# AND-OR/OR-AND cells, and sequential elements).
if len(driver.outs) > 1 or len(driver.ins) == 0 or driver.kind in ('XOR2', 'XOR3', 'XOR4', 'XNOR2', 'XNOR3', 'XNOR4', 'MUX21', 'AO22', 'AOI22', 'OA22', 'OAI22', 'DFF', 'LATCH'): return {fault}
if driver.kind == 'INV1': return {fault} | collect_equivalent_faults(circuit, driver.ins[0].index*2 + (1-polarity))
if driver.kind in ('BUF1', '__fork__'): return {fault} | collect_equivalent_faults(circuit, driver.ins[0].index*2 + (polarity))
def collapse(*faults: tuple) -> set:
"""Collapse the output fault onto the given (input_line, stuck_value) pairs."""
equiv = {fault}
for site, value in faults:
equiv |= collect_equivalent_faults(circuit, site.index*2 + value)
return equiv
# Simple gates: driving any input to its controlling value forces the output.
# AND : out s-a-0 == every input s-a-0 (out s-a-1 has no equivalent)
# NAND: out s-a-1 == every input s-a-0 (out s-a-0 has no equivalent)
# OR : out s-a-1 == every input s-a-1 (out s-a-0 has no equivalent)
# NOR : out s-a-0 == every input s-a-1 (out s-a-1 has no equivalent)
if driver.kind.startswith('NAND'):
return collapse(*((il, 0) for il in driver.ins.without_nones())) if polarity == 1 else {fault}
if driver.kind.startswith('AND'):
return collapse(*((il, 0) for il in driver.ins.without_nones())) if polarity == 0 else {fault}
if driver.kind.startswith('NOR'):
return collapse(*((il, 1) for il in driver.ins.without_nones())) if polarity == 0 else {fault}
if driver.kind.startswith('OR'):
return collapse(*((il, 1) for il in driver.ins.without_nones())) if polarity == 1 else {fault}
# AND-OR / OR-AND complex gates: only the "single" (un-paired) terms force the output.
# AO21 = (i0&i1) | i2 : out s-a-1 == i2 s-a-1
# AOI21 = ~AO21 : out s-a-0 == i2 s-a-1
# OA21 = (i0|i1) & i2 : out s-a-0 == i2 s-a-0
# OAI21 = ~OA21 : out s-a-1 == i2 s-a-0
# AO211 = (i0&i1) | i2 | i3 : out s-a-1 == i2 s-a-1, i3 s-a-1
# AOI211= ~AO211 : out s-a-0 == i2 s-a-1, i3 s-a-1
# OA211 = (i0|i1) & i2 & i3 : out s-a-0 == i2 s-a-0, i3 s-a-0
# OAI211= ~OA211 : out s-a-1 == i2 s-a-0, i3 s-a-0
if driver.kind == 'AO21': return collapse((driver.ins[2], 1)) if polarity == 1 else {fault}
if driver.kind == 'AOI21': return collapse((driver.ins[2], 1)) if polarity == 0 else {fault}
if driver.kind == 'OA21': return collapse((driver.ins[2], 0)) if polarity == 0 else {fault}
if driver.kind == 'OAI21': return collapse((driver.ins[2], 0)) if polarity == 1 else {fault}
if driver.kind == 'AO211': return collapse((driver.ins[2], 1), (driver.ins[3], 1)) if polarity == 1 else {fault}
if driver.kind == 'AOI211': return collapse((driver.ins[2], 1), (driver.ins[3], 1)) if polarity == 0 else {fault}
if driver.kind == 'OA211': return collapse((driver.ins[2], 0), (driver.ins[3], 0)) if polarity == 0 else {fault}
if driver.kind == 'OAI211': return collapse((driver.ins[2], 0), (driver.ins[3], 0)) if polarity == 1 else {fault}
raise ValueError(f'Unknown node kind {driver.kind}. Circuit should have been resolved to techlib.KYUPY?')
remaining = self.saf_set.copy()
circuit_resolved = circuit.copy()
circuit_resolved.resolve_tlib_cells(tlib)
for n in circuit.reversed_topological_order(tlib):
for il in n.ins.without_nones():
for polarity in (0, 1):
repr = il.index*2+polarity
if repr in remaining:
equiv = collect_equivalent_faults(circuit_resolved, repr) & remaining
remaining.difference_update(equiv)
self.saf_equiv_classes[repr] = equiv
self.circuit = circuit
self.tlib = tlib
def fault_str(self, fault: int) -> str:
site = self.circuit.lines[fault//2]
polarity = '@1' if fault&1 else '@0'
if site.driver.kind == '__fork__':
return f'{site.reader.name}/{self.tlib.pin_name(site.reader.kind, site.reader_pin)}{polarity}'
else:
return f'{site.driver.name}/{self.tlib.pin_name(site.driver.kind, site.driver_pin, output=True)}{polarity}'

2488
tests/c6288.bench

File diff suppressed because it is too large Load Diff

119
tests/test_fault_set.py

@ -0,0 +1,119 @@
from kyupy import bench
from kyupy.techlib import KYUPY
from fsim.static import FaultSet
def test_trivial_inv():
c = bench.parse('input(i) output(o) o=INV(i)')
fs = FaultSet(c, KYUPY)
assert len(fs.saf_set) == 4
assert len(fs.saf_equiv_classes) == 2
oline = c.forks['o'].ins[0]
iline = oline.driver.ins[0]
assert fs.fault_str(oline.index*2) == 'o/o@0'
assert fs.fault_str(oline.index*2+1) == 'o/o@1'
assert fs.fault_str(iline.index*2) == 'o/i0@0'
assert fs.fault_str(iline.index*2+1) == 'o/i0@1'
assert oline.index*2 in fs.saf_equiv_classes
assert iline.index*2+1 in fs.saf_equiv_classes[oline.index*2]
assert oline.index*2+1 in fs.saf_equiv_classes
assert iline.index*2 in fs.saf_equiv_classes[oline.index*2+1]
def test_and():
c = bench.parse('input(i0, i1) output(o) o=AND(i0, i1)')
for n in c.nodes:
print(n)
fs = FaultSet(c, KYUPY)
assert len(fs.saf_set) == 6
assert len(fs.saf_equiv_classes) == 4
oline = c.forks['o'].ins[0]
i0line = oline.driver.ins[0]
i1line = oline.driver.ins[0]
assert oline.index*2 in fs.saf_equiv_classes
assert i0line.index*2 not in fs.saf_equiv_classes
assert i1line.index*2 not in fs.saf_equiv_classes
assert oline.index*2+1 in fs.saf_equiv_classes
assert len(fs.saf_equiv_classes[oline.index*2]) == 3
assert len(fs.saf_equiv_classes[oline.index*2+1]) == 1
def _class_sizes(src):
"""Returns (#equiv class of output s-a-0, #equiv class of output s-a-1)."""
c = bench.parse(src)
fs = FaultSet(c, KYUPY)
oline = c.forks['o'].ins[0]
return (len(fs.saf_equiv_classes.get(oline.index*2, set())),
len(fs.saf_equiv_classes.get(oline.index*2+1, set())))
def test_nand():
# out s-a-1 collapses with both inputs s-a-0; out s-a-0 has no equivalent.
assert _class_sizes('input(i0,i1) output(o) o=NAND2(i0,i1)') == (1, 3)
def test_or():
# out s-a-1 collapses with all inputs s-a-1; out s-a-0 has no equivalent.
assert _class_sizes('input(i0,i1) output(o) o=OR2(i0,i1)') == (1, 3)
assert _class_sizes('input(i0,i1,i2,i3) output(o) o=OR4(i0,i1,i2,i3)') == (1, 5)
def test_nor():
# out s-a-0 collapses with all inputs s-a-1; out s-a-1 has no equivalent.
assert _class_sizes('input(i0,i1) output(o) o=NOR2(i0,i1)') == (3, 1)
def test_aoi_oai_complex():
# Only the "single" term of the and-or/or-and cells forces the output.
assert _class_sizes('input(i0,i1,i2) output(o) o=AO21(i0,i1,i2)') == (1, 2)
assert _class_sizes('input(i0,i1,i2) output(o) o=AOI21(i0,i1,i2)') == (2, 1)
assert _class_sizes('input(i0,i1,i2) output(o) o=OA21(i0,i1,i2)') == (2, 1)
assert _class_sizes('input(i0,i1,i2) output(o) o=OAI21(i0,i1,i2)') == (1, 2)
assert _class_sizes('input(i0,i1,i2,i3) output(o) o=AO211(i0,i1,i2,i3)') == (1, 3)
assert _class_sizes('input(i0,i1,i2,i3) output(o) o=OAI211(i0,i1,i2,i3)') == (1, 3)
# The two-term variants do not collapse onto any single input.
assert _class_sizes('input(i0,i1,i2,i3) output(o) o=AO22(i0,i1,i2,i3)') == (1, 1)
def test_s27():
# bench parser does not add any clock or set/reset logic.
c = bench.parse('''
# 1 outputs
# 3 D-type flipflops
# 2 inverters
# 8 gates (1 ANDs + 1 NANDs + 2 ORs + 4 NORs)
INPUT(G0)
INPUT(G1)
INPUT(G2)
INPUT(G3)
OUTPUT(G17)
G5 = DFF(G10)
G6 = DFF(G11)
G7 = DFF(G13)
G14 = NOT(G0)
G17 = NOT(G11)
G8 = AND(G14, G6)
G15 = OR(G12, G8)
G16 = OR(G3, G8)
G9 = NAND(G16, G15)
G10 = NOR(G14, G11)
G11 = NOR(G5, G9)
G12 = NOR(G1, G7)
G13 = NOR(G2, G12)
''')
fs = FaultSet(c, KYUPY)
assert len(fs.saf_set) == 52
assert len(fs.saf_equiv_classes) == 32
g11_line = c.cells['G11'].outs[0]
g11_sa_0 = g11_line.index*2
assert g11_sa_0 in fs.saf_equiv_classes
assert len(fs.saf_equiv_classes[g11_sa_0]) == 5 # collapse via G9
g15_line = c.cells['G15'].outs[0]
g15_sa_0 = g15_line.index*2
assert g15_sa_0 in fs.saf_equiv_classes[g11_sa_0]

62
tests/test_line_roles.py

@ -0,0 +1,62 @@
from kyupy import bench
from kyupy.techlib import KYUPY
from fsim.static import LineRoles
def test_trivial_inv():
c = bench.parse('input(i) output(o) o=INV(i)')
lr = LineRoles(c, KYUPY)
assert len(lr.line2roles) == 2
assert len(lr.roles2lines[LineRoles.LOGIC_OUT]) == 2
def test_input_fanout():
c = bench.parse('input(i) output(o1) output(o2) o1=INV(i) o2=INV(i)')
lr = LineRoles(c, KYUPY)
assert len(lr.line2roles) == 4
assert len(lr.roles2lines[LineRoles.LOGIC_OUT]) == 4
def test_fanout():
c = bench.parse('input(i) output(o1) output(o2) ii=INV(i) o1=INV(ii) o2=INV(ii)')
lr = LineRoles(c, KYUPY)
assert len(lr.line2roles) == 6
assert len(lr.roles2lines[LineRoles.LOGIC_OUT]) == 6
def test_s27():
# bench parser does not add any clock or set/reset logic.
c = bench.parse('''
# 1 outputs
# 3 D-type flipflops
# 2 inverters
# 8 gates (1 ANDs + 1 NANDs + 2 ORs + 4 NORs)
INPUT(G0)
INPUT(G1)
INPUT(G2)
INPUT(G3)
OUTPUT(G17)
G5 = DFF(G10)
G6 = DFF(G11)
G7 = DFF(G13)
G14 = NOT(G0)
G17 = NOT(G11)
G8 = AND(G14, G6)
G15 = OR(G12, G8)
G16 = OR(G3, G8)
G9 = NAND(G16, G15)
G10 = NOR(G14, G11)
G11 = NOR(G5, G9)
G12 = NOR(G1, G7)
G13 = NOR(G2, G12)
''')
lr = LineRoles(c, KYUPY)
assert len(lr.line2roles) == 34 # total number of signal lines in circuit
# 2 + 9 + 23 = 34
assert len(lr.roles2lines[LineRoles.LOGIC_OUT]) == 2
assert len(lr.roles2lines[LineRoles.LOGIC_SEQ]) == 9
assert len(lr.roles2lines[LineRoles.LOGIC_OUT|LineRoles.LOGIC_SEQ]) == 23

1950
uv.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save