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