basic project setup, tools and fsim
This commit is contained in:
commit
a74881de49
15 files changed
+5340
No files matched your search
+170
@@ -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 @@
|
||||
3.13
|
||||
@@ -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.
|
||||
|
||||
|
||||
Generated
+153
@@ -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
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
Submodule
+1
Submodule kyupy added at 449bf9a031.
@@ -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 }
|
||||
|
||||
Whitespace-only changes.
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
Reference in new issue
Block a user