commit
a74881de49
15 changed files with 5340 additions and 0 deletions
@ -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* |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
[submodule "kyupy"] |
||||||
|
path = kyupy |
||||||
|
url = git@git.vlab.cse.kyutech.ac.jp:stefan/kyupy.git |
||||||
|
branch = devel |
||||||
@ -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. |
||||||
|
|
||||||
|
|
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
]; |
||||||
|
}; |
||||||
|
} |
||||||
|
); |
||||||
|
}; |
||||||
|
} |
||||||
@ -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() |
||||||
@ -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,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}' |
||||||
@ -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] |
||||||
|
|
||||||
@ -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 |
||||||
Loading…
Reference in new issue