Browse Source

for release 0.0.4

- Circuit: is now pickleable and comparable
- Circuit: utilities for locating/indexing io-ports
- Verilog: parser fixes, support yosys-style verilog
- SDF: parser fixes, full XOR support
- STIL: parser fixes
- Simulators: faster, up to 4-input cells, pickleable
- WaveSim: WSA calculation support
- WaveSim: Per-simulation parameters and delays
- Logic: Data are now raw numpy arrays
- Logic: More tools for bit-packing
- Added DEF parser
- Better techlib support for NanGate, SAED, GSC180
- Tests and docs improvements
main
Stefan Holst 1 year ago
parent
commit
351d809306
  1. 12
      .gitignore
  2. 1348
      Demo.ipynb
  3. 2
      LICENSE.txt
  4. 2
      MANIFEST.in
  5. 14
      README.rst
  6. 2
      docs/Makefile
  7. 13
      docs/circuit.rst
  8. 4
      docs/conf.py
  9. 29
      docs/datastructures.rst
  10. 6
      docs/index.rst
  11. 7
      docs/logic.rst
  12. 4
      docs/miscellaneous.rst
  13. 9
      docs/parsers.rst
  14. 5
      docs/simulators.rst
  15. 7
      docs/techlib.rst
  16. 3502
      examples/Introduction.ipynb
  17. 5
      setup.py
  18. 101
      src/kyupy/__init__.py
  19. 8
      src/kyupy/bench.py
  20. 332
      src/kyupy/circuit.py
  21. 297
      src/kyupy/def_file.py
  22. 682
      src/kyupy/logic.py
  23. 475
      src/kyupy/logic_sim.py
  24. 227
      src/kyupy/sdf.py
  25. 333
      src/kyupy/sim.py
  26. 103
      src/kyupy/stil.py
  27. 656
      src/kyupy/techlib.py
  28. 186
      src/kyupy/verilog.py
  29. 1136
      src/kyupy/wave_sim.py
  30. BIN
      tests/b14.sdf.gz
  31. BIN
      tests/b14.stuck.stil.gz
  32. BIN
      tests/b14.transition.stil.gz
  33. BIN
      tests/b14.v.gz
  34. BIN
      tests/b15_2ig.sa_nf.stil.gz
  35. BIN
      tests/b15_2ig.sdf.gz
  36. BIN
      tests/b15_2ig.tf_nf.stil.gz
  37. BIN
      tests/b15_2ig.v.gz
  38. BIN
      tests/b15_4ig.sdf.gz
  39. BIN
      tests/b15_4ig.v.gz
  40. 14
      tests/conftest.py
  41. 335
      tests/rng_haltonBase2.synth_yosys.v
  42. 2
      tests/test_bench.py
  43. 33
      tests/test_circuit.py
  44. 313
      tests/test_logic.py
  45. 238
      tests/test_logic_sim.py
  46. 230
      tests/test_sdf.py
  47. 20
      tests/test_stil.py
  48. 45
      tests/test_verilog.py
  49. 300
      tests/test_wave_sim.py

12
.gitignore vendored

@ -1,10 +1,12 @@
**/__pycache__ __pycache__
**/.ipynb_checkpoints .ipynb_checkpoints
**/.pytest_cache .pytest_cache
**/.DS_Store .DS_Store
**/*.pyc *.pyc
docs/_build docs/_build
build build
dist dist
.idea .idea
.vscode
src/kyupy.egg-info src/kyupy.egg-info
*nogit*

1348
Demo.ipynb

File diff suppressed because it is too large Load Diff

2
LICENSE.txt

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020-2022 Stefan Holst Copyright (c) 2020-2023 Stefan Holst
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

2
MANIFEST.in

@ -1,5 +1,5 @@
include *.ipynb
include *.txt include *.txt
recursive-include examples *.ipynb
recursive-include tests *.bench recursive-include tests *.bench
recursive-include tests *.gz recursive-include tests *.gz
recursive-include tests *.py recursive-include tests *.py

14
README.rst

@ -6,7 +6,7 @@ It contains fundamental building blocks for research software in the fields of V
* Efficient data structures for gate-level circuits and related design data. * Efficient data structures for gate-level circuits and related design data.
* Partial `lark <https://github.com/lark-parser/lark>`_ parsers for common design files like * Partial `lark <https://github.com/lark-parser/lark>`_ parsers for common design files like
bench, gate-level verilog, standard delay format (SDF), standard test interface language (STIL). bench, gate-level Verilog, standard delay format (SDF), standard test interface language (STIL), design exchange format (DEF).
* Bit-parallel gate-level 2-, 4-, and 8-valued logic simulation. * Bit-parallel gate-level 2-, 4-, and 8-valued logic simulation.
* GPU-accelerated high-throughput gate-level timing simulation. * GPU-accelerated high-throughput gate-level timing simulation.
* High-performance through the use of `numpy <https://numpy.org>`_ and `numba <https://numba.pydata.org>`_. * High-performance through the use of `numpy <https://numpy.org>`_ and `numba <https://numba.pydata.org>`_.
@ -16,13 +16,17 @@ Getting Started
--------------- ---------------
KyuPy is available in `PyPI <https://pypi.org/project/kyupy>`_. KyuPy is available in `PyPI <https://pypi.org/project/kyupy>`_.
It requires Python 3.6 or newer, `lark-parser <https://pypi.org/project/lark-parser>`_, and `numpy`_. It requires Python 3.8 or newer, `lark-parser <https://pypi.org/project/lark-parser>`_, and `numpy`_.
Although optional, `numba`_ should be installed for best performance. Although optional, `numba`_ should be installed for best performance.
GPU/CUDA support in numba may `require some additional setup <https://numba.pydata.org/numba-doc/latest/cuda/index.html>`_. GPU/CUDA support in numba may `require some additional setup <https://numba.readthedocs.io/en/stable/cuda/index.html>`_.
If numba is not available, KyuPy will automatically fall back to slow, pure Python execution. If numba is not available, KyuPy will automatically fall back to slow, pure Python execution.
The Jupyter Notebook `Demo.ipynb <https://github.com/s-holst/kyupy/blob/main/Demo.ipynb>`_ contains some useful examples to get familiar with the API. The Jupyter Notebook `Introduction.ipynb <https://github.com/s-holst/kyupy/blob/main/examples/Introduction.ipynb>`_ contains some useful examples to get familiar with the API.
Development
-----------
To work with the latest pre-release source code, clone the `KyuPy GitHub repository <https://github.com/s-holst/kyupy>`_. To work with the latest pre-release source code, clone the `KyuPy GitHub repository <https://github.com/s-holst/kyupy>`_.
Run ``pip3 install --user -e .`` within your local checkout to make the package available in your Python environment. Run ``pip install -e .`` within your local checkout to make the package available in your Python environment.
The source code comes with tests that can be run with ``pytest``. The source code comes with tests that can be run with ``pytest``.

2
docs/Makefile

@ -1,3 +1,5 @@
# pip install sphinx sphinx-rtd-theme
#
# Minimal makefile for Sphinx documentation # Minimal makefile for Sphinx documentation
# #

13
docs/circuit.rst

@ -0,0 +1,13 @@
Circuit Graph - :mod:`kyupy.circuit`
====================================
.. automodule:: kyupy.circuit
.. autoclass:: kyupy.circuit.Node
:members:
.. autoclass:: kyupy.circuit.Line
:members:
.. autoclass:: kyupy.circuit.Circuit
:members:

4
docs/conf.py

@ -20,11 +20,11 @@ sys.path.insert(0, os.path.abspath('../src'))
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = 'KyuPy' project = 'KyuPy'
copyright = '2020-2021, Stefan Holst' copyright = '2020-2023, Stefan Holst'
author = 'Stefan Holst' author = 'Stefan Holst'
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = '0.0.3' release = '0.0.4'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------

29
docs/datastructures.rst

@ -1,29 +0,0 @@
Data Structures
===============
KyuPy provides two types of core data structures, one for gate-level circuits, and a few others for representing and storing logic data and signal values.
The data structures are designed to work together nicely with numpy arrays.
For example, all the nodes and connections in the circuit graph have consecutive integer indices that can be used to access ndarrays with associated data.
Circuit graphs also define an ordering of inputs, outputs and other nodes to easily process test vector data and alike.
Circuit Graph - :mod:`kyupy.circuit`
------------------------------------
.. automodule:: kyupy.circuit
.. autoclass:: kyupy.circuit.Node
:members:
.. autoclass:: kyupy.circuit.Line
:members:
.. autoclass:: kyupy.circuit.Circuit
:members:
Multi-Valued Logic - :mod:`kyupy.logic`
---------------------------------------
.. automodule:: kyupy.logic
:members:

6
docs/index.rst

@ -4,9 +4,11 @@ API Reference
------------- -------------
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 1
datastructures circuit
logic
techlib
parsers parsers
simulators simulators
miscellaneous miscellaneous

7
docs/logic.rst

@ -0,0 +1,7 @@
Multi-Valued Logic - :mod:`kyupy.logic`
=======================================
.. automodule:: kyupy.logic
:members:

4
docs/miscellaneous.rst

@ -4,7 +4,3 @@ Miscellaneous
.. automodule:: kyupy .. automodule:: kyupy
:members: :members:
.. automodule:: kyupy.techlib
:members:

9
docs/parsers.rst

@ -40,3 +40,12 @@ Standard Delay Format - :mod:`kyupy.sdf`
.. autoclass:: kyupy.sdf.DelayFile .. autoclass:: kyupy.sdf.DelayFile
:members: :members:
Design Exchange Format - :mod:`kyupy.def_file`
----------------------------------------------
.. automodule:: kyupy.def_file
:members: parse, load
.. autoclass:: kyupy.def_file.DefFile
:members:

5
docs/simulators.rst

@ -1,6 +1,11 @@
Simulators Simulators
========== ==========
KyuPy's simulators are optimized for cells with at most 4 inputs and 1 output.
More complex cells must be mapped to simulation primitives first.
Logic Simulation - :mod:`kyupy.logic_sim` Logic Simulation - :mod:`kyupy.logic_sim`
----------------------------------------- -----------------------------------------

7
docs/techlib.rst

@ -0,0 +1,7 @@
Technology Libraries
====================
.. automodule:: kyupy.techlib
:members:

3502
examples/Introduction.ipynb

File diff suppressed because it is too large Load Diff

5
setup.py

@ -14,7 +14,7 @@ setup(
url='https://github.com/s-holst/kyupy', url='https://github.com/s-holst/kyupy',
author='Stefan Holst', author='Stefan Holst',
author_email='mail@s-holst.de', author_email='mail@s-holst.de',
python_requires='>=3.6', python_requires='>=3.8',
install_requires=[ install_requires=[
'numpy>=1.17.0', 'numpy>=1.17.0',
'lark-parser>=0.8.0' 'lark-parser>=0.8.0'
@ -33,9 +33,8 @@ setup(
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
], ],
) )

101
src/kyupy/__init__.py

@ -1,11 +1,12 @@
"""A package for processing and analysis of non-hierarchical gate-level VLSI designs. """The kyupy package itself contains a miscellaneous utility functions.
The kyupy package itself contains a logger and other simple utility functions.
In addition, it defines a ``numba`` and a ``cuda`` objects that point to the actual packages In addition, it defines a ``numba`` and a ``cuda`` objects that point to the actual packages
if they are available and otherwise point to mocks. if they are available and otherwise point to mocks.
""" """
import time import time
import sys
from collections import defaultdict
import importlib.util import importlib.util
import gzip import gzip
@ -15,15 +16,19 @@ import numpy as np
_pop_count_lut = np.asarray([bin(x).count('1') for x in range(256)]) _pop_count_lut = np.asarray([bin(x).count('1') for x in range(256)])
def cdiv(x, y):
return -(x // -y)
def popcount(a): def popcount(a):
"""Returns the number of 1-bits in a given packed numpy array.""" """Returns the number of 1-bits in a given packed numpy array of type ``uint8``."""
return np.sum(_pop_count_lut[a]) return np.sum(_pop_count_lut[a])
def readtext(file): def readtext(file):
"""Reads and returns the text in a given file. Transparently decompresses \\*.gz files.""" """Reads and returns the text in a given file. Transparently decompresses \\*.gz files."""
if hasattr(file, 'read'): if hasattr(file, 'read'):
return file.read() return file.read().decode()
if str(file).endswith('.gz'): if str(file).endswith('.gz'):
with gzip.open(file, 'rt') as f: with gzip.open(file, 'rt') as f:
return f.read() return f.read()
@ -74,6 +79,39 @@ def hr_time(seconds):
return s return s
def batchrange(nitems, maxsize):
"""A simple generator that produces offsets and sizes for batch-loops."""
for offset in range(0, nitems, maxsize):
yield offset, min(nitems-offset, maxsize)
class Timer:
def __init__(self, s=0): self.s = s
def __enter__(self): self.start_time = time.perf_counter(); return self
def __exit__(self, *args): self.s += time.perf_counter() - self.start_time
@property
def ms(self): return self.s*1e3
@property
def us(self): return self.s*1e6
def __repr__(self): return f'{self.s:.3f}'
def __add__(self, t):
return Timer(self.s + t.s)
class Timers:
def __init__(self, t={}): self.timers = defaultdict(Timer) | t
def __getitem__(self, name): return self.timers[name]
def __repr__(self): return '{' + ', '.join([f'{k}: {v}' for k, v in self.timers.items()]) + '}'
def __add__(self, t):
tmr = Timers(self.timers)
for k, v in t.timers.items(): tmr.timers[k] += v
return tmr
def sum(self):
return sum([v.s for v in self.timers.values()])
def dict(self):
return dict([(k, v.s) for k, v in self.timers.items()])
class Log: class Log:
"""A very simple logger that formats the messages with the number of seconds since """A very simple logger that formats the messages with the number of seconds since
program start. program start.
@ -81,25 +119,58 @@ class Log:
def __init__(self): def __init__(self):
self.start = time.perf_counter() self.start = time.perf_counter()
self.logfile = None self.logfile = sys.stdout
"""When set to a file handle, log messages are written to it instead to standard output. """When set to a file handle, log messages are written to it instead to standard output.
After each write, ``flush()`` is called as well.
""" """
self.indent = 0
self._limit = -1
self.filtered = 0
def limit(self, log_limit):
class Limiter:
def __init__(self, l): self.l = l
def __enter__(self): self.l.start_limit(log_limit); return self
def __exit__(self, *args): self.l.stop_limit()
return Limiter(self)
def start_limit(self, limit):
self.filtered = 0
self._limit = limit
def stop_limit(self):
if self.filtered > 0:
log.info(f'{self.filtered} more messages (filtered).')
self.filtered = 0
self._limit = -1
def __getstate__(self): def __getstate__(self):
return {'elapsed': time.perf_counter() - self.start} return {'elapsed': time.perf_counter() - self.start}
def __setstate__(self, state): def __setstate__(self, state):
self.logfile = None self.logfile = sys.stdout
self.indent = 0
self.start = time.perf_counter() - state['elapsed'] self.start = time.perf_counter() - state['elapsed']
def write(self, s, indent=0):
self.logfile.write(' '*indent + s + '\n')
self.logfile.flush()
def li(self, item): self.write('- ' + str(item).replace('\n', '\n'+' '*(self.indent+1)), self.indent)
def lib(self): self.write('-', self.indent); self.indent += 1
def lin(self): self.write('-', self.indent-1)
def di(self, key, value): self.write(str(key) + ': ' + str(value).replace('\n', '\n'+' '*(self.indent+1)), self.indent)
def dib(self, key): self.write(str(key) + ':', self.indent); self.indent += 1
def din(self, key): self.write(str(key) + ':', self.indent-1)
def ie(self, n=1): self.indent -= n
def log(self, level, message): def log(self, level, message):
if self._limit == 0:
self.filtered += 1
return
t = time.perf_counter() - self.start t = time.perf_counter() - self.start
if self.logfile is None: self.logfile.write(f'# {t:011.3f} {level} {message}\n')
print(f'{t:011.3f} {level} {message}') self.logfile.flush()
else: self._limit -= 1
self.logfile.write(f'{t:011.3f} {level} {message}\n')
self.logfile.flush()
def info(self, message): def info(self, message):
"""Log an informational message.""" """Log an informational message."""
@ -156,7 +227,7 @@ class MockCuda:
self.x = 0 self.x = 0
self.y = 0 self.y = 0
def jit(self, device=False): def jit(self, func=None, device=False):
_ = device # silence "not used" warning _ = device # silence "not used" warning
outer = self outer = self
@ -184,7 +255,7 @@ class MockCuda:
return inner return inner
return Launcher(func) return Launcher(func)
return make_launcher return make_launcher(func) if func else make_launcher
@staticmethod @staticmethod
def to_device(array, to=None): def to_device(array, to=None):
@ -208,6 +279,8 @@ if importlib.util.find_spec('numba') is not None:
try: try:
list(numba.cuda.gpus) list(numba.cuda.gpus)
from numba import cuda from numba import cuda
from numba.core import config
config.CUDA_LOW_OCCUPANCY_WARNINGS = False
except CudaSupportError: except CudaSupportError:
log.warn('Cuda unavailable. Falling back to pure Python.') log.warn('Cuda unavailable. Falling back to pure Python.')
cuda = MockCuda() cuda = MockCuda()

8
src/kyupy/bench.py

@ -21,9 +21,9 @@ class BenchTransformer(Transformer):
def start(self, _): return self.c def start(self, _): return self.c
def parameters(self, args): return [self.c.get_or_add_fork(name) for name in args] def parameters(self, args): return [self.c.get_or_add_fork(str(name)) for name in args]
def interface(self, args): self.c.interface.extend(args[0]) def interface(self, args): self.c.io_nodes.extend(args[0])
def assignment(self, args): def assignment(self, args):
name, cell_type, drivers = args name, cell_type, drivers = args
@ -57,8 +57,8 @@ def parse(text, name=None):
def load(file, name=None): def load(file, name=None):
"""Parses the contents of ``file`` as ISCAS89 bench code. """Parses the contents of ``file`` as ISCAS89 bench code.
:param file: The file to be loaded. :param file: The file to be loaded. Files with `.gz`-suffix are decompressed on-the-fly.
:param name: The name of the circuit. If none given, the file name is used as circuit name. :param name: The name of the circuit. If None, the file name is used as circuit name.
:return: A :class:`Circuit` object. :return: A :class:`Circuit` object.
""" """
return parse(readtext(file), name=name or str(file)) return parse(readtext(file), name=name or str(file))

332
src/kyupy/circuit.py

@ -1,11 +1,19 @@
"""Data structures for representing non-hierarchical gate-level circuits. """Core module for handling non-hierarchical gate-level circuits.
The class :class:`Circuit` is a container of nodes connected by lines. The class :class:`Circuit` is a container of nodes connected by lines.
A node is an instance of class :class:`Node`, A node is an instance of class :class:`Node`,
and a line is an instance of class :class:`Line`. and a line is an instance of class :class:`Line`.
The data structures are designed to work together nicely with numpy arrays.
For example, all the nodes and connections in the circuit graph have consecutive integer indices that can be used to access ndarrays with associated data.
Circuit graphs also define an ordering of inputs, outputs and other nodes to easily process test vector data and alike.
""" """
from collections import deque from collections import deque, defaultdict
import re
import numpy as np
class GrowingList(list): class GrowingList(list):
@ -64,9 +72,9 @@ class Node:
self.index = len(circuit.nodes) - 1 self.index = len(circuit.nodes) - 1
"""A unique and consecutive integer index of the node within the circuit. """A unique and consecutive integer index of the node within the circuit.
It can be used to store additional data about the node :code:`n` It can be used to associate additional data to a node :code:`n`
by allocating an array or list :code:`my_data` of length :code:`len(n.circuit.nodes)` and by allocating an array or list :code:`my_data` of length :code:`len(n.circuit.nodes)` and
accessing it by :code:`my_data[n.index]`. accessing it by :code:`my_data[n.index]` or simply by :code:`my_data[n]`.
""" """
self.ins = GrowingList() self.ins = GrowingList()
"""A list of input connections (:class:`Line` objects). """A list of input connections (:class:`Line` objects).
@ -81,7 +89,9 @@ class Node:
def __repr__(self): def __repr__(self):
ins = ' '.join([f'<{line.index}' if line is not None else '<None' for line in self.ins]) ins = ' '.join([f'<{line.index}' if line is not None else '<None' for line in self.ins])
outs = ' '.join([f'>{line.index}' if line is not None else '>None' for line in self.outs]) outs = ' '.join([f'>{line.index}' if line is not None else '>None' for line in self.outs])
return f'{self.index}:{self.kind}"{self.name}" {ins} {outs}' ins = ' ' + ins if len(ins) else ''
outs = ' ' + outs if len(outs) else ''
return f'{self.index}:{self.kind}"{self.name}"{ins}{outs}'
def remove(self): def remove(self):
"""Removes the node from its circuit. """Removes the node from its circuit.
@ -135,7 +145,7 @@ class Line:
It can be used to store additional data about the line :code:`l` It can be used to store additional data about the line :code:`l`
by allocating an array or list :code:`my_data` of length :code:`len(l.circuit.lines)` and by allocating an array or list :code:`my_data` of length :code:`len(l.circuit.lines)` and
accessing it by :code:`my_data[l.index]`. accessing it by :code:`my_data[l.index]` or simply by :code:`my_data[l]`.
""" """
if not isinstance(driver, tuple): driver = (driver, driver.outs.free_index()) if not isinstance(driver, tuple): driver = (driver, driver.outs.free_index())
self.driver = driver[0] self.driver = driver[0]
@ -144,7 +154,7 @@ class Line:
self.driver_pin = driver[1] self.driver_pin = driver[1]
"""The output pin position of the driver node this line is connected to. """The output pin position of the driver node this line is connected to.
This is the position in the outs-list of the driving node this line referenced from: This is the position in the list :py:attr:`Node.outs` of the driving node this line referenced from:
:code:`self.driver.outs[self.driver_pin] == self`. :code:`self.driver.outs[self.driver_pin] == self`.
""" """
if not isinstance(reader, tuple): reader = (reader, reader.ins.free_index()) if not isinstance(reader, tuple): reader = (reader, reader.ins.free_index())
@ -154,7 +164,7 @@ class Line:
self.reader_pin = reader[1] self.reader_pin = reader[1]
"""The input pin position of the reader node this line is connected to. """The input pin position of the reader node this line is connected to.
This is the position in the ins-list of the reader node this line referenced from: This is the position in the list :py:attr:`Node.ins` of the reader node this line referenced from:
:code:`self.reader.ins[self.reader_pin] == self`. :code:`self.reader.ins[self.reader_pin] == self`.
""" """
self.driver.outs[self.driver_pin] = self self.driver.outs[self.driver_pin] = self
@ -166,7 +176,11 @@ class Line:
To keep the indices consecutive, the line with the highest index within the circuit To keep the indices consecutive, the line with the highest index within the circuit
will be assigned the index of the removed line. will be assigned the index of the removed line.
""" """
if self.driver is not None: self.driver.outs[self.driver_pin] = None if self.driver is not None:
self.driver.outs[self.driver_pin] = None
if self.driver.kind == '__fork__': # squeeze outputs
del self.driver.outs[self.driver_pin]
for i, l in enumerate(self.driver.outs): l.driver_pin = i
if self.reader is not None: self.reader.ins[self.reader_pin] = None if self.reader is not None: self.reader.ins[self.reader_pin] = None
if self.circuit is not None: del self.circuit.lines[self.index] if self.circuit is not None: del self.circuit.lines[self.index]
self.driver = None self.driver = None
@ -202,41 +216,237 @@ class Circuit:
to enforce consecutiveness. to enforce consecutiveness.
A subset of nodes can be designated as primary input- or output-ports of the circuit. A subset of nodes can be designated as primary input- or output-ports of the circuit.
This is done by adding them to the :py:attr:`interface` list. This is done by adding them to the :py:attr:`io_nodes` list.
""" """
def __init__(self, name=None): def __init__(self, name=None):
self.name = name self.name = name
"""The name of the circuit. """The name of the circuit.
""" """
self.nodes = IndexList() self.nodes : list[Node] = IndexList()
"""A list of all :class:`Node` objects contained in the circuit. """A list of all :class:`Node` objects contained in the circuit.
The position of a node in this list equals its index :code:`self.nodes[42].index == 42`. The position of a node in this list equals its index :code:`self.nodes[42].index == 42`.
This list must not be changed directly.
Use the :class:`Node` constructor and :py:attr:`Node.remove()` to add and remove nodes.
""" """
self.lines = IndexList() self.lines : list[Line] = IndexList()
"""A list of all :class:`Line` objects contained in the circuit. """A list of all :class:`Line` objects contained in the circuit.
The position of a line in this list equals its index :code:`self.lines[42].index == 42`. The position of a line in this list equals its index :code:`self.lines[42].index == 42`.
This list must not be changed directly.
Use the :class:`Line` constructor and :py:attr:`Line.remove()` to add and remove lines.
""" """
self.interface = GrowingList() self.io_nodes : list[Node] = GrowingList()
"""A list of nodes that are designated as primary input- or output-ports. """A list of nodes that are designated as primary input- or output-ports.
Port-nodes are contained in :py:attr:`nodes` as well as :py:attr:`interface`. Port-nodes are contained in :py:attr:`nodes` as well as :py:attr:`io_nodes`.
The position of a node in the interface list corresponds to positions of logic values in test vectors. The position of a node in the io_nodes list corresponds to positions of logic values in test vectors.
The port direction is not stored explicitly. The port direction is not stored explicitly.
Usually, nodes in the interface list without any lines in their :py:attr:`Node.ins` list are primary inputs, Usually, nodes in the io_nodes list without any lines in their :py:attr:`Node.ins` list are primary inputs,
and nodes without any lines in their :py:attr:`Node.outs` list are regarded as primary outputs. and all other nodes in the io_nodes list are regarded as primary outputs.
""" """
self.cells = {} self.cells : dict[str, Node] = {}
"""A dictionary to access cells by name. """A dictionary to access cells by name.
This dictionary must not be changed directly.
Use the :class:`Node` constructor and :py:attr:`Node.remove()` to add and remove nodes.
""" """
self.forks = {} self.forks : dict[str, Node] = {}
"""A dictionary to access forks by name. """A dictionary to access forks by name.
This dictionary must not be changed directly.
Use the :class:`Node` constructor and :py:attr:`Node.remove()` to add and remove nodes.
""" """
@property
def s_nodes(self):
"""A list of all primary I/Os as well as all flip-flops and latches in the circuit (in that order).
The s_nodes list defines the order of all ports and all sequential elements in the circuit.
This list is constructed on-the-fly. If used in some inner toop, consider caching the list for better performance.
"""
return list(self.io_nodes) + [n for n in self.nodes if 'dff' in n.kind.lower()] + [n for n in self.nodes if 'latch' in n.kind.lower()]
def io_locs(self, prefix):
"""Returns the indices of primary I/Os that start with given name prefix.
The returned values are used to index into the :py:attr:`io_nodes` array.
If only one I/O cell matches the given prefix, a single integer is returned.
If a bus matches the given prefix, a sorted list of indices is returned.
Busses are identified by integers in the cell names following the given prefix.
Lists for bus indices are sorted from LSB (e.g. :code:`data[0]`) to MSB (e.g. :code:`data[31]`).
If a prefix matches multiple different signals or busses, alphanumerically sorted
lists of lists are returned. Therefore, higher-dimensional busses
(e.g. :code:`data0[0], data0[1], ...`, :code:`data1[0], data1[1], ...`) are supported as well.
"""
return self._locs(prefix, list(self.io_nodes))
def s_locs(self, prefix):
"""Returns the indices of I/Os and sequential elements that start with given name prefix.
The returned values are used to index into the :py:attr:`s_nodes` list.
It works the same as :py:attr:`io_locs`. See there for more details.
"""
return self._locs(prefix, self.s_nodes)
def _locs(self, prefix, nodes):
d_top = dict()
for i, n in enumerate(nodes):
if m := re.match(fr'({prefix}.*?)((?:[\d_\[\]])*$)', n.name):
path = [m[1]] + [int(v) for v in re.split(r'[_\[\]]+', m[2]) if len(v) > 0]
d = d_top
for j in path[:-1]:
d[j] = d.get(j, dict())
d = d[j]
d[path[-1]] = i
# sort recursively for multi-dimensional lists.
def sorted_values(d): return [sorted_values(v) for k, v in sorted(d.items())] if isinstance(d, dict) else d
l = sorted_values(d_top)
while isinstance(l, list) and len(l) == 1: l = l[0]
return None if isinstance(l, list) and len(l) == 0 else l
@property
def stats(self):
"""A dictionary with the counts of all different elements in the circuit.
The dictionary contains the number of all different kinds of nodes, the number
of lines, as well various sums like number of combinational gates, number of
primary I/Os, number of sequential elements, and so on.
The count of regular cells use their :py:attr:`Node.kind` as key, other statistics use
dunder-keys like: `__comb__`, `__io__`, `__seq__`, and so on.
"""
stats = defaultdict(int)
stats['__node__'] = len(self.nodes)
stats['__cell__'] = len(self.cells)
stats['__fork__'] = len(self.forks)
stats['__io__'] = len(self.io_nodes)
stats['__line__'] = len(self.lines)
for n in self.cells.values():
stats[n.kind] += 1
if 'dff' in n.kind.lower(): stats['__dff__'] += 1
elif 'latch' in n.kind.lower(): stats['__latch__'] += 1
elif 'put' not in n.kind.lower(): stats['__comb__'] += 1 # no input or output
stats['__seq__'] = stats['__dff__'] + stats['__latch__']
return dict(stats)
def get_or_add_fork(self, name): def get_or_add_fork(self, name):
return self.forks[name] if name in self.forks else Node(self, name) return self.forks[name] if name in self.forks else Node(self, name)
def remove_dangling_nodes(self, root_node:Node):
if len([l for l in root_node.outs if l is not None]) > 0: return
lines = [l for l in root_node.ins if l is not None]
drivers = [l.driver for l in lines]
root_node.remove()
for l in lines:
l.remove()
for d in drivers:
self.remove_dangling_nodes(d)
def eliminate_1to1_forks(self):
"""Removes all forks that drive only one node.
Such forks are inserted by parsers to annotate signal names. If this
information is not needed, such forks can be removed and the two neighbors
can be connected directly using one line. Forks that drive more than one node
are not removed by this function.
This function may remove some nodes and some lines from the circuit.
Therefore that indices of other nodes and lines may change to keep the indices consecutive.
It may therefore invalidate external data for nodes and lines.
"""
ios = set(self.io_nodes)
for n in list(self.forks.values()):
if n in ios: continue
if len(n.outs) != 1: continue
in_line = n.ins[0]
out_line = n.outs[0]
out_reader = out_line.reader
out_reader_pin = out_line.reader_pin
n.remove()
out_line.remove()
in_line.reader = out_reader
in_line.reader_pin = out_reader_pin
in_line.reader.ins[in_line.reader_pin] = in_line
def substitute(self, node, impl):
"""Replaces a given node with the given implementation circuit.
The given node will be removed, the implementation is copied in and
the signal lines are connected appropriately. The number and arrangement
of the input and output ports must match the pins of the replaced node.
This function tries to preserve node and line indices as much as possible.
Usually, it only adds additional nodes and lines, preserving the order of
all existing nodes and lines. If an implementation is empty, however, nodes
and lines may get removed, changing indices and invalidating external data.
"""
ios = set(impl.io_nodes)
impl_in_nodes = [n for n in impl.io_nodes if len(n.ins) == 0]
impl_out_lines = [n.ins[0] for n in impl.io_nodes if len(n.ins) > 0]
designated_cell = None
if len(impl_out_lines) > 0:
n = impl_out_lines[0].driver
while n.kind == '__fork__' and n not in ios:
n = n.ins[0].driver
designated_cell = n
node_in_lines = list(node.ins) + [None] * (len(impl_in_nodes)-len(node.ins))
node_out_lines = list(node.outs) + [None] * (len(impl_out_lines)-len(node.outs))
assert len(node_in_lines) == len(impl_in_nodes)
assert len(node_out_lines) == len(impl_out_lines)
node_map = dict()
if designated_cell is not None:
node.kind = designated_cell.kind
node_map[designated_cell] = node
node.ins = GrowingList()
node.outs = GrowingList()
else:
node.remove()
ios = set(impl.io_nodes)
for n in impl.nodes: # add all nodes to main circuit
if n not in ios:
if n != designated_cell:
node_map[n] = Node(self, f'{node.name}~{n.name}', n.kind)
elif len(n.outs) > 0 and len(n.ins) > 0: # output is also read by impl. circuit, need to add a fork.
node_map[n] = Node(self, f'{node.name}~{n.name}')
elif len(n.ins) == 0 and len(n.outs) > 1: # input is read by multiple nodes, need to add fork.
node_map[n] = Node(self, f'{node.name}~{n.name}')
for l in impl.lines: # add all internal lines to main circuit
if l.reader in node_map and l.driver in node_map:
Line(self, (node_map[l.driver], l.driver_pin), (node_map[l.reader], l.reader_pin))
for inn, ll in zip(impl_in_nodes, node_in_lines): # connect inputs
if ll is None: continue
if len(inn.outs) == 1:
l = inn.outs[0]
ll.reader = node_map[l.reader]
ll.reader_pin = l.reader_pin
else:
ll.reader = node_map[inn] # connect to existing fork
ll.reader_pin = 0
ll.reader.ins[ll.reader_pin] = ll
for l, ll in zip(impl_out_lines, node_out_lines): # connect outputs
if ll is None:
if l.driver in node_map:
self.remove_dangling_nodes(node_map[l.driver])
continue
if len(l.reader.outs) > 0: # output is also read by impl. circuit, connect to fork.
ll.driver = node_map[l.reader]
ll.driver_pin = len(l.reader.outs)
else:
ll.driver = node_map[l.driver]
ll.driver_pin = l.driver_pin
ll.driver.outs[ll.driver_pin] = ll
def resolve_tlib_cells(self, tlib):
"""Substitute all technology library cells with kyupy native simulation primitives.
See :py:attr:`substitute()` for more detail.
"""
for n in list(self.nodes):
if n.kind in tlib.cells:
self.substitute(n, tlib.cells[n.kind][0])
def copy(self): def copy(self):
"""Returns a deep copy of the circuit. """Returns a deep copy of the circuit.
""" """
@ -247,69 +457,71 @@ class Circuit:
d = c.forks[line.driver.name] if line.driver.kind == '__fork__' else c.cells[line.driver.name] d = c.forks[line.driver.name] if line.driver.kind == '__fork__' else c.cells[line.driver.name]
r = c.forks[line.reader.name] if line.reader.kind == '__fork__' else c.cells[line.reader.name] r = c.forks[line.reader.name] if line.reader.kind == '__fork__' else c.cells[line.reader.name]
Line(c, (d, line.driver_pin), (r, line.reader_pin)) Line(c, (d, line.driver_pin), (r, line.reader_pin))
for node in self.interface: for node in self.io_nodes:
if node.kind == '__fork__': if node.kind == '__fork__':
n = c.forks[node.name] n = c.forks[node.name]
else: else:
n = c.cells[node.name] n = c.cells[node.name]
c.interface.append(n) c.io_nodes.append(n)
return c return c
def __getstate__(self): def __getstate__(self):
nodes = [(node.name, node.kind) for node in self.nodes] nodes = [(node.name, node.kind) for node in self.nodes]
lines = [(line.driver.index, line.driver_pin, line.reader.index, line.reader_pin) for line in self.lines] lines = [(line.driver.index, line.driver_pin, line.reader.index, line.reader_pin) for line in self.lines]
interface = [n.index for n in self.interface] io_nodes = [n.index for n in self.io_nodes]
return {'name': self.name, return {'name': self.name,
'nodes': nodes, 'nodes': nodes,
'lines': lines, 'lines': lines,
'interface': interface } 'io_nodes': io_nodes }
def __setstate__(self, state): def __setstate__(self, state):
self.name = state['name'] self.name = state['name']
self.nodes = IndexList() self.nodes = IndexList()
self.lines = IndexList() self.lines = IndexList()
self.interface = GrowingList() self.io_nodes = GrowingList()
self.cells = {} self.cells = {}
self.forks = {} self.forks = {}
for s in state['nodes']: for s in state['nodes']:
Node(self, *s) Node(self, *s)
for driver, driver_pin, reader, reader_pin in state['lines']: for driver, driver_pin, reader, reader_pin in state['lines']:
Line(self, (self.nodes[driver], driver_pin), (self.nodes[reader], reader_pin)) Line(self, (self.nodes[driver], driver_pin), (self.nodes[reader], reader_pin))
for n in state['interface']: for n in state['io_nodes']:
self.interface.append(self.nodes[n]) self.io_nodes.append(self.nodes[n])
def __eq__(self, other): def __eq__(self, other):
return self.nodes == other.nodes and self.lines == other.lines and self.interface == other.interface return self.nodes == other.nodes and self.lines == other.lines and self.io_nodes == other.io_nodes
def dump(self):
"""Returns a string representation of the circuit and all its nodes.
"""
header = f'{self.name}({",".join([str(n.index) for n in self.interface])})\n'
return header + '\n'.join([str(n) for n in self.nodes])
def __repr__(self): def __repr__(self):
name = f' {self.name}' if self.name else '' return f'{{name: "{self.name}", cells: {len(self.cells)}, forks: {len(self.forks)}, lines: {len(self.lines)}, io_nodes: {len(self.io_nodes)}}}'
return f'<Circuit{name} cells={len(self.cells)} forks={len(self.forks)} ' + \
f'lines={len(self.lines)} ports={len(self.interface)}>'
def topological_order(self): def topological_order(self):
"""Generator function to iterate over all nodes in topological order. """Generator function to iterate over all nodes in topological order.
Nodes without input lines and nodes whose :py:attr:`Node.kind` contains the substring 'DFF' are Nodes without input lines and nodes whose :py:attr:`Node.kind` contains the
yielded first. substrings 'dff' or 'latch' are yielded first.
""" """
visit_count = [0] * len(self.nodes) visit_count = np.zeros(len(self.nodes), dtype=np.uint32)
queue = deque(n for n in self.nodes if len(n.ins) == 0 or 'dff' in n.kind.lower()) queue = deque(n for n in self.nodes if len(n.ins) == 0 or 'dff' in n.kind.lower() or 'latch' in n.kind.lower())
while len(queue) > 0: while len(queue) > 0:
n = queue.popleft() n = queue.popleft()
for line in n.outs: for line in n.outs:
if line is None: continue if line is None: continue
succ = line.reader succ = line.reader
visit_count[succ] += 1 visit_count[succ] += 1
if visit_count[succ] == len(succ.ins) and 'dff' not in succ.kind.lower(): if visit_count[succ] == len(succ.ins) and 'dff' not in succ.kind.lower() and 'latch' not in succ.kind.lower():
queue.append(succ) queue.append(succ)
yield n yield n
def topological_order_with_level(self):
level = np.zeros(len(self.nodes), dtype=np.int32) - 1
for n in self.topological_order():
if len(n.ins) == 0 or 'dff' in n.kind.lower() or 'latch' in n.kind.lower():
l = 0
else:
l = level[[l.driver.index for l in n.ins if l is not None]].max() + 1
level[n] = l
yield n, l
def topological_line_order(self): def topological_line_order(self):
"""Generator function to iterate over all lines in topological order. """Generator function to iterate over all lines in topological order.
""" """
@ -321,17 +533,17 @@ class Circuit:
def reversed_topological_order(self): def reversed_topological_order(self):
"""Generator function to iterate over all nodes in reversed topological order. """Generator function to iterate over all nodes in reversed topological order.
Nodes without output lines and nodes whose :py:attr:`Node.kind` contains the substring 'DFF' are Nodes without output lines and nodes whose :py:attr:`Node.kind` contains the
yielded first. substrings 'dff' or 'latch' are yielded first.
""" """
visit_count = [0] * len(self.nodes) visit_count = [0] * len(self.nodes)
queue = deque(n for n in self.nodes if len(n.outs) == 0 or 'dff' in n.kind.lower()) queue = deque(n for n in self.nodes if len(n.outs) == 0 or 'dff' in n.kind.lower() or 'latch' in n.kind.lower())
while len(queue) > 0: while len(queue) > 0:
n = queue.popleft() n = queue.popleft()
for line in n.ins: for line in n.ins:
pred = line.driver pred = line.driver
visit_count[pred] += 1 visit_count[pred] += 1
if visit_count[pred] == len(pred.outs) and 'dff' not in pred.kind.lower(): if visit_count[pred] == len(pred.outs) and 'dff' not in pred.kind.lower() and 'latch' not in pred.kind.lower():
queue.append(pred) queue.append(pred)
yield n yield n
@ -371,3 +583,33 @@ class Circuit:
queue.extend(preds) queue.extend(preds)
region.append(n) region.append(n)
yield stem, region yield stem, region
def dot(self, format='svg'):
from graphviz import Digraph
dot = Digraph(format=format, graph_attr={'rankdir': 'LR', 'splines': 'true'})
s_dict = dict((n, i) for i, n in enumerate(self.s_nodes))
node_level = np.zeros(len(self.nodes), dtype=np.uint32)
level_nodes = defaultdict(list)
for n, lv in self.topological_order_with_level():
level_nodes[lv].append(n)
node_level[n] = lv
for lv in level_nodes:
with dot.subgraph() as s:
s.attr(rank='same')
for n in level_nodes[lv]:
ins = '|'.join([f'<i{i}>{i}' for i in range(len(n.ins))])
outs = '|'.join([f'<o{i}>{i}' for i in range(len(n.outs))])
io = f' [{s_dict[n]}]' if n in s_dict else ''
s.node(name=str(n.index), label = f'{{{{{ins}}}|{n.index}{io}\n{n.kind}\n{n.name}|{{{outs}}}}}', shape='record')
for l in self.lines:
driver, reader = f'{l.driver.index}:o{l.driver_pin}', f'{l.reader.index}:i{l.reader_pin}'
if node_level[l.driver] >= node_level[l.reader]:
dot.edge(driver, reader, style='dotted', label=str(l.index))
pass
else:
dot.edge(driver, reader, label=str(l.index))
return dot

297
src/kyupy/def_file.py

@ -0,0 +1,297 @@
"""A simple and incomplete parser for the Design Exchange Format (DEF).
This parser extracts information on components and nets from DEF files and make them available
as an intermediate representation (:class:`DefFile` object).
"""
from collections import defaultdict
from lark import Lark, Transformer, Tree
from kyupy import readtext
class DefNet:
def __init__(self, name):
self.name = name
self.pins = []
@property
def wires(self):
ww = defaultdict(list)
[ww[dw.layer].append((int(dw.width), dw.wire_points)) for dw in self.routed if len(dw.wire_points) > 0]
return ww
@property
def vias(self):
vv = defaultdict(list)
[vv[vtype].extend(locs) for dw in self.routed for vtype, locs in dw.vias.items()]
return vv
class DefWire:
def __init__(self):
self.layer = None
self.width = None
self.points = []
@property
def wire_points(self):
start = [self.points[0]]
rest = [p for p in self.points[1:] if not isinstance(p[0], str)] # skip over vias
return start + rest if len(rest) > 0 else []
@property
def vias(self):
vv = defaultdict(list)
loc = self.points[0]
for p in self.points[1:]:
if not isinstance(p[0], str): # new location
loc = (loc[0] if p[0] is None else p[0], loc[1] if p[1] is None else p[1]) # if None, keep previous value
continue
vtype, param = p
if isinstance(param, tuple): # expand "DO x BY y STEP xs ys"
x_cnt, y_cnt, x_sp, y_sp = param
[vv[vtype].append((loc[0] + x*x_sp, loc[1] + y*y_sp, 'N')) for x in range(x_cnt) for y in range(y_cnt)]
else:
vv[vtype].append((loc[0], loc[1], param or 'N'))
return vv
def __repr__(self):
return f'<DefWire {self.layer} {self.width} {self.points}>'
class DefVia:
def __init__(self, name):
self.name = name
self.rowcol = [1, 1]
self.cutspacing = [0, 0]
class DefPin:
def __init__(self, name):
self.name = name
self.points = []
class DefFile:
"""Intermediate representation of a DEF file."""
def __init__(self):
self.rows = []
self.tracks = []
self.units = []
self.vias = {}
self.components = {}
self.pins = {}
self.specialnets = {}
self.nets = {}
class DefTransformer(Transformer):
def __init__(self): self.def_file = DefFile()
def start(self, args): return self.def_file
def design(self, args): self.def_file.design = args[0].value
def point(self, args): return tuple(int(arg.value) if arg != '*' else None for arg in args)
def do_step(self, args): return tuple(map(int, args))
def spnet_wires(self, args): return args[0].lower(), args[1:]
def net_wires(self, args): return args[0].lower(), args[1:]
def sppoints(self, args): return args
def points(self, args): return args
def net_pin(self, args): return '__pin__', (args[0].value, args[1].value)
def net_opt(self, args): return args[0].lower(), args[1].value
def file_stmt(self, args):
value = args[1].value
value = value[1:-1] if value[0] == '"' else value
setattr(self.def_file, args[0].lower(), value)
def design_stmt(self, args):
stmt = args[0].lower()
if stmt == 'units': self.def_file.units.append((args[1].value, args[2].value, int(args[3])))
elif stmt == 'diearea': self.def_file.diearea = args[1:]
elif stmt == 'row':
self.def_file.rows.append((args[1].value, # rowName
args[2].value, # siteName
(int(args[3]), int(args[4])), # origin x/y
args[5].value, # orientation
max(args[6][0], args[6][1]), # number of sites
max(args[6][2], args[6][3]) # site width
))
elif stmt == 'tracks':
self.def_file.tracks.append((args[1].value, # orientation
int(args[2]), # start
int(args[3]), # number of tracks
int(args[4]), # spacing
args[5].value # layer
))
def vias_stmt(self, args):
via = DefVia(args[0].value)
[setattr(via, opt, val) for opt, val in args[1:]]
self.def_file.vias[via.name] = via
def vias_opt(self, args):
opt = args[0].lower()
if opt in ['viarule', 'pattern']: val = args[1].value
elif opt in ['layers']: val = [arg.value for arg in args[1:]]
else: val = [int(arg) for arg in args[1:]]
return opt, val
def comp_stmt(self, args):
name = args[0].value
kind = args[1].value
point = args[2]
orientation = args[3].value
self.def_file.components[name] = (kind, point, orientation)
def pins_stmt(self, args):
pin = DefPin(args[0].value)
[pin.points.append(val) if opt == 'placed' else setattr(pin, opt, val) for opt, val in args[1:]]
self.def_file.pins[pin.name] = pin
def pins_opt(self, args):
opt = args[0].lower()
if opt in ['net', 'direction', 'use']: val = args[1].value
elif opt in ['layer']: val = [args[1].value] + args[2:]
elif opt in ['placed']: val = (args[1][0], args[1][1], args[2].value)
else: val = []
return opt, val
def spnets_stmt(self, args):
dnet = DefNet(args[0].value)
for arg in args[1:]:
if arg[0] == '__pin__': dnet.pins.append(arg[1])
else: setattr(dnet, arg[0], arg[1])
self.def_file.specialnets[dnet.name] = dnet
def nets_stmt(self, args):
dnet = DefNet(args[0].value)
for arg in args[1:]:
if arg[0] == '__pin__': dnet.pins.append(arg[1])
else: setattr(dnet, arg[0], arg[1])
self.def_file.nets[dnet.name] = dnet
def spwire(self, args):
wire = DefWire()
wire.layer = args[0].value
wire.width = args[1].value
wire.points = args[-1]
return wire
def wire(self, args):
wire = DefWire()
wire.layer = args[0].value
wire.points = args[-1]
return wire
def sppoints_via(self, args):
if len(args) == 1: return args[0].value, None
else: return args[0].value, args[1]
def points_via(self, args):
if len(args) == 1: return args[0].value, 'N'
else: return args[0].value, args[1].value.strip()
GRAMMAR = r"""
start: /#[^\n]*/? file_stmt*
?file_stmt: /VERSION/ ID ";"
| /DIVIDERCHAR/ STRING ";"
| /BUSBITCHARS/ STRING ";"
| design
design: "DESIGN" ID ";" design_stmt* "END" "DESIGN"
?design_stmt: /UNITS/ ID ID NUMBER ";"
| /DIEAREA/ point+ ";"
| /ROW/ ID ID NUMBER NUMBER ID do_step ";"
| /TRACKS/ /[XY]/ NUMBER "DO" NUMBER "STEP" NUMBER "LAYER" ID ";"
| propdef | vias | nondef | comp | pins | pinprop | spnets | nets
propdef: "PROPERTYDEFINITIONS" propdef_stmt* "END" "PROPERTYDEFINITIONS"
propdef_stmt: /COMPONENTPIN/ ID ID ";"
vias: "VIAS" NUMBER ";" vias_stmt* "END" "VIAS"
vias_stmt: "-" ID vias_opt* ";"
vias_opt: "+" /VIARULE/ ID
| "+" /CUTSIZE/ NUMBER NUMBER
| "+" /LAYERS/ ID ID ID
| "+" /CUTSPACING/ NUMBER NUMBER
| "+" /ENCLOSURE/ NUMBER NUMBER NUMBER NUMBER
| "+" /ROWCOL/ NUMBER NUMBER
| "+" /PATTERN/ ID
nondef: "NONDEFAULTRULES" NUMBER ";" nondef_stmt+ "END" "NONDEFAULTRULES"
nondef_stmt: "-" ID ( "+" /HARDSPACING/
| "+" /LAYER/ ID "WIDTH" NUMBER "SPACING" NUMBER
| "+" /VIA/ ID )* ";"
comp: "COMPONENTS" NUMBER ";" comp_stmt* "END" "COMPONENTS"
comp_stmt: "-" ID ID "+" "PLACED" point ID ";"
pins: "PINS" NUMBER ";" pins_stmt* "END" "PINS"
pins_stmt: "-" ID pins_opt* ";"
pins_opt: "+" /NET/ ID
| "+" /SPECIAL/
| "+" /DIRECTION/ ID
| "+" /USE/ ID
| "+" /PORT/
| "+" /LAYER/ ID point point
| "+" /PLACED/ point ID
pinprop: "PINPROPERTIES" NUMBER ";" pinprop_stmt* "END" "PINPROPERTIES"
pinprop_stmt: "-" "PIN" ID "+" "PROPERTY" ID STRING ";"
spnets: "SPECIALNETS" NUMBER ";" spnets_stmt* "END" "SPECIALNETS"
spnets_stmt: "-" ID ( net_pin | net_opt | spnet_wires )* ";"
spnet_wires: "+" ( /COVER/ | /FIXED/ | /ROUTED/ ) spwire ( "NEW" spwire )*
spwire: ID NUMBER spwire_opt* sppoints
spwire_opt: "+" /SHAPE/ ID
| "+" /STYLE/ ID
sppoints: point ( point | sppoints_via )+
sppoints_via: ID do_step?
nets: "NETS" NUMBER ";" nets_stmt* "END" "NETS"
nets_stmt: "-" ID ( net_pin | net_opt | net_wires )* ";"
net_pin: "(" ID ID ")"
net_opt: "+" /USE/ ID
| "+" /NONDEFAULTRULE/ ID
net_wires: "+" ( /COVER/ | /FIXED/ | /ROUTED/ | /NOSHIELD/ ) wire ( "NEW" wire )*
wire: ID wire_opt points
wire_opt: ( "TAPER" | "TAPERRULE" ID )? ("STYLE" ID)?
points: point ( point | points_via )+
points_via: ID ORIENTATION?
point: "(" (NUMBER|/\*/) (NUMBER|/\*/) NUMBER? ")"
do_step: "DO" NUMBER "BY" NUMBER "STEP" (NUMBER|SIGNED_NUMBER) (NUMBER|SIGNED_NUMBER)
ORIENTATION.2: /F?[NWES]/ WS
ID: /[^ \t\f\r\n+][^ \t\f\r\n]*/
STRING : "\"" /.*?/s /(?<!\\)(\\\\)*?/ "\""
WS: /[ \t\f\r\n]/
%import common.NUMBER
%import common.SIGNED_NUMBER
%ignore WS (/#[^\n]*/)?
"""
def parse(text):
"""Parses the given ``text`` and returns a :class:`DefFile` object."""
return Lark(GRAMMAR, parser="lalr", transformer=DefTransformer()).parse(text)
def load(file):
"""Parses the contents of ``file`` and returns a :class:`DefFile` object.
Files with `.gz`-suffix are decompressed on-the-fly.
"""
return parse(readtext(file))

682
src/kyupy/logic.py

@ -1,4 +1,9 @@
"""This module contains definitions and data structures for 2-, 4-, and 8-valued logic operations. """Core module for handling 2-, 4-, and 8-valued logic data and signal values.
Logic values are stored in numpy arrays with data type ``np.uint8``.
There are no explicit data structures in KyuPy for holding patterns, pattern sets or vectors.
However, there are conventions on logic value encoding and on the order of axes.
Utility functions defined here follow these conventions.
8 logic values are defined as integer constants. 8 logic values are defined as integer constants.
@ -6,21 +11,39 @@
* 4-valued logic adds: ``UNASSIGNED`` and ``UNKNOWN`` * 4-valued logic adds: ``UNASSIGNED`` and ``UNKNOWN``
* 8-valued logic adds: ``RISE``, ``FALL``, ``PPULSE``, and ``NPULSE``. * 8-valued logic adds: ``RISE``, ``FALL``, ``PPULSE``, and ``NPULSE``.
The bits in these constants have the following meaning: In general, the bits in these constants have the following meaning:
* bit0: Final/settled binary value of a signal
* bit1: Initial binary value of a signal
* bit2: Activity or transitions are present on a signal
Except when bit0 differs from bit1, but bit2 (activity) is 0:
* bit0 = 1, bit1 = 0, bit2 = 0 means ``UNKNOWN`` in 4-valued and 8-valued logic.
* bit0 = 0, bit1 = 1, bit2 = 0 means ``UNASSIGNED`` in 4-valued and 8-valued logic.
2-valued logic only considers bit0, but should store logic one as ``ONE=0b011`` for interoperability.
4-valued logic only considers bit0 and bit1.
8-valued logic considers all 3 bits.
* bit 0: Final/settled binary value of a signal Logic values are stored in numpy arrays of data type ``np.uint8``.
* bit 1: Initial binary value of a signal The axis convention is as follows:
* bit 2: Activity or transitions are present on a signal
Special meaning is given to values where bits 0 and 1 differ, but bit 2 (activity) is 0. * The **last** axis goes along patterns/vectors. I.e. ``values[...,0]`` is pattern 0, ``values[...,1]`` is pattern 1, etc.
These values are interpreted as ``UNKNOWN`` or ``UNASSIGNED`` in 4-valued and 8-valued logic. * The **second-to-last** axis goes along the I/O and flip-flops of circuits. For a circuit ``c``, this axis is usually
``len(c.s_nodes)`` long. The values of all inputs, outputs and flip-flops are stored within the same array and the location
along the second-to-last axis is determined by the order in :py:attr:`~kyupy.circuit.Circuit.s_nodes`.
Two storage formats are used in KyuPy:
* ``mv...`` (for "multi-valued"): Each logic value is stored in the least significant 3 bits of ``np.uint8``.
* ``bp...`` (for "bit-parallel"): Groups of 8 logic values are stored as three ``np.uint8``. This format is used
for bit-parallel logic simulations. It is also more memory-efficient.
The functions in this module use the ``mv...`` and ``bp...`` prefixes to signify the storage format they operate on.
In general, 2-valued logic only considers bit 0, 4-valued logic considers bits 0 and 1, and 8-valued logic
considers all 3 bits.
The only exception is constant ``ONE=0b11`` which has two bits set for all logics including 2-valued logic.
""" """
import math
from collections.abc import Iterable from collections.abc import Iterable
import numpy as np import numpy as np
@ -66,245 +89,152 @@ def interpret(value):
""" """
if isinstance(value, Iterable) and not (isinstance(value, str) and len(value) == 1): if isinstance(value, Iterable) and not (isinstance(value, str) and len(value) == 1):
return list(map(interpret, value)) return list(map(interpret, value))
if value in [0, '0', False, 'L', 'l']: if value in [0, '0', False, 'L', 'l']: return ZERO
return ZERO if value in [1, '1', True, 'H', 'h']: return ONE
if value in [1, '1', True, 'H', 'h']: if value in [None, '-', 'Z', 'z']: return UNASSIGNED
return ONE if value in ['R', 'r', '/']: return RISE
if value in [None, '-', 'Z', 'z']: if value in ['F', 'f', '\\']: return FALL
return UNASSIGNED if value in ['P', 'p', '^']: return PPULSE
if value in ['R', 'r', '/']: if value in ['N', 'n', 'v']: return NPULSE
return RISE
if value in ['F', 'f', '\\']:
return FALL
if value in ['P', 'p', '^']:
return PPULSE
if value in ['N', 'n', 'v']:
return NPULSE
return UNKNOWN return UNKNOWN
_bit_in_lut = np.array([2 ** x for x in range(7, -1, -1)], dtype='uint8') def mvarray(*a):
"""Converts (lists of) Boolean values or strings into a multi-valued array.
@numba.njit The given values are interpreted and the axes are arranged as per KyuPy's convention.
def bit_in(a, pos): Use this function to convert strings into multi-valued arrays.
return a[pos >> 3] & _bit_in_lut[pos & 7] """
mva = np.array(interpret(a), dtype=np.uint8)
if mva.ndim < 2: return mva
class MVArray: if mva.shape[-2] > 1: return mva.swapaxes(-1, -2)
"""An n-dimensional array of m-valued logic values. return mva[..., 0, :]
This class wraps a numpy.ndarray of type uint8 and adds support for encoding and
interpreting 2-valued, 4-valued, and 8-valued logic values.
Each logic value is stored as an uint8, manipulations of individual values are cheaper than in
:py:class:`BPArray`.
:param a: If a tuple is given, it is interpreted as desired shape. To make an array of ``n`` vectors def mv_str(mva, delim='\n'):
compatible with a simulator ``sim``, use ``(len(sim.interface), n)``. If a :py:class:`BPArray` or """Renders a given multi-valued array into a string.
:py:class:`MVArray` is given, a deep copy is made. If a string, a list of strings, a list of characters,
or a list of lists of characters are given, the data is interpreted best-effort and the array is
initialized accordingly.
:param m: The arity of the logic. Can be set to 2, 4, or 8. If None is given, the arity of a given
:py:class:`BPArray` or :py:class:`MVArray` is used, or, if the array is initialized differently, 8 is used.
""" """
sa = np.choose(mva, np.array([*'0X-1PRFN'], dtype=np.unicode_))
if not hasattr(mva, 'ndim') or mva.ndim == 0: return sa
if mva.ndim == 1: return ''.join(sa)
return delim.join([''.join(c) for c in sa.swapaxes(-1,-2)])
def __init__(self, a, m=None): def _mv_not(out, inp):
self.m = m or 8
assert self.m in [2, 4, 8]
# Try our best to interpret given a.
if isinstance(a, MVArray):
self.data = a.data.copy()
"""The wrapped 2-dimensional ndarray of logic values.
* Axis 0 is PI/PO/FF position, the length of this axis is called "width".
* Axis 1 is vector/pattern, the length of this axis is called "length".
"""
self.m = m or a.m
elif hasattr(a, 'data'): # assume it is a BPArray. Can't use isinstance() because BPArray isn't declared yet.
self.data = np.zeros((a.width, a.length), dtype=np.uint8)
self.m = m or a.m
for i in range(a.data.shape[-2]):
self.data[...] <<= 1
self.data[...] |= np.unpackbits(a.data[..., -i-1, :], axis=1)[:, :a.length]
if a.data.shape[-2] == 1:
self.data *= 3
elif isinstance(a, int):
self.data = np.full((a, 1), UNASSIGNED, dtype=np.uint8)
elif isinstance(a, tuple):
self.data = np.full(a, UNASSIGNED, dtype=np.uint8)
else:
if isinstance(a, str): a = [a]
self.data = np.asarray(interpret(a), dtype=np.uint8)
self.data = self.data[:, np.newaxis] if self.data.ndim == 1 else np.moveaxis(self.data, -2, -1)
# Cast data to m-valued logic.
if self.m == 2:
self.data[...] = ((self.data & 0b001) & ((self.data >> 1) & 0b001) | (self.data == RISE)) * ONE
elif self.m == 4:
self.data[...] = (self.data & 0b011) & ((self.data != FALL) * ONE) | ((self.data == RISE) * ONE)
elif self.m == 8:
self.data[...] = self.data & 0b111
self.length = self.data.shape[-1]
self.width = self.data.shape[-2]
def __repr__(self):
return f'<MVArray length={self.length} width={self.width} m={self.m} mem={hr_bytes(self.data.nbytes)}>'
def __str__(self):
return str([self[idx] for idx in range(self.length)])
def __getitem__(self, vector_idx):
"""Returns a string representing the desired vector."""
chars = ["0", "X", "-", "1", "P", "R", "F", "N"]
return ''.join(chars[v] for v in self.data[:, vector_idx])
def __len__(self):
return self.length
def mv_cast(*args, m=8):
return [a if isinstance(a, MVArray) else MVArray(a, m=m) for a in args]
def mv_getm(*args):
return max([a.m for a in args if isinstance(a, MVArray)] + [0]) or 8
def _mv_not(m, out, inp):
np.bitwise_xor(inp, 0b11, out=out) # this also exchanges UNASSIGNED <-> UNKNOWN np.bitwise_xor(inp, 0b11, out=out) # this also exchanges UNASSIGNED <-> UNKNOWN
if m > 2: np.putmask(out, (inp == UNKNOWN), UNKNOWN) # restore UNKNOWN
np.putmask(out, (inp == UNKNOWN), UNKNOWN) # restore UNKNOWN
def mv_not(x1, out=None): def mv_not(x1 : np.ndarray, out=None):
"""A multi-valued NOT operator. """A multi-valued NOT operator.
:param x1: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. :param x1: A multi-valued array.
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray` :param out: An optional storage destination. If None, a new multi-valued array is returned.
is returned. :return: A multi-valued array with the result.
:return: An :py:class:`MVArray` with the result.
""" """
m = mv_getm(x1) out = out or np.empty(x1.shape, dtype=np.uint8)
x1 = mv_cast(x1, m=m)[0] _mv_not(out, x1)
out = out or MVArray(x1.data.shape, m=m)
_mv_not(m, out.data, x1.data)
return out return out
def _mv_or(m, out, *ins): def _mv_or(out, *ins):
if m > 2: any_unknown = (ins[0] == UNKNOWN) | (ins[0] == UNASSIGNED)
any_unknown = (ins[0] == UNKNOWN) | (ins[0] == UNASSIGNED) for inp in ins[1:]: any_unknown |= (inp == UNKNOWN) | (inp == UNASSIGNED)
for inp in ins[1:]: any_unknown |= (inp == UNKNOWN) | (inp == UNASSIGNED) any_one = (ins[0] == ONE)
any_one = (ins[0] == ONE) for inp in ins[1:]: any_one |= (inp == ONE)
for inp in ins[1:]: any_one |= (inp == ONE)
out[...] = ZERO out[...] = ZERO
np.putmask(out, any_one, ONE) np.putmask(out, any_one, ONE)
for inp in ins: for inp in ins:
np.bitwise_or(out, inp, out=out, where=~any_one) np.bitwise_or(out, inp, out=out, where=~any_one)
np.putmask(out, (any_unknown & ~any_one), UNKNOWN) np.putmask(out, (any_unknown & ~any_one), UNKNOWN)
else:
out[...] = ZERO
for inp in ins: np.bitwise_or(out, inp, out=out)
def mv_or(x1, x2, out=None): def mv_or(x1, x2, out=None):
"""A multi-valued OR operator. """A multi-valued OR operator.
:param x1: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. :param x1: A multi-valued array.
:param x2: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. :param x2: A multi-valued array.
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray` :param out: An optional storage destination. If None, a new multi-valued array is returned.
is returned. :return: A multi-valued array with the result.
:return: An :py:class:`MVArray` with the result.
""" """
m = mv_getm(x1, x2) out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8)
x1, x2 = mv_cast(x1, x2, m=m) _mv_or(out, x1, x2)
out = out or MVArray(np.broadcast(x1.data, x2.data).shape, m=m)
_mv_or(m, out.data, x1.data, x2.data)
return out return out
def _mv_and(m, out, *ins): def _mv_and(out, *ins):
if m > 2: any_unknown = (ins[0] == UNKNOWN) | (ins[0] == UNASSIGNED)
any_unknown = (ins[0] == UNKNOWN) | (ins[0] == UNASSIGNED) for inp in ins[1:]: any_unknown |= (inp == UNKNOWN) | (inp == UNASSIGNED)
for inp in ins[1:]: any_unknown |= (inp == UNKNOWN) | (inp == UNASSIGNED) any_zero = (ins[0] == ZERO)
any_zero = (ins[0] == ZERO) for inp in ins[1:]: any_zero |= (inp == ZERO)
for inp in ins[1:]: any_zero |= (inp == ZERO)
out[...] = ONE out[...] = ONE
np.putmask(out, any_zero, ZERO) np.putmask(out, any_zero, ZERO)
for inp in ins: for inp in ins:
np.bitwise_and(out, inp | 0b100, out=out, where=~any_zero) np.bitwise_and(out, inp | 0b100, out=out, where=~any_zero)
if m > 4: np.bitwise_or(out, inp & 0b100, out=out, where=~any_zero) np.bitwise_or(out, inp & 0b100, out=out, where=~any_zero)
np.putmask(out, (any_unknown & ~any_zero), UNKNOWN) np.putmask(out, (any_unknown & ~any_zero), UNKNOWN)
else:
out[...] = ONE
for inp in ins: np.bitwise_and(out, inp, out=out)
def mv_and(x1, x2, out=None): def mv_and(x1, x2, out=None):
"""A multi-valued AND operator. """A multi-valued AND operator.
:param x1: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. :param x1: A multi-valued array.
:param x2: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. :param x2: A multi-valued array.
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray` :param out: An optional storage destination. If None, a new multi-valued array is returned.
is returned. :return: A multi-valued array with the result.
:return: An :py:class:`MVArray` with the result.
""" """
m = mv_getm(x1, x2) out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8)
x1, x2 = mv_cast(x1, x2, m=m) _mv_and(out, x1, x2)
out = out or MVArray(np.broadcast(x1.data, x2.data).shape, m=m)
_mv_and(m, out.data, x1.data, x2.data)
return out return out
def _mv_xor(m, out, *ins): def _mv_xor(out, *ins):
if m > 2: any_unknown = (ins[0] == UNKNOWN) | (ins[0] == UNASSIGNED)
any_unknown = (ins[0] == UNKNOWN) | (ins[0] == UNASSIGNED) for inp in ins[1:]: any_unknown |= (inp == UNKNOWN) | (inp == UNASSIGNED)
for inp in ins[1:]: any_unknown |= (inp == UNKNOWN) | (inp == UNASSIGNED)
out[...] = ZERO out[...] = ZERO
for inp in ins: for inp in ins:
np.bitwise_xor(out, inp & 0b011, out=out) np.bitwise_xor(out, inp & 0b011, out=out)
if m > 4: np.bitwise_or(out, inp & 0b100, out=out) np.bitwise_or(out, inp & 0b100, out=out)
np.putmask(out, any_unknown, UNKNOWN) np.putmask(out, any_unknown, UNKNOWN)
else:
out[...] = ZERO
for inp in ins: np.bitwise_xor(out, inp, out=out)
def mv_xor(x1, x2, out=None): def mv_xor(x1, x2, out=None):
"""A multi-valued XOR operator. """A multi-valued XOR operator.
:param x1: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. :param x1: A multi-valued array.
:param x2: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. :param x2: A multi-valued array.
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray` :param out: An optional storage destination. If None, a new multi-valued array is returned.
is returned. :return: A multi-valued array with the result.
:return: An :py:class:`MVArray` with the result.
""" """
m = mv_getm(x1, x2) out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8)
x1, x2 = mv_cast(x1, x2, m=m) _mv_xor(out, x1, x2)
out = out or MVArray(np.broadcast(x1.data, x2.data).shape, m=m)
_mv_xor(m, out.data, x1.data, x2.data)
return out return out
def mv_latch(d, t, q_prev, out=None): def mv_latch(d, t, q_prev, out=None):
"""A latch that is transparent if `t` is high. `q_prev` has to be the output value from the previous clock cycle. """A multi-valued latch operator.
A latch outputs ``d`` when transparent (``t`` is high).
It outputs ``q_prev`` when in latched state (``t`` is low).
:param d: A multi-valued array for the data input.
:param t: A multi-valued array for the control input.
:param q_prev: A multi-valued array with the output value of this latch from the previous clock cycle.
:param out: An optional storage destination. If None, a new multi-valued array is returned.
:return: A multi-valued array for the latch output ``q``.
""" """
m = mv_getm(d, t, q_prev) out = out or np.empty(np.broadcast(d, t, q_prev).shape, dtype=np.uint8)
d, t, q_prev = mv_cast(d, t, q_prev, m=m) out[...] = t & d & 0b011
out = out or MVArray(np.broadcast(d.data, t.data, q_prev).shape, m=m) out[...] |= ~t & 0b010 & (q_prev << 1)
out.data[...] = t.data & d.data & 0b011 out[...] |= ~t & 0b001 & (out >> 1)
out.data[...] |= ~t.data & 0b010 & (q_prev.data << 1) out[...] |= ((out << 1) ^ (out << 2)) & 0b100
out.data[...] |= ~t.data & 0b001 & (out.data >> 1) unknown = (t == UNKNOWN) \
out.data[...] |= ((out.data << 1) ^ (out.data << 2)) & 0b100 | (t == UNASSIGNED) \
unknown = (t.data == UNKNOWN) \ | (((d == UNKNOWN) | (d == UNASSIGNED)) & (t != ZERO))
| (t.data == UNASSIGNED) \ np.putmask(out, unknown, UNKNOWN)
| (((d.data == UNKNOWN) | (d.data == UNASSIGNED)) & (t.data != ZERO))
np.putmask(out.data, unknown, UNKNOWN)
return out return out
@ -313,191 +243,191 @@ def mv_transition(init, final, out=None):
Pulses in the input data are ignored. If any of the inputs are ``UNKNOWN``, the result is ``UNKNOWN``. Pulses in the input data are ignored. If any of the inputs are ``UNKNOWN``, the result is ``UNKNOWN``.
If both inputs are ``UNASSIGNED``, the result is ``UNASSIGNED``. If both inputs are ``UNASSIGNED``, the result is ``UNASSIGNED``.
:param init: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. :param init: A multi-valued array.
:param final: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts. :param final: A multi-valued array.
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray` :param out: An optional storage destination. If None, a new multi-valued array is returned.
is returned. :return: A multi-valued array with the result.
:return: An :py:class:`MVArray` with the result.
""" """
m = mv_getm(init, final) out = out or np.empty(np.broadcast(init, final).shape, dtype=np.uint8)
init, final = mv_cast(init, final, m=m) out[...] = (init & 0b010) | (final & 0b001)
init = init.data out[...] |= ((out << 1) ^ (out << 2)) & 0b100
final = final.data
out = out or MVArray(np.broadcast(init, final).shape, m=8)
out.data[...] = (init & 0b010) | (final & 0b001)
out.data[...] |= ((out.data << 1) ^ (out.data << 2)) & 0b100
unknown = (init == UNKNOWN) | (init == UNASSIGNED) | (final == UNKNOWN) | (final == UNASSIGNED) unknown = (init == UNKNOWN) | (init == UNASSIGNED) | (final == UNKNOWN) | (final == UNASSIGNED)
unassigned = (init == UNASSIGNED) & (final == UNASSIGNED) unassigned = (init == UNASSIGNED) & (final == UNASSIGNED)
np.putmask(out.data, unknown, UNKNOWN) np.putmask(out, unknown, UNKNOWN)
np.putmask(out.data, unassigned, UNASSIGNED) np.putmask(out, unassigned, UNASSIGNED)
return out return out
class BPArray: def mv_to_bp(mva):
"""An n-dimensional array of m-valued logic values that uses bit-parallel storage. """Converts a multi-valued array into a bit-parallel array.
"""
if mva.ndim == 1: mva = mva[..., np.newaxis]
return np.packbits(unpackbits(mva)[...,:3], axis=-2, bitorder='little').swapaxes(-1,-2)
def bparray(*a):
"""Converts (lists of) Boolean values or strings into a bit-parallel array.
The given values are interpreted and the axes are arranged as per KyuPy's convention.
Use this function to convert strings into bit-parallel arrays.
"""
return mv_to_bp(mvarray(*a))
The primary use of this format is in aiding efficient bit-parallel logic simulation.
The secondary benefit over :py:class:`MVArray` is its memory efficiency.
Accessing individual values is more expensive than with :py:class:`MVArray`.
Therefore it may be more efficient to unpack the data into an :py:class:`MVArray` and pack it again into a
:py:class:`BPArray` for simulation.
See :py:class:`MVArray` for constructor parameters. def bp_to_mv(bpa):
"""Converts a bit-parallel array into a multi-valued array.
""" """
return packbits(np.unpackbits(bpa, axis=-1, bitorder='little').swapaxes(-1,-2))
def __init__(self, a, m=None):
if not isinstance(a, MVArray) and not isinstance(a, BPArray): def bp4v_buf(out, inp):
a = MVArray(a, m) unknown = inp[..., 0, :] ^ inp[..., 1, :]
self.m = a.m out[..., 0, :] = inp[..., 0, :] | unknown
if isinstance(a, MVArray): out[..., 1, :] = inp[..., 1, :] & ~unknown
if m is not None and m != a.m: return out
a = MVArray(a, m) # cast data
self.m = a.m
assert self.m in [2, 4, 8] def bp8v_buf(out, inp):
nwords = math.ceil(math.log2(self.m)) unknown = (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :]
nbytes = (a.data.shape[-1] - 1) // 8 + 1 out[..., 0, :] = inp[..., 0, :] | unknown
self.data = np.zeros(a.data.shape[:-1] + (nwords, nbytes), dtype=np.uint8) out[..., 1, :] = inp[..., 1, :] & ~unknown
"""The wrapped 3-dimensional ndarray. out[..., 2, :] = inp[..., 2, :] & ~unknown
return out
* Axis 0 is PI/PO/FF position, the length of this axis is called "width".
* Axis 1 has length ``ceil(log2(m))`` for storing all bits.
* Axis 2 are the vectors/patterns packed into uint8 words. def bp4v_not(out, inp):
""" unknown = inp[..., 0, :] ^ inp[..., 1, :]
for i in range(self.data.shape[-2]): out[..., 0, :] = ~inp[..., 0, :] | unknown
self.data[..., i, :] = np.packbits((a.data >> i) & 1, axis=-1) out[..., 1, :] = ~inp[..., 1, :] & ~unknown
else: # we have a BPArray return out
self.data = a.data.copy() # TODO: support conversion to different m
self.m = a.m
self.length = a.length def bp8v_not(out, inp):
self.width = a.width unknown = (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :]
out[..., 0, :] = ~inp[..., 0, :] | unknown
def __repr__(self): out[..., 1, :] = ~inp[..., 1, :] & ~unknown
return f'<BPArray length={self.length} width={self.width} m={self.m} mem={hr_bytes(self.data.nbytes)}>' out[..., 2, :] = inp[..., 2, :] & ~unknown
return out
def __len__(self):
return self.length
def bp4v_or(out, *ins):
def bp_buf(out, inp):
md = out.shape[-2]
assert md == inp.shape[-2]
if md > 1:
unknown = inp[..., 0, :] ^ inp[..., 1, :]
if md > 2: unknown &= ~inp[..., 2, :]
out[..., 0, :] = inp[..., 0, :] | unknown
out[..., 1, :] = inp[..., 1, :] & ~unknown
if md > 2: out[..., 2, :] = inp[..., 2, :] & ~unknown
else:
out[..., 0, :] = inp[..., 0, :]
def bp_not(out, inp):
md = out.shape[-2]
assert md == inp.shape[-2]
if md > 1:
unknown = inp[..., 0, :] ^ inp[..., 1, :]
if md > 2: unknown &= ~inp[..., 2, :]
out[..., 0, :] = ~inp[..., 0, :] | unknown
out[..., 1, :] = ~inp[..., 1, :] & ~unknown
if md > 2: out[..., 2, :] = inp[..., 2, :] & ~unknown
else:
out[..., 0, :] = ~inp[..., 0, :]
def bp_or(out, *ins):
md = out.shape[-2]
for inp in ins: assert md == inp.shape[-2]
out[...] = 0 out[...] = 0
if md == 1: any_unknown = ins[0][..., 0, :] ^ ins[0][..., 1, :]
for inp in ins: out[..., 0, :] |= inp[..., 0, :] for inp in ins[1:]: any_unknown |= inp[..., 0, :] ^ inp[..., 1, :]
elif md == 2: any_one = ins[0][..., 0, :] & ins[0][..., 1, :]
any_unknown = ins[0][..., 0, :] ^ ins[0][..., 1, :] for inp in ins[1:]: any_one |= inp[..., 0, :] & inp[..., 1, :]
for inp in ins[1:]: any_unknown |= inp[..., 0, :] ^ inp[..., 1, :] for inp in ins:
any_one = ins[0][..., 0, :] & ins[0][..., 1, :] out[..., 0, :] |= inp[..., 0, :] | any_unknown
for inp in ins[1:]: any_one |= inp[..., 0, :] & inp[..., 1, :] out[..., 1, :] |= inp[..., 1, :] & (~any_unknown | any_one)
for inp in ins: return out
out[..., 0, :] |= inp[..., 0, :] | any_unknown
out[..., 1, :] |= inp[..., 1, :] & (~any_unknown | any_one)
else: def bp8v_or(out, *ins):
any_unknown = (ins[0][..., 0, :] ^ ins[0][..., 1, :]) & ~ins[0][..., 2, :] out[...] = 0
for inp in ins[1:]: any_unknown |= (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :] any_unknown = (ins[0][..., 0, :] ^ ins[0][..., 1, :]) & ~ins[0][..., 2, :]
any_one = ins[0][..., 0, :] & ins[0][..., 1, :] & ~ins[0][..., 2, :] for inp in ins[1:]: any_unknown |= (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :]
for inp in ins[1:]: any_one |= inp[..., 0, :] & inp[..., 1, :] & ~inp[..., 2, :] any_one = ins[0][..., 0, :] & ins[0][..., 1, :] & ~ins[0][..., 2, :]
for inp in ins: for inp in ins[1:]: any_one |= inp[..., 0, :] & inp[..., 1, :] & ~inp[..., 2, :]
out[..., 0, :] |= inp[..., 0, :] | any_unknown for inp in ins:
out[..., 1, :] |= inp[..., 1, :] & (~any_unknown | any_one) out[..., 0, :] |= inp[..., 0, :] | any_unknown
out[..., 2, :] |= inp[..., 2, :] & (~any_unknown | any_one) & ~any_one out[..., 1, :] |= inp[..., 1, :] & (~any_unknown | any_one)
out[..., 2, :] |= inp[..., 2, :] & (~any_unknown | any_one) & ~any_one
return out
def bp_and(out, *ins):
md = out.shape[-2]
for inp in ins: assert md == inp.shape[-2] def bp4v_and(out, *ins):
out[...] = 0xff
any_unknown = ins[0][..., 0, :] ^ ins[0][..., 1, :]
for inp in ins[1:]: any_unknown |= inp[..., 0, :] ^ inp[..., 1, :]
any_zero = ~ins[0][..., 0, :] & ~ins[0][..., 1, :]
for inp in ins[1:]: any_zero |= ~inp[..., 0, :] & ~inp[..., 1, :]
for inp in ins:
out[..., 0, :] &= inp[..., 0, :] | (any_unknown & ~any_zero)
out[..., 1, :] &= inp[..., 1, :] & ~any_unknown
return out
def bp8v_and(out, *ins):
out[...] = 0xff out[...] = 0xff
if md == 1: any_unknown = (ins[0][..., 0, :] ^ ins[0][..., 1, :]) & ~ins[0][..., 2, :]
for inp in ins: out[..., 0, :] &= inp[..., 0, :] for inp in ins[1:]: any_unknown |= (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :]
elif md == 2: any_zero = ~ins[0][..., 0, :] & ~ins[0][..., 1, :] & ~ins[0][..., 2, :]
any_unknown = ins[0][..., 0, :] ^ ins[0][..., 1, :] for inp in ins[1:]: any_zero |= ~inp[..., 0, :] & ~inp[..., 1, :] & ~inp[..., 2, :]
for inp in ins[1:]: any_unknown |= inp[..., 0, :] ^ inp[..., 1, :] out[..., 2, :] = 0
any_zero = ~ins[0][..., 0, :] & ~ins[0][..., 1, :] for inp in ins:
for inp in ins[1:]: any_zero |= ~inp[..., 0, :] & ~inp[..., 1, :] out[..., 0, :] &= inp[..., 0, :] | (any_unknown & ~any_zero)
for inp in ins: out[..., 1, :] &= inp[..., 1, :] & ~any_unknown
out[..., 0, :] &= inp[..., 0, :] | (any_unknown & ~any_zero) out[..., 2, :] |= inp[..., 2, :] & (~any_unknown | any_zero) & ~any_zero
out[..., 1, :] &= inp[..., 1, :] & ~any_unknown return out
else:
any_unknown = (ins[0][..., 0, :] ^ ins[0][..., 1, :]) & ~ins[0][..., 2, :]
for inp in ins[1:]: any_unknown |= (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :] def bp4v_xor(out, *ins):
any_zero = ~ins[0][..., 0, :] & ~ins[0][..., 1, :] & ~ins[0][..., 2, :]
for inp in ins[1:]: any_zero |= ~inp[..., 0, :] & ~inp[..., 1, :] & ~inp[..., 2, :]
out[..., 2, :] = 0
for inp in ins:
out[..., 0, :] &= inp[..., 0, :] | (any_unknown & ~any_zero)
out[..., 1, :] &= inp[..., 1, :] & ~any_unknown
out[..., 2, :] |= inp[..., 2, :] & (~any_unknown | any_zero) & ~any_zero
def bp_xor(out, *ins):
md = out.shape[-2]
for inp in ins: assert md == inp.shape[-2]
out[...] = 0 out[...] = 0
if md == 1: any_unknown = ins[0][..., 0, :] ^ ins[0][..., 1, :]
for inp in ins: out[..., 0, :] ^= inp[..., 0, :] for inp in ins[1:]: any_unknown |= inp[..., 0, :] ^ inp[..., 1, :]
elif md == 2: for inp in ins:
any_unknown = ins[0][..., 0, :] ^ ins[0][..., 1, :] out[..., 0, :] ^= inp[..., 0, :]
for inp in ins[1:]: any_unknown |= inp[..., 0, :] ^ inp[..., 1, :] out[..., 1, :] ^= inp[..., 1, :]
for inp in ins: out[...] ^= inp out[..., 0, :] |= any_unknown
out[..., 0, :] |= any_unknown out[..., 1, :] &= ~any_unknown
out[..., 1, :] &= ~any_unknown return out
else:
any_unknown = (ins[0][..., 0, :] ^ ins[0][..., 1, :]) & ~ins[0][..., 2, :]
for inp in ins[1:]: any_unknown |= (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :] def bp8v_xor(out, *ins):
for inp in ins: out[...] = 0
out[..., 0, :] ^= inp[..., 0, :] any_unknown = (ins[0][..., 0, :] ^ ins[0][..., 1, :]) & ~ins[0][..., 2, :]
out[..., 1, :] ^= inp[..., 1, :] for inp in ins[1:]: any_unknown |= (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :]
out[..., 2, :] |= inp[..., 2, :] for inp in ins:
out[..., 0, :] |= any_unknown out[..., 0, :] ^= inp[..., 0, :]
out[..., 1, :] &= ~any_unknown out[..., 1, :] ^= inp[..., 1, :]
out[..., 2, :] &= ~any_unknown out[..., 2, :] |= inp[..., 2, :]
out[..., 0, :] |= any_unknown
out[..., 1, :] &= ~any_unknown
def bp_latch(out, d, t, q_prev): out[..., 2, :] &= ~any_unknown
md = out.shape[-2] return out
assert md == d.shape[-2]
assert md == t.shape[-2]
assert md == q_prev.shape[-2] def bp8v_latch(out, d, t, q_prev):
if md == 1: any_unknown = (t[..., 0, :] ^ t[..., 1, :]) & ~t[..., 2, :]
out[...] = (d & t) | (q_prev & ~t) any_unknown |= ((d[..., 0, :] ^ d[..., 1, :]) & ~d[..., 2, :]) & (t[..., 0, :] | t[..., 1, :] | t[..., 2, :])
elif md == 2: out[..., 1, :] = (d[..., 1, :] & t[..., 1, :]) | (q_prev[..., 0, :] & ~t[..., 1, :])
any_unknown = t[..., 0, :] ^ t[..., 1, :] out[..., 0, :] = (d[..., 0, :] & t[..., 0, :]) | (out[..., 1, :] & ~t[..., 0, :])
any_unknown |= (d[..., 0, :] ^ d[..., 1, :]) & (t[..., 0, :] | t[..., 1, :]) out[..., 2, :] = out[..., 1, :] ^ out[..., 0, :]
out[...] = (d & t) | (q_prev & ~t) out[..., 0, :] |= any_unknown
out[..., 0, :] |= any_unknown out[..., 1, :] &= ~any_unknown
out[..., 1, :] &= ~any_unknown out[..., 2, :] &= ~any_unknown
else: return out
any_unknown = (t[..., 0, :] ^ t[..., 1, :]) & ~t[..., 2, :]
any_unknown |= ((d[..., 0, :] ^ d[..., 1, :]) & ~d[..., 2, :]) & (t[..., 0, :] | t[..., 1, :] | t[..., 2, :])
out[..., 1, :] = (d[..., 1, :] & t[..., 1, :]) | (q_prev[..., 0, :] & ~t[..., 1, :]) _bit_in_lut = np.array([2 ** x for x in range(7, -1, -1)], dtype='uint8')
out[..., 0, :] = (d[..., 0, :] & t[..., 0, :]) | (out[..., 1, :] & ~t[..., 0, :])
out[..., 2, :] = out[..., 1, :] ^ out[..., 0, :]
out[..., 0, :] |= any_unknown @numba.njit
out[..., 1, :] &= ~any_unknown def bit_in(a, pos):
out[..., 2, :] &= ~any_unknown return a[pos >> 3] & _bit_in_lut[pos & 7]
def unpackbits(a : np.ndarray):
"""Unpacks the bits of given ndarray ``a``.
Similar to ``np.unpackbits``, but accepts any dtype, preserves the shape of ``a`` and
adds a new last axis with the bits of each item. Bits are in 'little'-order, i.e.,
a[...,0] is the least significant bit of each item.
"""
return np.unpackbits(a.view(np.uint8), bitorder='little').reshape(*a.shape, 8*a.itemsize)
def packbits(a, dtype=np.uint8):
"""Packs the values of a boolean-valued array ``a`` along its last axis into bits.
Similar to ``np.packbits``, but returns an array of given dtype and the shape of ``a`` with the last axis removed.
The last axis of `a` is truncated or padded according to the bit-width of the given dtype.
Signed integer datatypes are padded with the most significant bit, all others are padded with `0`.
"""
dtype = np.dtype(dtype)
bits = 8 * dtype.itemsize
a = a[...,:bits]
if a.shape[-1] < bits:
p = [(0,0)]*(len(a.shape)-1) + [(0, bits-a.shape[-1])]
a = np.pad(a, p, 'edge') if dtype.name[0] == 'i' else np.pad(a, p, 'constant', constant_values=0)
return np.packbits(a, bitorder='little').view(dtype).reshape(a.shape[:-1])

475
src/kyupy/logic_sim.py

@ -1,7 +1,7 @@
"""A high-throughput combinational logic simulator. """A high-throughput combinational logic simulator.
The class :py:class:`~kyupy.logic_sim.LogicSim` performs parallel simulations of the combinational part of a circuit. The class :py:class:`~kyupy.logic_sim.LogicSim` performs parallel simulations of the combinational part of a circuit.
The logic operations are performed bit-parallel on packed numpy arrays. The logic operations are performed bit-parallel on packed numpy arrays (see bit-parallel (bp) array description in :py:mod:`~kyupy.logic`).
Simple sequential circuits can be simulated by repeated assignments and propagations. Simple sequential circuits can be simulated by repeated assignments and propagations.
However, this simulator ignores the clock network and simply assumes that all state-elements are clocked all the time. However, this simulator ignores the clock network and simply assumes that all state-elements are clocked all the time.
""" """
@ -10,127 +10,52 @@ import math
import numpy as np import numpy as np
from . import logic, hr_bytes from . import numba, logic, hr_bytes, sim
from .circuit import Circuit
class LogicSim(sim.SimOps):
class LogicSim:
"""A bit-parallel naïve combinational simulator for 2-, 4-, or 8-valued logic. """A bit-parallel naïve combinational simulator for 2-, 4-, or 8-valued logic.
:param circuit: The circuit to simulate. :param circuit: The circuit to simulate.
:type circuit: :py:class:`~kyupy.circuit.Circuit`
:param sims: The number of parallel logic simulations to perform. :param sims: The number of parallel logic simulations to perform.
:type sims: int
:param m: The arity of the logic, must be 2, 4, or 8. :param m: The arity of the logic, must be 2, 4, or 8.
:type m: int :param c_reuse: If True, intermediate signal values may get overwritten when not needed anymore to save memory.
:param strip_forks: If True, forks are not included in the simulation model to save memory and simulation time.
""" """
def __init__(self, circuit, sims=8, m=8): def __init__(self, circuit: Circuit, sims: int = 8, m: int = 8, c_reuse: bool = False, strip_forks: bool = False):
assert m in [2, 4, 8] assert m in [2, 4, 8]
super().__init__(circuit, c_reuse=c_reuse, strip_forks=strip_forks)
self.m = m self.m = m
mdim = math.ceil(math.log2(m)) self.mdim = math.ceil(math.log2(m))
self.circuit = circuit
self.sims = sims self.sims = sims
nbytes = (sims - 1) // 8 + 1 nbytes = (sims - 1) // 8 + 1
dffs = [n for n in circuit.nodes if 'dff' in n.kind.lower()]
latches = [n for n in circuit.nodes if 'latch' in n.kind.lower()]
self.interface = list(circuit.interface) + dffs + latches
self.width = len(self.interface)
"""The number of bits in the circuit state (number of ports + number of state-elements)."""
self.state = np.zeros((len(circuit.lines), mdim, nbytes), dtype='uint8')
self.state_epoch = np.zeros(len(circuit.nodes), dtype='int8') - 1
self.tmp = np.zeros((5, mdim, nbytes), dtype='uint8')
self.zero = np.zeros((mdim, nbytes), dtype='uint8')
self.epoch = 0
self.latch_dict = dict((n.index, i) for i, n in enumerate(latches))
self.latch_state = np.zeros((len(latches), mdim, nbytes), dtype='uint8')
known_fct = [(f[:-4], getattr(self, f)) for f in dir(self) if f.endswith('_fct')]
self.node_fct = []
for n in circuit.nodes:
t = n.kind.lower().replace('__fork__', 'fork')
t = t.replace('nbuff', 'fork')
t = t.replace('input', 'fork')
t = t.replace('output', 'fork')
t = t.replace('__const0__', 'const0')
t = t.replace('__const1__', 'const1')
t = t.replace('tieh', 'const1')
t = t.replace('ibuff', 'not')
t = t.replace('inv', 'not')
fcts = [f for n, f in known_fct if t.startswith(n)]
if len(fcts) < 1:
raise ValueError(f'Unknown node kind {n.kind}')
self.node_fct.append(fcts[0])
def __repr__(self): self.c = np.zeros((self.c_len, self.mdim, nbytes), dtype=np.uint8)
return f'<LogicSim {self.circuit.name} sims={self.sims} m={self.m} state_mem={hr_bytes(self.state.nbytes)}>' self.s = np.zeros((2, self.s_len, 3, nbytes), dtype=np.uint8)
"""Logic values of the sequential elements (flip-flops) and ports.
def assign(self, stimuli): It is a pair of arrays in bit-parallel (bp) storage format:
"""Assign stimuli to the primary inputs and state-elements (flip-flops).
:param stimuli: The input data to assign. Must be in bit-parallel storage format and in a compatible shape. * ``s[0]`` Assigned values. Simulator will read (P)PI value from here.
:type stimuli: :py:class:`~kyupy.logic.BPArray` * ``s[1]`` Result values. Simulator will write (P)PO values here.
:returns: The given stimuli object.
""" Access this array to assign new values to the (P)PIs or read values from the (P)POs.
for node, stim in zip(self.interface, stimuli.data if hasattr(stimuli, 'data') else stimuli):
if len(node.outs) == 0: continue
if node.index in self.latch_dict:
self.latch_state[self.latch_dict[node.index]] = stim
else:
outputs = [self.state[line] if line else self.tmp[3] for line in node.outs]
self.node_fct[node]([stim], outputs)
for line in node.outs:
if line is not None: self.state_epoch[line.reader] = self.epoch
for n in self.circuit.nodes:
if n.kind in ('__const1__', '__const0__'):
outputs = [self.state[line] if line else self.tmp[3] for line in n.outs]
self.node_fct[n]([], outputs)
for line in n.outs:
if line is not None: self.state_epoch[line.reader] = self.epoch
return stimuli
def capture(self, responses):
"""Capture the current values at the primary outputs and in the state-elements (flip-flops).
For primary outputs, the logic value is stored unmodified in the given target array.
For flip-flops, the logic value is constructed from the previous state and the new state.
:param responses: A bit-parallel storage target for the responses in a compatible shape.
:type responses: :py:class:`~kyupy.logic.BPArray`
:returns: The given responses object.
""" """
for node, resp in zip(self.interface, responses.data if hasattr(responses, 'data') else responses): self.s[:,:,1,:] = 255 # unassigned
if len(node.ins) == 0: continue
if node.index in self.latch_dict:
resp[...] = self.state[node.outs[0]]
else:
resp[...] = self.state[node.ins[0]]
# FIXME: unclear why we should use outs for DFFs
#if self.m > 2 and 'dff' in node.kind.lower() and len(node.outs) > 0:
# if node.outs[0] is None:
# resp[1, :] = ~self.state[node.outs[1], 0, :] # assume QN is connected, take inverse of that.
# else:
# resp[1, :] = self.state[node.outs[0], 0, :]
# if self.m > 4:
# resp[..., 2, :] = resp[..., 0, :] ^ resp[..., 1, :]
# # We don't handle X or - correctly.
return responses
def propagate(self, inject_cb=None):
"""Propagate the input values towards the outputs (Perform all logic operations in topological order).
If the circuit is sequential (it contains flip-flops), one call simulates one clock cycle. def __repr__(self):
Multiple clock cycles are simulated by a assign-propagate-capture loop: return f'{{name: "{self.circuit.name}", sims: {self.sims}, m: {self.m}, c_bytes: {self.c.nbytes}}}'
def s_to_c(self):
"""Copies the values from ``s[0]`` the inputs of the combinational portion.
"""
self.c[self.pippi_c_locs] = self.s[0, self.pippi_s_locs, :self.mdim]
.. code-block:: python def c_prop(self, inject_cb=None):
"""Propagate the input values through the combinational circuit towards the outputs.
# initial state in state_bp Performs all logic operations in topological order.
for cycle in range(10): # simulate 10 clock cycles If the circuit is sequential (it contains flip-flops), one call simulates one clock cycle.
sim.assign(state_bp)
sim.propagate()
sim.capture(state_bp)
:param inject_cb: A callback function for manipulating intermediate signal values. :param inject_cb: A callback function for manipulating intermediate signal values.
This function is called with a line and its new logic values (in bit-parallel format) after This function is called with a line and its new logic values (in bit-parallel format) after
@ -138,83 +63,273 @@ class LogicSim:
resumes with the manipulated values after the callback returns. resumes with the manipulated values after the callback returns.
:type inject_cb: ``f(Line, ndarray)`` :type inject_cb: ``f(Line, ndarray)``
""" """
for node in self.circuit.topological_order(): t0 = self.c_locs[self.tmp_idx]
if self.state_epoch[node] != self.epoch: continue t1 = self.c_locs[self.tmp2_idx]
inputs = [self.state[line] if line else self.zero for line in node.ins] if self.m == 2:
outputs = [self.state[line] if line else self.tmp[3] for line in node.outs] if inject_cb is None:
if node.index in self.latch_dict: _prop_cpu(self.ops, self.c_locs, self.c)
inputs.append(self.latch_state[self.latch_dict[node.index]]) else:
self.node_fct[node](inputs, outputs) for op, o0, i0, i1, i2, i3 in self.ops[:,:6]:
for line in node.outs: o0, i0, i1, i2, i3 = [self.c_locs[x] for x in (o0, i0, i1, i2, i3)]
if inject_cb is not None: inject_cb(line, self.state[line]) if op == sim.BUF1: self.c[o0]=self.c[i0]
self.state_epoch[line.reader] = self.epoch elif op == sim.INV1: self.c[o0] = ~self.c[i0]
self.epoch = (self.epoch + 1) % 128 elif op == sim.AND2: self.c[o0] = self.c[i0] & self.c[i1]
elif op == sim.AND3: self.c[o0] = self.c[i0] & self.c[i1] & self.c[i2]
def cycle(self, state, inject_cb=None): elif op == sim.AND4: self.c[o0] = self.c[i0] & self.c[i1] & self.c[i2] & self.c[i3]
"""Assigns the given state, propagates it and captures the new state. elif op == sim.NAND2: self.c[o0] = ~(self.c[i0] & self.c[i1])
elif op == sim.NAND3: self.c[o0] = ~(self.c[i0] & self.c[i1] & self.c[i2])
:param state: A bit-parallel array in a compatible shape holding the current circuit state. elif op == sim.NAND4: self.c[o0] = ~(self.c[i0] & self.c[i1] & self.c[i2] & self.c[i3])
The contained data is assigned to the PI and PPI and overwritten by data at the PO and PPO after elif op == sim.OR2: self.c[o0] = self.c[i0] | self.c[i1]
propagation. elif op == sim.OR3: self.c[o0] = self.c[i0] | self.c[i1] | self.c[i2]
:type state: :py:class:`~kyupy.logic.BPArray` elif op == sim.OR4: self.c[o0] = self.c[i0] | self.c[i1] | self.c[i2] | self.c[i3]
:param inject_cb: A callback function for manipulating intermediate signal values. See :py:func:`propagate`. elif op == sim.NOR2: self.c[o0] = ~(self.c[i0] | self.c[i1])
:returns: The given state object. elif op == sim.NOR3: self.c[o0] = ~(self.c[i0] | self.c[i1] | self.c[i2])
elif op == sim.NOR4: self.c[o0] = ~(self.c[i0] | self.c[i1] | self.c[i2] | self.c[i3])
elif op == sim.XOR2: self.c[o0] = self.c[i0] ^ self.c[i1]
elif op == sim.XOR3: self.c[o0] = self.c[i0] ^ self.c[i1] ^ self.c[i2]
elif op == sim.XOR4: self.c[o0] = self.c[i0] ^ self.c[i1] ^ self.c[i2] ^ self.c[i3]
elif op == sim.XNOR2: self.c[o0] = ~(self.c[i0] ^ self.c[i1])
elif op == sim.XNOR3: self.c[o0] = ~(self.c[i0] ^ self.c[i1] ^ self.c[i2])
elif op == sim.XNOR4: self.c[o0] = ~(self.c[i0] ^ self.c[i1] ^ self.c[i2] ^ self.c[i3])
elif op == sim.AO21: self.c[o0] = (self.c[i0] & self.c[i1]) | self.c[i2]
elif op == sim.AOI21: self.c[o0] = ~((self.c[i0] & self.c[i1]) | self.c[i2])
elif op == sim.OA21: self.c[o0] = (self.c[i0] | self.c[i1]) & self.c[i2]
elif op == sim.OAI21: self.c[o0] = ~((self.c[i0] | self.c[i1]) & self.c[i2])
elif op == sim.AO22: self.c[o0] = (self.c[i0] & self.c[i1]) | (self.c[i2] & self.c[i3])
elif op == sim.AOI22: self.c[o0] = ~((self.c[i0] & self.c[i1]) | (self.c[i2] & self.c[i3]))
elif op == sim.OA22: self.c[o0] = (self.c[i0] | self.c[i1]) & (self.c[i2] | self.c[i3])
elif op == sim.OAI22: self.c[o0] = ~((self.c[i0] | self.c[i1]) & (self.c[i2] | self.c[i3]))
elif op == sim.AO211: self.c[o0] = (self.c[i0] & self.c[i1]) | self.c[i2] | self.c[i3]
elif op == sim.AOI211:self.c[o0] = ~((self.c[i0] & self.c[i1]) | self.c[i2] | self.c[i3])
elif op == sim.OA211: self.c[o0] = (self.c[i0] | self.c[i1]) & self.c[i2] & self.c[i3]
elif op == sim.OAI211:self.c[o0] = ~((self.c[i0] | self.c[i1]) & self.c[i2] & self.c[i3])
elif op == sim.MUX21: self.c[o0] = (self.c[i0] & ~self.c[i2]) | (self.c[i1] & self.c[i2])
else: print(f'unknown op {op}')
inject_cb(o0, self.s[o0])
elif self.m == 4:
for op, o0, i0, i1, i2, i3 in self.ops[:,:6]:
o0, i0, i1, i2, i3 = [self.c_locs[x] for x in (o0, i0, i1, i2, i3)]
if op == sim.BUF1: self.c[o0]=self.c[i0]
elif op == sim.INV1: logic.bp4v_not(self.c[o0], self.c[i0])
elif op == sim.AND2: logic.bp4v_and(self.c[o0], self.c[i0], self.c[i1])
elif op == sim.AND3: logic.bp4v_and(self.c[o0], self.c[i0], self.c[i1], self.c[i2])
elif op == sim.AND4: logic.bp4v_and(self.c[o0], self.c[i0], self.c[i1], self.c[i2], self.c[i3])
elif op == sim.NAND2: logic.bp4v_and(self.c[o0], self.c[i0], self.c[i1]); logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.NAND3: logic.bp4v_and(self.c[o0], self.c[i0], self.c[i1], self.c[i2]); logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.NAND4: logic.bp4v_and(self.c[o0], self.c[i0], self.c[i1], self.c[i2], self.c[i3]); logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.OR2: logic.bp4v_or(self.c[o0], self.c[i0], self.c[i1])
elif op == sim.OR3: logic.bp4v_or(self.c[o0], self.c[i0], self.c[i1], self.c[i2])
elif op == sim.OR4: logic.bp4v_or(self.c[o0], self.c[i0], self.c[i1], self.c[i2], self.c[i3])
elif op == sim.NOR2: logic.bp4v_or(self.c[o0], self.c[i0], self.c[i1]); logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.NOR3: logic.bp4v_or(self.c[o0], self.c[i0], self.c[i1], self.c[i2]); logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.NOR4: logic.bp4v_or(self.c[o0], self.c[i0], self.c[i1], self.c[i2], self.c[i3]); logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.XOR2: logic.bp4v_xor(self.c[o0], self.c[i0], self.c[i1])
elif op == sim.XOR3: logic.bp4v_xor(self.c[o0], self.c[i0], self.c[i1], self.c[i2])
elif op == sim.XOR4: logic.bp4v_xor(self.c[o0], self.c[i0], self.c[i1], self.c[i2], self.c[i3])
elif op == sim.XNOR2: logic.bp4v_xor(self.c[o0], self.c[i0], self.c[i1]); logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.XNOR3: logic.bp4v_xor(self.c[o0], self.c[i0], self.c[i1], self.c[i2]); logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.XNOR4: logic.bp4v_xor(self.c[o0], self.c[i0], self.c[i1], self.c[i2], self.c[i3]); logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.AO21:
logic.bp4v_and(self.c[t0], self.c[i0], self.c[i1])
logic.bp4v_or(self.c[o0], self.c[t0], self.c[i2])
elif op == sim.AOI21:
logic.bp4v_and(self.c[t0], self.c[i0], self.c[i1])
logic.bp4v_or(self.c[o0], self.c[t0], self.c[i2])
logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.OA21:
logic.bp4v_or(self.c[t0], self.c[i0], self.c[i1])
logic.bp4v_and(self.c[o0], self.c[t0], self.c[i2])
elif op == sim.OAI21:
logic.bp4v_or(self.c[t0], self.c[i0], self.c[i1])
logic.bp4v_and(self.c[o0], self.c[t0], self.c[i2])
logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.AO22:
logic.bp4v_and(self.c[t0], self.c[i0], self.c[i1])
logic.bp4v_and(self.c[t1], self.c[i2], self.c[i3])
logic.bp4v_or(self.c[o0], self.c[t0], self.c[t1])
elif op == sim.AOI22:
logic.bp4v_and(self.c[t0], self.c[i0], self.c[i1])
logic.bp4v_and(self.c[t1], self.c[i2], self.c[i3])
logic.bp4v_or(self.c[o0], self.c[t0], self.c[t1])
logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.OA22:
logic.bp4v_or(self.c[t0], self.c[i0], self.c[i1])
logic.bp4v_or(self.c[t1], self.c[i2], self.c[i3])
logic.bp4v_and(self.c[o0], self.c[t0], self.c[t1])
elif op == sim.OAI22:
logic.bp4v_or(self.c[t0], self.c[i0], self.c[i1])
logic.bp4v_or(self.c[t1], self.c[i2], self.c[i3])
logic.bp4v_and(self.c[o0], self.c[t0], self.c[t1])
logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.AO211:
logic.bp4v_and(self.c[t0], self.c[i0], self.c[i1])
logic.bp4v_or(self.c[o0], self.c[t0], self.c[i2], self.c[i3])
elif op == sim.AOI211:
logic.bp4v_and(self.c[t0], self.c[i0], self.c[i1])
logic.bp4v_or(self.c[o0], self.c[t0], self.c[i2], self.c[i3])
logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.OA211:
logic.bp4v_or(self.c[t0], self.c[i0], self.c[i1])
logic.bp4v_and(self.c[o0], self.c[t0], self.c[i2], self.c[i3])
elif op == sim.OAI211:
logic.bp4v_or(self.c[t0], self.c[i0], self.c[i1])
logic.bp4v_and(self.c[o0], self.c[t0], self.c[i2], self.c[i3])
logic.bp4v_not(self.c[o0], self.c[o0])
elif op == sim.MUX21:
logic.bp4v_not(self.c[t1], self.c[i2])
logic.bp4v_and(self.c[t0], self.c[i0], self.c[t1])
logic.bp4v_and(self.c[t1], self.c[i1], self.c[i2])
logic.bp4v_or(self.c[o0], self.c[t0], self.c[t1])
else: print(f'unknown op {op}')
else:
for op, o0, i0, i1, i2, i3 in self.ops[:,:6]:
o0, i0, i1, i2, i3 = [self.c_locs[x] for x in (o0, i0, i1, i2, i3)]
if op == sim.BUF1: self.c[o0]=self.c[i0]
elif op == sim.INV1: logic.bp8v_not(self.c[o0], self.c[i0])
elif op == sim.AND2: logic.bp8v_and(self.c[o0], self.c[i0], self.c[i1])
elif op == sim.AND3: logic.bp8v_and(self.c[o0], self.c[i0], self.c[i1], self.c[i2])
elif op == sim.AND4: logic.bp8v_and(self.c[o0], self.c[i0], self.c[i1], self.c[i2], self.c[i3])
elif op == sim.NAND2: logic.bp8v_and(self.c[o0], self.c[i0], self.c[i1]); logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.NAND3: logic.bp8v_and(self.c[o0], self.c[i0], self.c[i1], self.c[i2]); logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.NAND4: logic.bp8v_and(self.c[o0], self.c[i0], self.c[i1], self.c[i2], self.c[i3]); logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.OR2: logic.bp8v_or(self.c[o0], self.c[i0], self.c[i1])
elif op == sim.OR3: logic.bp8v_or(self.c[o0], self.c[i0], self.c[i1], self.c[i2])
elif op == sim.OR4: logic.bp8v_or(self.c[o0], self.c[i0], self.c[i1], self.c[i2], self.c[i3])
elif op == sim.NOR2: logic.bp8v_or(self.c[o0], self.c[i0], self.c[i1]); logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.NOR3: logic.bp8v_or(self.c[o0], self.c[i0], self.c[i1], self.c[i2]); logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.NOR4: logic.bp8v_or(self.c[o0], self.c[i0], self.c[i1], self.c[i2], self.c[i3]); logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.XOR2: logic.bp8v_xor(self.c[o0], self.c[i0], self.c[i1])
elif op == sim.XOR3: logic.bp8v_xor(self.c[o0], self.c[i0], self.c[i1], self.c[i2])
elif op == sim.XOR4: logic.bp8v_xor(self.c[o0], self.c[i0], self.c[i1], self.c[i2], self.c[i3])
elif op == sim.XNOR2: logic.bp8v_xor(self.c[o0], self.c[i0], self.c[i1]); logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.XNOR3: logic.bp8v_xor(self.c[o0], self.c[i0], self.c[i1], self.c[i2]); logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.XNOR4: logic.bp8v_xor(self.c[o0], self.c[i0], self.c[i1], self.c[i2], self.c[i3]); logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.AO21:
logic.bp8v_and(self.c[t0], self.c[i0], self.c[i1])
logic.bp8v_or(self.c[o0], self.c[t0], self.c[i2])
elif op == sim.AOI21:
logic.bp8v_and(self.c[t0], self.c[i0], self.c[i1])
logic.bp8v_or(self.c[o0], self.c[t0], self.c[i2])
logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.OA21:
logic.bp8v_or(self.c[t0], self.c[i0], self.c[i1])
logic.bp8v_and(self.c[o0], self.c[t0], self.c[i2])
elif op == sim.OAI21:
logic.bp8v_or(self.c[t0], self.c[i0], self.c[i1])
logic.bp8v_and(self.c[o0], self.c[t0], self.c[i2])
logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.AO22:
logic.bp8v_and(self.c[t0], self.c[i0], self.c[i1])
logic.bp8v_and(self.c[t1], self.c[i2], self.c[i3])
logic.bp8v_or(self.c[o0], self.c[t0], self.c[t1])
elif op == sim.AOI22:
logic.bp8v_and(self.c[t0], self.c[i0], self.c[i1])
logic.bp8v_and(self.c[t1], self.c[i2], self.c[i3])
logic.bp8v_or(self.c[o0], self.c[t0], self.c[t1])
logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.OA22:
logic.bp8v_or(self.c[t0], self.c[i0], self.c[i1])
logic.bp8v_or(self.c[t1], self.c[i2], self.c[i3])
logic.bp8v_and(self.c[o0], self.c[t0], self.c[t1])
elif op == sim.OAI22:
logic.bp8v_or(self.c[t0], self.c[i0], self.c[i1])
logic.bp8v_or(self.c[t1], self.c[i2], self.c[i3])
logic.bp8v_and(self.c[o0], self.c[t0], self.c[t1])
logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.AO211:
logic.bp8v_and(self.c[t0], self.c[i0], self.c[i1])
logic.bp8v_or(self.c[o0], self.c[t0], self.c[i2], self.c[i3])
elif op == sim.AOI211:
logic.bp8v_and(self.c[t0], self.c[i0], self.c[i1])
logic.bp8v_or(self.c[o0], self.c[t0], self.c[i2], self.c[i3])
logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.OA211:
logic.bp8v_or(self.c[t0], self.c[i0], self.c[i1])
logic.bp8v_and(self.c[o0], self.c[t0], self.c[i2], self.c[i3])
elif op == sim.OAI211:
logic.bp8v_or(self.c[t0], self.c[i0], self.c[i1])
logic.bp8v_and(self.c[o0], self.c[t0], self.c[i2], self.c[i3])
logic.bp8v_not(self.c[o0], self.c[o0])
elif op == sim.MUX21:
logic.bp8v_not(self.c[t1], self.c[i2])
logic.bp8v_and(self.c[t0], self.c[i0], self.c[t1])
logic.bp8v_and(self.c[t1], self.c[i1], self.c[i2])
logic.bp8v_or(self.c[o0], self.c[t0], self.c[t1])
else: print(f'unknown op {op}')
if inject_cb is not None: inject_cb(o0, self.s[o0])
def c_to_s(self):
"""Copies (captures) the results of the combinational portion to ``s[1]``.
""" """
self.assign(state) self.s[1, self.poppo_s_locs, :self.mdim] = self.c[self.poppo_c_locs]
self.propagate(inject_cb) if self.mdim == 1:
return self.capture(state) self.s[1, self.poppo_s_locs, 1:2] = self.c[self.poppo_c_locs]
def fork_fct(self, inputs, outputs):
for o in outputs: o[...] = inputs[0]
def const0_fct(self, _, outputs): def s_ppo_to_ppi(self):
for o in outputs: o[...] = 0 """Constructs a new assignment based on the current data in ``s``.
def const1_fct(self, _, outputs): Use this function for simulating consecutive clock cycles.
for o in outputs:
o[...] = 0
logic.bp_not(o, o)
def not_fct(self, inputs, outputs): For 2-valued or 4-valued simulations, all valued from PPOs (in ``s[1]``) and copied to the PPIs (in ``s[0]``).
logic.bp_not(outputs[0], inputs[0]) For 8-valued simulations, PPI transitions are constructed from the final values of the assignment (in ``s[0]``) and the
final values of the results (in ``s[1]``).
def and_fct(self, inputs, outputs): """
logic.bp_and(outputs[0], *inputs) # TODO: handle latches correctly
if self.mdim < 3:
def or_fct(self, inputs, outputs): self.s[0, self.ppio_s_locs] = self.s[1, self.ppio_s_locs]
logic.bp_or(outputs[0], *inputs) else:
self.s[0, self.ppio_s_locs, 1] = self.s[0, self.ppio_s_locs, 0] # initial value is previously assigned final value
def xor_fct(self, inputs, outputs): self.s[0, self.ppio_s_locs, 0] = self.s[1, self.ppio_s_locs, 0] # final value is newly captured final value
logic.bp_xor(outputs[0], *inputs) self.s[0, self.ppio_s_locs, 2] = self.s[0, self.ppio_s_locs, 0] ^ self.s[0, self.ppio_s_locs, 1] # TODO: not correct for X, -
def sdff_fct(self, inputs, outputs): def cycle(self, cycles: int = 1, inject_cb=None):
logic.bp_buf(outputs[0], inputs[0]) """Repeatedly assigns a state, propagates it, captures the new state, and transfers PPOs to PPIs.
if len(outputs) > 1:
logic.bp_not(outputs[1], inputs[0]) :param cycles: The number of cycles to simulate.
:param inject_cb: A callback function for manipulating intermediate signal values. See :py:func:`c_prop`.
def dff_fct(self, inputs, outputs): """
logic.bp_buf(outputs[0], inputs[0]) for _ in range(cycles):
if len(outputs) > 1: self.s_to_c()
logic.bp_not(outputs[1], inputs[0]) self.c_prop(inject_cb)
self.c_to_s()
def latch_fct(self, inputs, outputs): self.s_ppo_to_ppi()
logic.bp_latch(outputs[0], inputs[0], inputs[1], inputs[2])
if len(outputs) > 1:
logic.bp_not(outputs[1], inputs[0]) @numba.njit
def _prop_cpu(ops, c_locs, c):
def nand_fct(self, inputs, outputs): for op, o0, i0, i1, i2, i3 in ops[:,:6]:
logic.bp_and(outputs[0], *inputs) o0, i0, i1, i2, i3 = [c_locs[x] for x in (o0, i0, i1, i2, i3)]
logic.bp_not(outputs[0], outputs[0]) if op == sim.BUF1: c[o0]=c[i0]
elif op == sim.INV1: c[o0] = ~c[i0]
def nor_fct(self, inputs, outputs): elif op == sim.AND2: c[o0] = c[i0] & c[i1]
logic.bp_or(outputs[0], *inputs) elif op == sim.AND3: c[o0] = c[i0] & c[i1] & c[i2]
logic.bp_not(outputs[0], outputs[0]) elif op == sim.AND4: c[o0] = c[i0] & c[i1] & c[i2] & c[i3]
elif op == sim.NAND2: c[o0] = ~(c[i0] & c[i1])
def xnor_fct(self, inputs, outputs): elif op == sim.NAND3: c[o0] = ~(c[i0] & c[i1] & c[i2])
logic.bp_xor(outputs[0], *inputs) elif op == sim.NAND4: c[o0] = ~(c[i0] & c[i1] & c[i2] & c[i3])
logic.bp_not(outputs[0], outputs[0]) elif op == sim.OR2: c[o0] = c[i0] | c[i1]
elif op == sim.OR3: c[o0] = c[i0] | c[i1] | c[i2]
def aoi21_fct(self, inputs, outputs): elif op == sim.OR4: c[o0] = c[i0] | c[i1] | c[i2] | c[i3]
logic.bp_and(self.tmp[0], inputs[0], inputs[1]) elif op == sim.NOR2: c[o0] = ~(c[i0] | c[i1])
logic.bp_or(outputs[0], self.tmp[0], inputs[2]) elif op == sim.NOR3: c[o0] = ~(c[i0] | c[i1] | c[i2])
logic.bp_not(outputs[0], outputs[0]) elif op == sim.NOR4: c[o0] = ~(c[i0] | c[i1] | c[i2] | c[i3])
elif op == sim.XOR2: c[o0] = c[i0] ^ c[i1]
elif op == sim.XOR3: c[o0] = c[i0] ^ c[i1] ^ c[i2]
elif op == sim.XOR4: c[o0] = c[i0] ^ c[i1] ^ c[i2] ^ c[i3]
elif op == sim.XNOR2: c[o0] = ~(c[i0] ^ c[i1])
elif op == sim.XNOR3: c[o0] = ~(c[i0] ^ c[i1] ^ c[i2])
elif op == sim.XNOR4: c[o0] = ~(c[i0] ^ c[i1] ^ c[i2] ^ c[i3])
elif op == sim.AO21: c[o0] = (c[i0] & c[i1]) | c[i2]
elif op == sim.OA21: c[o0] = (c[i0] | c[i1]) & c[i2]
elif op == sim.AO22: c[o0] = (c[i0] & c[i1]) | (c[i2] & c[i3])
elif op == sim.OA22: c[o0] = (c[i0] | c[i1]) & (c[i2] | c[i3])
elif op == sim.AOI21: c[o0] = ~((c[i0] & c[i1]) | c[i2])
elif op == sim.OAI21: c[o0] = ~((c[i0] | c[i1]) & c[i2])
elif op == sim.AOI22: c[o0] = ~((c[i0] & c[i1]) | (c[i2] & c[i3]))
elif op == sim.OAI22: c[o0] = ~((c[i0] | c[i1]) & (c[i2] | c[i3]))
elif op == sim.AO211: c[o0] = (c[i0] & c[i1]) | c[i2] | c[i3]
elif op == sim.OA211: c[o0] = (c[i0] | c[i1]) & c[i2] & c[i3]
elif op == sim.AOI211: c[o0] = ~((c[i0] & c[i1]) | c[i2] | c[i3])
elif op == sim.OAI211: c[o0] = ~((c[i0] | c[i1]) & c[i2] & c[i3])
elif op == sim.MUX21: c[o0] = (c[i0] & ~c[i2]) | (c[i1] & c[i2])
else: print(f'unknown op {op}')

227
src/kyupy/sdf.py

@ -1,11 +1,10 @@
"""A simple and incomplete parser for the Standard Delay Format (SDF). """A simple and incomplete parser for the Standard Delay Format (SDF).
The main purpose of this parser is to extract pin-to-pin delay and interconnect delay information from SDF files. This parser extracts pin-to-pin delay and interconnect delay information from SDF files.
Sophisticated timing specifications (timing checks, conditional delays, etc.) are currently not supported. Sophisticated timing specifications (timing checks, conditional delays, etc.) are ignored.
The functions :py:func:`load` and :py:func:`read` return an intermediate representation (:class:`DelayFile` object).
Call :py:func:`DelayFile.annotation` to match the intermediate representation to a given circuit.
The functions :py:func:`parse` and :py:func:`load` return an intermediate representation (:class:`DelayFile` object).
Call :py:func:`DelayFile.iopaths` and :py:func:`DelayFile.interconnects` to generate delay information for a given circuit.
""" """
from collections import namedtuple from collections import namedtuple
@ -15,6 +14,7 @@ import numpy as np
from lark import Lark, Transformer from lark import Lark, Transformer
from . import log, readtext from . import log, readtext
from .circuit import Circuit
from .techlib import TechLib from .techlib import TechLib
@ -27,145 +27,112 @@ class DelayFile:
""" """
def __init__(self, name, cells): def __init__(self, name, cells):
self.name = name self.name = name
if None in cells: self._interconnects = cells.get(None, None)
self.interconnects = cells[None]
else:
self.interconnects = None
self.cells = dict((n, l) for n, l in cells.items() if n) self.cells = dict((n, l) for n, l in cells.items() if n)
def __repr__(self): def __repr__(self):
return '\n'.join(f'{n}: {l}' for n, l in self.cells.items()) + '\n' + \ return '\n'.join(f'{n}: {l}' for n, l in self.cells.items()) + '\n' + \
'\n'.join(str(i) for i in self.interconnects) '\n'.join(str(i) for i in self._interconnects)
def annotation(self, circuit, tlib=TechLib(), dataset=1, interconnect=True, ffdelays=True): def iopaths(self, circuit:Circuit, tlib:TechLib):
"""Constructs an 3-dimensional ndarray with timing data for each line in ``circuit``. """Constructs an ndarray containing all IOPATH delays.
An IOPATH delay for a node is annotated to the line connected to the input pin specified in the IOPATH. All IOPATH delays for a node ``n`` are annotated to the line connected to the input pin specified in the IOPATH.
Currently, only ABSOLUTE IOPATH and INTERCONNECT delays are supported. Limited support of SDF spec:
Pulse rejection limits are derived from absolute delays, explicit declarations (PATHPULSE etc.) are ignored.
* Only ABSOLUTE delay values are supported.
:param circuit: The circuit to annotate. Names from the STIL file are matched to the node names. * Only two delvals per delval_list is supported. First is rising/posedge, second is falling/negedge
:type circuit: :class:`~kyupy.circuit.Circuit` transition at the output of the IOPATH (SDF spec, pp. 3-17).
:param tlib: A technology library object that provides pin name mappings. * PATHPULSE declarations are ignored.
:type tlib: :py:class:`~kyupy.techlib.TechLib`
:param dataset: SDFs store multiple values for each delay (e.g. minimum, typical, maximum). The axes convention of KyuPy's delay data arrays is as follows:
An integer selects the dataset to use (default is 1 for 'typical').
If a tuple is given, the annotator will calculate the average of multiple datasets. * Axis 0: dataset (usually 3 datasets per SDF-file)
:type dataset: ``int`` or ``tuple`` * Axis 1: line index (e.g. ``n.ins[0]``, ``n.ins[1]``)
:param interconnect: Whether or not to include the delays of interconnects in the annotation. * Axis 2: polarity of the transition at the IOPATH-input (e.g. at ``n.ins[0]`` or ``n.ins[1]``), 0='rising/posedge', 1='falling/negedge'
To properly annotate interconnect delays, the circuit model has to include a '__fork__' node on * Axis 3: polarity of the transition at the IOPATH-output (at ``n.outs[0]``), 0='rising/posedge', 1='falling/negedge'
every signal and every fanout-branch. The Verilog parser aids in this by setting the parameter
`branchforks=True` in :py:func:`kyupy.verilog.parse`.
:type interconnect: ``bool``
:param ffdelays: Whether or not to include the delays of flip-flops in the annotation.
:type ffdelays: ``bool``
:return: A 3-dimensional ndarray with timing data.
* Axis 0: line index.
* Axis 1: type of timing data: 0='delay', 1='pulse rejection limit'.
* Axis 2: The polarity of the output transition of the reading node: 0='rising', 1='falling'.
The polarity for pulse rejection is determined by the latter transition of the pulse.
E.g., ``timing[42, 1, 0]`` is the rejection limit of a negative pulse at the output
of the reader of line 42.
""" """
def select_del(_delvals, idx):
if isinstance(dataset, tuple): def find_cell(name:str):
return sum(_delvals[idx][d] for d in dataset) / len(dataset) if name not in circuit.cells: name = name.replace('\\', '')
return _delvals[idx][dataset] if name not in circuit.cells: name = name.replace('[', '_').replace(']', '_')
return circuit.cells.get(name, None)
def find_cell(name):
if name not in circuit.cells: delays = np.zeros((len(circuit.lines), 2, 2, 3)) # dataset last during construction.
name = name.replace('\\', '')
if name not in circuit.cells: for name, iopaths in self.cells.items():
name = name.replace('[', '_').replace(']', '_') name = name.replace('\\', '')
if name not in circuit.cells: if cell := circuit.cells.get(name, None):
return None for i_pin_spec, o_pin_spec, *dels in iopaths:
return circuit.cells[name] if i_pin_spec.startswith('(posedge '): i_pol_idxs = [0]
elif i_pin_spec.startswith('(negedge '): i_pol_idxs = [1]
timing = np.zeros((len(circuit.lines), 2, 2)) else: i_pol_idxs = [0, 1]
for cn, iopaths in self.cells.items(): i_pin_spec = re.sub(r'\((neg|pos)edge ([^)]+)\)', r'\2', i_pin_spec)
for ipn, opn, *delvals in iopaths: if line := cell.ins[tlib.pin_index(cell.kind, i_pin_spec)]:
delvals = [d if len(d) > 0 else [0, 0, 0] for d in delvals] delays[line, i_pol_idxs] = [d if len(d) > 0 else [0, 0, 0] for d in dels]
if max(max(delvals)) == 0:
continue
cell = find_cell(cn)
if cell is None:
#log.warn(f'Cell from SDF not found in circuit: {cn}')
continue
ipn = re.sub(r'\((neg|pos)edge ([^)]+)\)', r'\2', ipn)
ipin = tlib.pin_index(cell.kind, ipn)
opin = tlib.pin_index(cell.kind, opn)
kind = cell.kind.lower()
def add_delays(_line):
if _line is not None:
timing[_line, :, 0] += select_del(delvals, 0)
timing[_line, :, 1] += select_del(delvals, 1)
take_avg = False
if kind.startswith('sdff'):
if not ipn.startswith('CLK'):
continue
if ffdelays and (len(cell.outs) > opin):
add_delays(cell.outs[opin])
else:
if ipin < len(cell.ins):
if kind.startswith(('xor', 'xnor')):
# print(ipn, ipin, times[cell.i_lines[ipin], 0, 0])
take_avg = timing[cell.ins[ipin]].sum() > 0
add_delays(cell.ins[ipin])
if take_avg:
timing[cell.ins[ipin]] /= 2
else: else:
log.warn(f'No line to annotate pin {ipn} of {cell}') log.warn(f'No line to annotate in circuit: {i_pin_spec} for {cell}')
else:
log.warn(f'Name from SDF not found in circuit: {name}')
return np.moveaxis(delays, -1, 0)
def interconnects(self, circuit:Circuit, tlib:TechLib):
"""Constructs an ndarray containing all INTERCONNECT delays.
To properly annotate interconnect delays, the circuit model has to include a '__fork__' node on
every signal and every fanout-branch. The Verilog parser aids in this by setting the parameter
`branchforks=True` in :py:func:`~kyupy.verilog.parse` or :py:func:`~kyupy.verilog.load`.
if not interconnect or self.interconnects is None: Limited support of SDF spec:
return timing
for n1, n2, *delvals in self.interconnects: * Only ABSOLUTE delay values are supported.
* Only two delvals per delval_list is supported. First is rising/posedge, second is falling/negedge
transition.
* PATHPULSE declarations are ignored.
The axes convention of KyuPy's delay data arrays is as follows:
* Axis 0: dataset (usually 3 datasets per SDF-file)
* Axis 1: line index. Usually input line of a __fork__.
* Axis 2: (axis of size 2 for compatability to IOPATH results. Values are broadcast along this axis.)
* Axis 3: polarity of the transition, 0='rising/posedge', 1='falling/negedge'
"""
delays = np.zeros((len(circuit.lines), 2, 2, 3)) # dataset last during construction.
for n1, n2, *delvals in self._interconnects:
delvals = [d if len(d) > 0 else [0, 0, 0] for d in delvals] delvals = [d if len(d) > 0 else [0, 0, 0] for d in delvals]
if max(max(delvals)) == 0: if max(max(delvals)) == 0: continue
cn1, pn1 = n1.split('/') if '/' in n1 else (n1, None)
cn2, pn2 = n2.split('/') if '/' in n2 else (n2, None)
cn1 = cn1.replace('\\','')
cn2 = cn2.replace('\\','')
c1, c2 = circuit.cells[cn1], circuit.cells[cn2]
p1 = tlib.pin_index(c1.kind, pn1) if pn1 is not None else 0
p2 = tlib.pin_index(c2.kind, pn2) if pn2 is not None else 0
if len(c1.outs) <= p1 or c1.outs[p1] is None:
log.warn(f'No line to annotate pin {pn1} of {c1}')
continue continue
if '/' in n1: if len(c2.ins) <= p2 or c2.ins[p2] is None:
i = n1.rfind('/')
cn1 = n1[0:i]
pn1 = n1[i+1:]
else:
cn1, pn1 = (n1, 'Z')
if '/' in n2:
i = n2.rfind('/')
cn2 = n2[0:i]
pn2 = n2[i+1:]
else:
cn2, pn2 = (n2, 'IN')
c1 = find_cell(cn1)
if c1 is None:
#log.warn(f'Cell from SDF not found in circuit: {cn1}')
continue
c2 = find_cell(cn2)
if c2 is None:
#log.warn(f'Cell from SDF not found in circuit: {cn2}')
continue
p1, p2 = tlib.pin_index(c1.kind, pn1), tlib.pin_index(c2.kind, pn2)
line = None
if len(c2.ins) <= p2:
log.warn(f'No line to annotate pin {pn2} of {c2}') log.warn(f'No line to annotate pin {pn2} of {c2}')
continue continue
f1, f2 = c1.outs[p1].reader, c2.ins[p2].driver f1, f2 = c1.outs[p1].reader, c2.ins[p2].driver # find the forks between cells.
if f1 != f2: # possible branchfork assert f1.kind == '__fork__'
assert len(f2.ins) == 1 assert f2.kind == '__fork__'
if f1 != f2: # at least two forks, make sure f2 is a branchfork connected to f1
assert len(f2.outs) == 1
assert f1.outs[f2.ins[0].driver_pin] == f2.ins[0]
line = f2.ins[0] line = f2.ins[0]
assert f1.outs[f2.ins[0].driver_pin] == line elif len(f2.outs) == 1: # f1==f2, only OK when there is no fanout.
elif len(f2.outs) == 1: # no fanout?
line = f2.ins[0] line = f2.ins[0]
if line is not None:
timing[line, :, 0] += select_del(delvals, 0)
timing[line, :, 1] += select_del(delvals, 1)
else: else:
log.warn(f'No branchfork for annotating interconnect delay {c1.name}/{p1}->{c2.name}/{p2}') log.warn(f'No branchfork to annotate interconnect delay {c1.name}/{p1}->{c2.name}/{p2}')
return timing continue
delays[line, :] = delvals
return np.moveaxis(delays, -1, 0)
def sanitize(args): def sanitize(args):
@ -236,6 +203,6 @@ def parse(text):
def load(file): def load(file):
"""Parses the contents of ``file`` and returns a :class:`DelayFile` object. """Parses the contents of ``file`` and returns a :class:`DelayFile` object.
The given file may be gzip compressed. Files with `.gz`-suffix are decompressed on-the-fly.
""" """
return parse(readtext(file)) return parse(readtext(file))

333
src/kyupy/sim.py

@ -0,0 +1,333 @@
from collections import defaultdict
from bisect import bisect, insort_left
import numpy as np
BUF1 = np.uint16(0b1010_1010_1010_1010)
INV1 = ~BUF1
AND2 = np.uint16(0b1000_1000_1000_1000)
AND3 = np.uint16(0b1000_0000_1000_0000)
AND4 = np.uint16(0b1000_0000_0000_0000)
NAND2, NAND3, NAND4 = ~AND2, ~AND3, ~AND4
OR2 = np.uint16(0b1110_1110_1110_1110)
OR3 = np.uint16(0b1111_1110_1111_1110)
OR4 = np.uint16(0b1111_1111_1111_1110)
NOR2, NOR3, NOR4 = ~OR2, ~OR3, ~OR4
XOR2 = np.uint16(0b0110_0110_0110_0110)
XOR3 = np.uint16(0b1001_0110_1001_0110)
XOR4 = np.uint16(0b0110_1001_1001_0110)
XNOR2, XNOR3, XNOR4 = ~XOR2, ~XOR3, ~XOR4
AO21 = np.uint16(0b1111_1000_1111_1000) # (i0 & i1) | i2
AO22 = np.uint16(0b1111_1000_1000_1000) # (i0 & i1) | (i2 & i3)
OA21 = np.uint16(0b1110_0000_1110_0000) # (i0 | i1) & i2
OA22 = np.uint16(0b1110_1110_1110_0000) # (i0 | i1) & (i2 | i3)
AOI21, AOI22, OAI21, OAI22 = ~AO21, ~AO22, ~OA21, ~OA22
AO211 = np.uint16(0b1111_1111_1111_1000) # (i0 & i1) | i2 | i3
OA211 = np.uint16(0b1110_0000_0000_0000) # (i0 | i1) & i2 & i3
AOI211, OAI211 = ~AO211, ~OA211
MUX21 = np.uint16(0b1100_1010_1100_1010) # z = i1 if i2 else i0 (i2 is select)
names = dict([(v, k) for k, v in globals().items() if isinstance(v, np.uint16)])
kind_prefixes = {
'nand': (NAND4, NAND3, NAND2),
'nor': (NOR4, NOR3, NOR2),
'and': (AND4, AND3, AND2),
'or': (OR4, OR3, OR2),
'isolor': (OR2, OR2, OR2),
'xor': (XOR4, XOR3, XOR2),
'xnor': (XNOR4, XNOR3, XNOR2),
'not': (INV1, INV1, INV1),
'inv': (INV1, INV1, INV1),
'ibuf': (INV1, INV1, INV1),
'__const1__': (INV1, INV1, INV1),
'tieh': (INV1, INV1, INV1),
'buf': (BUF1, BUF1, BUF1),
'nbuf': (BUF1, BUF1, BUF1),
'delln': (BUF1, BUF1, BUF1),
'__const0__': (BUF1, BUF1, BUF1),
'tiel': (BUF1, BUF1, BUF1),
'ao211': (AO211, AO211, AO211),
'oa211': (OA211, OA211, OA211),
'aoi211': (AOI211, AOI211, AOI211),
'oai211': (OAI211, OAI211, OAI211),
'ao22': (AO22, AO22, AO22),
'aoi22': (AOI22, AOI22, AOI22),
'ao21': (AO21, AO21, AO21),
'aoi21': (AOI21, AOI21, AOI21),
'oa22': (OA22, OA22, OA22),
'oai22': (OAI22, OAI22, OAI22),
'oa21': (OA21, OA21, OA21),
'oai21': (OAI21, OAI21, OAI21),
'mux21': (MUX21, MUX21, MUX21),
}
class Heap:
def __init__(self):
self.chunks = dict() # map start location to chunk size
self.released = list() # chunks that were released
self.current_size = 0
self.max_size = 0
def alloc(self, size):
for idx, loc in enumerate(self.released):
if self.chunks[loc] == size:
del self.released[idx]
return loc
if self.chunks[loc] > size: # split chunk
chunksize = self.chunks[loc]
self.chunks[loc] = size
self.chunks[loc + size] = chunksize - size
self.released[idx] = loc + size # move released pointer: loc -> loc+size
return loc
# no previously released chunk; make new one
loc = self.current_size
self.chunks[loc] = size
self.current_size += size
self.max_size = max(self.max_size, self.current_size)
return loc
def free(self, loc):
size = self.chunks[loc]
if loc + size == self.current_size: # end of managed area, remove chunk
del self.chunks[loc]
self.current_size -= size
# check and remove prev chunk if free
if len(self.released) > 0:
prev = self.released[-1]
if prev + self.chunks[prev] == self.current_size:
chunksize = self.chunks[prev]
del self.chunks[prev]
del self.released[-1]
self.current_size -= chunksize
return
released_idx = bisect(self.released, loc)
if released_idx < len(self.released) and loc + size == self.released[released_idx]: # next chunk is free, merge
chunksize = size + self.chunks[loc + size]
del self.chunks[loc + size]
self.chunks[loc] = chunksize
size = self.chunks[loc]
self.released[released_idx] = loc
else:
insort_left(self.released, loc) # put in a new release
if released_idx > 0: # check if previous chunk is free
prev = self.released[released_idx - 1]
if prev + self.chunks[prev] == loc: # previous chunk is adjacent to freed one, merge
chunksize = size + self.chunks[prev]
del self.chunks[loc]
self.chunks[prev] = chunksize
del self.released[released_idx]
def __repr__(self):
r = []
for loc in sorted(self.chunks.keys()):
size = self.chunks[loc]
released_idx = bisect(self.released, loc)
is_released = released_idx > 0 and len(self.released) > 0 and self.released[released_idx - 1] == loc
r.append(f'{loc:5d}: {"free" if is_released else "used"} {size}')
return "\n".join(r)
class SimOps:
"""A static scheduler that translates a Circuit into a topologically sorted list of basic logic operations (self.ops) and
a memory mapping (self.c_locs, self.c_caps) for use in simulators.
:param circuit: The circuit to create a schedule for.
:param strip_forks: If enabled, the scheduler will not include fork nodes to safe simulation time.
Stripping forks will cause interconnect delay annotations of lines read by fork nodes to be ignored.
:param c_reuse: If enabled, memory of intermediate signal waveforms will be re-used. This greatly reduces
memory footprint, but intermediate signal waveforms become unaccessible after a propagation.
"""
def __init__(self, circuit, c_caps=1, c_caps_min=1, a_ctrl=None, c_reuse=False, strip_forks=False):
self.circuit = circuit
self.s_len = len(circuit.s_nodes)
if isinstance(c_caps, int):
c_caps = [c_caps] * (len(circuit.lines)+3)
if a_ctrl is None:
a_ctrl = np.zeros((len(circuit.lines)+3, 3), dtype=np.int32) # add 3 for zero, tmp, tmp2
a_ctrl[:,0] = -1
# special locations and offsets in c_locs/c_caps
self.zero_idx = len(circuit.lines)
self.tmp_idx = self.zero_idx + 1
self.tmp2_idx = self.tmp_idx + 1
self.ppi_offset = self.tmp2_idx + 1
self.ppo_offset = self.ppi_offset + self.s_len
self.c_locs_len = self.ppo_offset + self.s_len
# translate circuit structure into self.ops
ops = []
interface_dict = dict((n, i) for i, n in enumerate(circuit.s_nodes))
for n in circuit.topological_order():
if n in interface_dict:
inp_idx = self.ppi_offset + interface_dict[n]
if len(n.outs) > 0 and n.outs[0] is not None: # first output of a PI/PPI
ops.append((BUF1, n.outs[0].index, inp_idx, self.zero_idx, self.zero_idx, self.zero_idx, *a_ctrl[n.outs[0]]))
if 'dff' in n.kind.lower(): # second output of DFF is inverted
if len(n.outs) > 1 and n.outs[1] is not None:
ops.append((INV1, n.outs[1].index, inp_idx, self.zero_idx, self.zero_idx, self.zero_idx, *a_ctrl[n.outs[1]]))
else: # if not DFF, no output is inverted.
for o_line in n.outs[1:]:
if o_line is not None:
ops.append((BUF1, o_line.index, inp_idx, self.zero_idx, self.zero_idx, self.zero_idx, *a_ctrl[o_line]))
continue
# regular node, not PI/PPI or PO/PPO
o0_idx = n.outs[0].index if len(n.outs) > 0 and n.outs[0] is not None else self.tmp_idx
i0_idx = n.ins[0].index if len(n.ins) > 0 and n.ins[0] is not None else self.zero_idx
i1_idx = n.ins[1].index if len(n.ins) > 1 and n.ins[1] is not None else self.zero_idx
i2_idx = n.ins[2].index if len(n.ins) > 2 and n.ins[2] is not None else self.zero_idx
i3_idx = n.ins[3].index if len(n.ins) > 3 and n.ins[3] is not None else self.zero_idx
kind = n.kind.lower()
if kind == '__fork__':
if not strip_forks:
for o_line in n.outs:
if o_line is not None:
ops.append((BUF1, o_line.index, i0_idx, i1_idx, i2_idx, i3_idx, *a_ctrl[o_line]))
continue
sp = None
for prefix, prims in kind_prefixes.items():
if kind.startswith(prefix):
sp = prims[0]
if i3_idx == self.zero_idx:
sp = prims[1]
if i2_idx == self.zero_idx:
sp = prims[2]
break
if sp is None:
print('unknown cell type', kind)
else:
ops.append((sp, o0_idx, i0_idx, i1_idx, i2_idx, i3_idx, *a_ctrl[o0_idx]))
self.ops = np.asarray(ops, dtype='int32')
# create a map from fanout lines to stem lines for fork stripping
stems = np.zeros(self.c_locs_len, dtype='int32') - 1 # default to -1: 'no fanout line'
if strip_forks:
for f in circuit.forks.values():
prev_line = f.ins[0]
while prev_line.driver.kind == '__fork__':
prev_line = prev_line.driver.ins[0]
stem_idx = prev_line.index
for ol in f.outs:
if ol is not None:
stems[ol] = stem_idx
# calculate level (distance from PI/PPI) and reference count for each line
levels = np.zeros(self.c_locs_len, dtype='int32')
ref_count = np.zeros(self.c_locs_len, dtype='int32')
level_starts = [0]
current_level = 1
for i, op in enumerate(self.ops):
# if we fork-strip, always take the stems for determining fan-in level
i0_idx = stems[op[2]] if stems[op[2]] >= 0 else op[2]
i1_idx = stems[op[3]] if stems[op[3]] >= 0 else op[3]
i2_idx = stems[op[4]] if stems[op[4]] >= 0 else op[4]
i3_idx = stems[op[5]] if stems[op[5]] >= 0 else op[5]
if levels[i0_idx] >= current_level or levels[i1_idx] >= current_level or levels[i2_idx] >= current_level or levels[i3_idx] >= current_level:
current_level += 1
level_starts.append(i)
levels[op[1]] = current_level # set level of the output line
ref_count[i0_idx] += 1
ref_count[i1_idx] += 1
ref_count[i2_idx] += 1
ref_count[i3_idx] += 1
self.level_starts = np.asarray(level_starts, dtype='int32')
self.level_stops = np.asarray(level_starts[1:] + [len(self.ops)], dtype='int32')
# combinational signal allocation table. maps line and interface indices to self.c memory locations
self.c_locs = np.full((self.c_locs_len,), -1, dtype=np.int32)
self.c_caps = np.zeros((self.c_locs_len,), dtype=np.int32)
h = Heap()
# allocate and keep memory for special fields
self.c_locs[self.zero_idx], self.c_caps[self.zero_idx] = h.alloc(c_caps_min), c_caps_min
self.c_locs[self.tmp_idx], self.c_caps[self.tmp_idx] = h.alloc(c_caps_min), c_caps_min
self.c_locs[self.tmp2_idx], self.c_caps[self.tmp2_idx] = h.alloc(c_caps_min), c_caps_min
ref_count[self.zero_idx] += 1
ref_count[self.tmp_idx] += 1
ref_count[self.tmp2_idx] += 1
# allocate and keep memory for PI/PPI, keep memory for PO/PPO (allocated later)
for i, n in enumerate(circuit.s_nodes):
if len(n.outs) > 0:
self.c_locs[self.ppi_offset + i], self.c_caps[self.ppi_offset + i] = h.alloc(c_caps_min), c_caps_min
ref_count[self.ppi_offset + i] += 1
if len(n.ins) > 0:
i0_idx = stems[n.ins[0]] if stems[n.ins[0]] >= 0 else n.ins[0]
ref_count[i0_idx] += 1
# allocate memory for the rest of the circuit
for op_start, op_stop in zip(self.level_starts, self.level_stops):
free_set = set()
for op in self.ops[op_start:op_stop]:
# if we fork-strip, always take the stems
i0_idx = stems[op[2]] if stems[op[2]] >= 0 else op[2]
i1_idx = stems[op[3]] if stems[op[3]] >= 0 else op[3]
i2_idx = stems[op[4]] if stems[op[4]] >= 0 else op[4]
i3_idx = stems[op[5]] if stems[op[5]] >= 0 else op[5]
ref_count[i0_idx] -= 1
ref_count[i1_idx] -= 1
ref_count[i2_idx] -= 1
ref_count[i3_idx] -= 1
if ref_count[i0_idx] <= 0: free_set.add(self.c_locs[i0_idx])
if ref_count[i1_idx] <= 0: free_set.add(self.c_locs[i1_idx])
if ref_count[i2_idx] <= 0: free_set.add(self.c_locs[i2_idx])
if ref_count[i3_idx] <= 0: free_set.add(self.c_locs[i3_idx])
o_idx = op[1]
cap = max(c_caps_min, c_caps[o_idx])
self.c_locs[o_idx], self.c_caps[o_idx] = h.alloc(cap), cap
if c_reuse:
for loc in free_set:
h.free(loc)
# copy memory location and capacity from stems to fanout lines
for lidx, stem in enumerate(stems):
if stem >= 0: # if at a fanout line
self.c_locs[lidx], self.c_caps[lidx] = self.c_locs[stem], self.c_caps[stem]
# copy memory location to PO/PPO area
for i, n in enumerate(circuit.s_nodes):
if len(n.ins) > 0:
self.c_locs[self.ppo_offset + i], self.c_caps[self.ppo_offset + i] = self.c_locs[n.ins[0]], self.c_caps[n.ins[0]]
self.c_len = h.max_size
d = defaultdict(int)
for op in self.ops[:,0]: d[names[op]] += 1
self.prim_counts = dict(d)
self.pi_s_locs = np.flatnonzero(self.c_locs[self.ppi_offset+np.arange(len(self.circuit.io_nodes))] >= 0)
self.po_s_locs = np.flatnonzero(self.c_locs[self.ppo_offset+np.arange(len(self.circuit.io_nodes))] >= 0)
self.ppio_s_locs = np.arange(len(self.circuit.io_nodes), self.s_len)
self.pippi_s_locs = np.concatenate([self.pi_s_locs, self.ppio_s_locs])
self.poppo_s_locs = np.concatenate([self.po_s_locs, self.ppio_s_locs])
self.pi_c_locs = self.c_locs[self.ppi_offset+self.pi_s_locs]
self.po_c_locs = self.c_locs[self.ppo_offset+self.po_s_locs]
self.ppi_c_locs = self.c_locs[self.ppi_offset+self.ppio_s_locs]
self.ppo_c_locs = self.c_locs[self.ppo_offset+self.ppio_s_locs]
self.pippi_c_locs = np.concatenate([self.pi_c_locs, self.ppi_c_locs])
self.poppo_c_locs = np.concatenate([self.po_c_locs, self.ppo_c_locs])

103
src/kyupy/stil.py

@ -1,16 +1,17 @@
"""A simple and incomplete parser for the Standard Test Interface Language (STIL). """A simple and incomplete parser for the Standard Test Interface Language (STIL).
The main purpose of this parser is to load scan pattern sets from STIL files. The main purpose of this parser is to load scan pattern sets from STIL files.
It supports only a very limited subset of STIL. It supports only a subset of STIL.
The functions :py:func:`load` and :py:func:`read` return an intermediate representation (:class:`StilFile` object). The functions :py:func:`parse` and :py:func:`load` return an intermediate representation (:py:class:`StilFile` object).
Call :py:func:`StilFile.tests`, :py:func:`StilFile.tests_loc`, or :py:func:`StilFile.responses` to Call :py:func:`StilFile.tests()`, :py:func:`StilFile.tests_loc()`, or :py:func:`StilFile.responses()` to
obtain the appropriate vector sets. obtain the appropriate vector sets.
""" """
import re import re
from collections import namedtuple from collections import namedtuple
import numpy as np
from lark import Lark, Transformer from lark import Lark, Transformer
from . import readtext, logic from . import readtext, logic
@ -55,7 +56,7 @@ class StilFile:
capture = dict((k, v.replace('\n', '').replace('N', '-')) for k, v in call.parameters.items()) capture = dict((k, v.replace('\n', '').replace('N', '-')) for k, v in call.parameters.items())
def _maps(self, c): def _maps(self, c):
interface = list(c.interface) + [n for n in c.nodes if 'DFF' in n.kind] interface = list(c.io_nodes) + [n for n in c.nodes if 'DFF' in n.kind]
intf_pos = dict((n.name, i) for i, n in enumerate(interface)) intf_pos = dict((n.name, i) for i, n in enumerate(interface))
pi_map = [intf_pos[n] for n in self.signal_groups['_pi']] pi_map = [intf_pos[n] for n in self.signal_groups['_pi']]
po_map = [intf_pos[n] for n in self.signal_groups['_po']] po_map = [intf_pos[n] for n in self.signal_groups['_po']]
@ -81,73 +82,99 @@ class StilFile:
scan_out_inversion.append(inversion) scan_out_inversion.append(inversion)
scan_maps[chain[0]] = scan_map scan_maps[chain[0]] = scan_map
scan_maps[chain[-1]] = scan_map scan_maps[chain[-1]] = scan_map
scan_inversions[chain[0]] = scan_in_inversion scan_inversions[chain[0]] = logic.mvarray(scan_in_inversion)[0]
scan_inversions[chain[-1]] = scan_out_inversion scan_inversions[chain[-1]] = logic.mvarray(scan_out_inversion)[0]
return interface, pi_map, po_map, scan_maps, scan_inversions return interface, pi_map, po_map, scan_maps, scan_inversions
def tests(self, circuit): def tests(self, circuit):
"""Assembles and returns a scan test pattern set for given circuit. """Assembles and returns a scan test pattern set for given circuit.
This function assumes a static (stuck-at fault) test. This function assumes a static (stuck-at fault) test.
:param circuit: The circuit to assemble the patterns for. The patterns will follow the
:py:attr:`~kyupy.circuit.Circuit.s_nodes` ordering of the this circuit.
:return: A 4-valued multi-valued (mv) logic array (see :py:mod:`~kyupy.logic`).
The values for primary inputs and sequential elements are filled, the primary outputs are left unassigned.
""" """
interface, pi_map, _, scan_maps, scan_inversions = self._maps(circuit) interface, pi_map, _, scan_maps, scan_inversions = self._maps(circuit)
tests = logic.MVArray((len(interface), len(self.patterns))) tests = np.full((len(interface), len(self.patterns)), logic.UNASSIGNED)
for i, p in enumerate(self.patterns): for i, p in enumerate(self.patterns):
for si_port in self.si_ports.keys(): for si_port in self.si_ports.keys():
pattern = logic.mv_xor(p.load[si_port], scan_inversions[si_port]) pattern = logic.mvarray(p.load[si_port])
tests.data[scan_maps[si_port], i] = pattern.data[:, 0] inversions = np.choose((pattern == logic.UNASSIGNED) | (pattern == logic.UNKNOWN),
tests.data[pi_map, i] = logic.MVArray(p.capture['_pi']).data[:, 0] [scan_inversions[si_port], logic.ZERO]).astype(np.uint8)
np.bitwise_xor(pattern, inversions, out=pattern)
tests[scan_maps[si_port], i] = pattern
tests[pi_map, i] = logic.mvarray(p.capture['_pi'])
return tests return tests
def tests_loc(self, circuit): def tests_loc(self, circuit, init_filter=None, launch_filter=None):
"""Assembles and returns a LoC scan test pattern set for given circuit. """Assembles and returns a LoC scan test pattern set for given circuit.
This function assumes a launch-on-capture (LoC) delay test. This function assumes a launch-on-capture (LoC) delay test.
It performs a logic simulation to obtain the first capture pattern (the one that launches the delay It performs a logic simulation to obtain the first capture pattern (the one that launches the delay
test) and assembles the test pattern set from from pairs for initialization- and launch-patterns. test) and assembles the test pattern set from from pairs for initialization- and launch-patterns.
:param circuit: The circuit to assemble the patterns for. The patterns will follow the
:py:attr:`~kyupy.circuit.Circuit.s_nodes` ordering of the this circuit.
:param init_filter: A function for filtering the initialization patterns. This function is called
with the initialization patterns from the STIL file as mvarray before logic simulation.
It shall return an mvarray with the same shape. This function can be used, for example, to fill
patterns.
:param launch_filter: A function for filtering the launch patterns. This function is called
with the launch patterns generated by logic simulation before they are combined with
the initialization patterns to form the final 8-valued test patterns.
The function shall return an mvarray with the same shape. This function can be used, for example, to fill
patterns.
:return: An 8-valued multi-valued (mv) logic array (see :py:mod:`~kyupy.logic`). The values for primary
inputs and sequential elements are filled, the primary outputs are left unassigned.
""" """
interface, pi_map, po_map, scan_maps, scan_inversions = self._maps(circuit) interface, pi_map, po_map, scan_maps, scan_inversions = self._maps(circuit)
init = logic.MVArray((len(interface), len(self.patterns)), m=4) init = np.full((len(interface), len(self.patterns)), logic.UNASSIGNED)
# init = PackedVectors(len(self.patterns), len(interface), 2)
for i, p in enumerate(self.patterns): for i, p in enumerate(self.patterns):
# init.set_values(i, '0' * len(interface)) # init.set_values(i, '0' * len(interface))
for si_port in self.si_ports.keys(): for si_port in self.si_ports.keys():
pattern = logic.mv_xor(p.load[si_port], scan_inversions[si_port]) pattern = logic.mvarray(p.load[si_port])
init.data[scan_maps[si_port], i] = pattern.data[:, 0] inversions = np.choose((pattern == logic.UNASSIGNED) | (pattern == logic.UNKNOWN),
init.data[pi_map, i] = logic.MVArray(p.launch['_pi'] if '_pi' in p.launch else p.capture['_pi']).data[:, 0] [scan_inversions[si_port], logic.ZERO]).astype(np.uint8)
launch_bp = logic.BPArray(init) np.bitwise_xor(pattern, inversions, out=pattern)
sim4v = LogicSim(circuit, len(init), m=4) init[scan_maps[si_port], i] = pattern
sim4v.assign(launch_bp) init[pi_map, i] = logic.mvarray(p.launch['_pi'] if '_pi' in p.launch else p.capture['_pi'])
sim4v.propagate() if init_filter: init = init_filter(init)
sim4v.capture(launch_bp) sim8v = LogicSim(circuit, init.shape[-1], m=8)
launch = logic.MVArray(launch_bp) sim8v.s[0] = logic.mv_to_bp(init)
sim8v.s_to_c()
sim8v.c_prop()
sim8v.c_to_s()
launch = logic.bp_to_mv(sim8v.s[1])[..., :init.shape[-1]]
for i, p in enumerate(self.patterns): for i, p in enumerate(self.patterns):
# if there was no launch cycle or launch clock, then init = launch # if there was no launch cycle or launch clock, then init = launch
if '_pi' not in p.launch or 'P' not in p.launch['_pi'] or 'P' not in p.capture['_pi']: if '_pi' not in p.launch or 'P' not in p.launch['_pi'] or 'P' not in p.capture['_pi']:
for si_port in self.si_ports.keys(): for si_port in self.si_ports.keys():
pattern = logic.mv_xor(p.load[si_port], scan_inversions[si_port]) pattern = logic.mv_xor(logic.mvarray(p.load[si_port]), scan_inversions[si_port])
launch.data[scan_maps[si_port], i] = pattern.data[:, 0] launch[scan_maps[si_port], i] = pattern
if '_pi' in p.capture and 'P' in p.capture['_pi']: if '_pi' in p.capture and 'P' in p.capture['_pi']:
launch.data[pi_map, i] = logic.MVArray(p.capture['_pi']).data[:, 0] launch[pi_map, i] = logic.mvarray(p.capture['_pi'])
launch.data[po_map, i] = logic.UNASSIGNED launch[po_map, i] = logic.UNASSIGNED
if launch_filter: launch = launch_filter(launch)
return logic.mv_transition(init, launch) return logic.mv_transition(init, launch)
def responses(self, circuit): def responses(self, circuit):
"""Assembles and returns a scan test response pattern set for given circuit.""" """Assembles and returns a scan test response pattern set for given circuit.
:param circuit: The circuit to assemble the patterns for. The patterns will follow the
:py:attr:`~kyupy.circuit.Circuit.s_nodes` ordering of the this circuit.
:return: A 4-valued multi-valued (mv) logic array (see :py:mod:`~kyupy.logic`).
The values for primary outputs and sequential elements are filled, the primary inputs are left unassigned.
"""
interface, _, po_map, scan_maps, scan_inversions = self._maps(circuit) interface, _, po_map, scan_maps, scan_inversions = self._maps(circuit)
resp = logic.MVArray((len(interface), len(self.patterns))) resp = np.full((len(interface), len(self.patterns)), logic.UNASSIGNED)
# resp = PackedVectors(len(self.patterns), len(interface), 2)
for i, p in enumerate(self.patterns): for i, p in enumerate(self.patterns):
resp.data[po_map, i] = logic.MVArray(p.capture['_po'] if len(p.capture) > 0 else p.launch['_po']).data[:, 0] resp[po_map, i] = logic.mvarray(p.capture['_po'] if len(p.capture) > 0 else p.launch['_po'])
# if len(p.capture) > 0:
# resp.set_values(i, p.capture['_po'], po_map)
# else:
# resp.set_values(i, p.launch['_po'], po_map)
for so_port in self.so_ports.keys(): for so_port in self.so_ports.keys():
pattern = logic.mv_xor(p.unload[so_port], scan_inversions[so_port]) pattern = logic.mv_xor(logic.mvarray(p.unload[so_port]), scan_inversions[so_port])
resp.data[scan_maps[so_port], i] = pattern.data[:, 0] resp[scan_maps[so_port], i] = pattern
# resp.set_values(i, p.unload[so_port], scan_maps[so_port], scan_inversions[so_port])
return resp return resp
@ -246,6 +273,6 @@ def parse(text):
def load(file): def load(file):
"""Parses the contents of ``file`` and returns a :class:`StilFile` object. """Parses the contents of ``file`` and returns a :class:`StilFile` object.
The given file may be gzip compressed. Files with `.gz`-suffix are decompressed on-the-fly.
""" """
return parse(readtext(file)) return parse(readtext(file))

656
src/kyupy/techlib.py

@ -1,38 +1,27 @@
from .circuit import Node, Line """KyuPy's Built-In Technology Libraries
def add_and_connect(circuit, name, kind, in1=None, in2=None, out=None):
n = Node(circuit, name, kind)
if in1 is not None:
n.ins[0] = in1
in1.reader = n
in1.reader_pin = 0
if in2 is not None:
n.ins[1] = in2
in2.reader = n
in2.reader_pin = 1
if out is not None:
n.outs[0] = out
out.driver = n
out.driver_pin = 0
return n
Technology libraries provide cell definitions and their implementation with simulation primitives.
A couple of common standard cell libraries are built-in.
Others can be easily added by providing a bench-like description of the cells.
"""
class TechLib: import re
"""Provides some information specific to standard cell libraries necessary from itertools import product
for loading gate-level designs. :py:class:`~kyupy.circuit.Node` objects do not
have pin names. The methods defined here map pin names to pin directions and defined from . import bench
positions in the ``node.ins`` and ``node.outs`` lists. The default implementation
provides mappings for SAED-inspired standard cell libraries.
"""
class TechLibOld:
@staticmethod @staticmethod
def pin_index(kind, pin): def pin_index(kind, pin):
"""Returns a pin list position for a given node kind and pin name.""" if isinstance(pin, int):
return max(0, pin-1)
if kind[:3] in ('OAI', 'AOI'): if kind[:3] in ('OAI', 'AOI'):
if pin[0] == 'A': return int(pin[1]) - 1 if pin[0] == 'A': return int(pin[1]) - 1
if pin[0] == 'B': return int(pin[1]) + int(kind[4]) - 1 if pin == 'B': return int(kind[3])
if pin[0] == 'B': return int(pin[1]) - 1 + int(kind[3])
for prefix, pins, index in [('HADD', ('B0', 'SO'), 1), for prefix, pins, index in [('HADD', ('B0', 'SO'), 1),
('HADD', ('A0', 'C1'), 0),
('MUX21', ('S', 'S0'), 2), ('MUX21', ('S', 'S0'), 2),
('MX2', ('S0',), 2), ('MX2', ('S0',), 2),
('TBUF', ('OE',), 1), ('TBUF', ('OE',), 1),
@ -45,7 +34,9 @@ class TechLib:
('SDFF', ('QN',), 1), ('SDFF', ('QN',), 1),
('SDFF', ('CLK',), 3), ('SDFF', ('CLK',), 3),
('SDFF', ('RSTB', 'RN'), 4), ('SDFF', ('RSTB', 'RN'), 4),
('SDFF', ('SETB',), 5)]: ('SDFF', ('SETB',), 5),
('ISOL', ('ISO',), 0),
('ISOL', ('D',), 1)]:
if kind.startswith(prefix) and pin in pins: return index if kind.startswith(prefix) and pin in pins: return index
for index, pins in enumerate([('A1', 'IN1', 'A', 'S', 'INP', 'I', 'Q', 'QN', 'Y', 'Z', 'ZN'), for index, pins in enumerate([('A1', 'IN1', 'A', 'S', 'INP', 'I', 'Q', 'QN', 'Y', 'Z', 'ZN'),
('A2', 'IN2', 'B', 'CK', 'CLK', 'CO', 'SE'), ('A2', 'IN2', 'B', 'CK', 'CLK', 'CO', 'SE'),
@ -58,254 +49,367 @@ class TechLib:
@staticmethod @staticmethod
def pin_is_output(kind, pin): def pin_is_output(kind, pin):
"""Returns True, if given pin name of a node kind is an output.""" if isinstance(pin, int):
return pin == 0
if 'MUX' in kind and pin == 'S': return False if 'MUX' in kind and pin == 'S': return False
return pin in ('Q', 'QN', 'Z', 'ZN', 'Y', 'CO', 'S', 'SO', 'C1') return pin in ('Q', 'QN', 'Z', 'ZN', 'Y', 'CO', 'S', 'SO', 'C1')
@staticmethod
def split_complex_gates(circuit): class TechLib:
node_list = circuit.nodes """Class for standard cell library definitions.
for n in node_list:
name = n.name :py:class:`~kyupy.circuit.Node` objects do not have pin names.
ins = n.ins This class maps pin names to pin directions and defined positions in the ``node.ins`` and ``node.outs`` lists.
outs = n.outs Furthermore, it gives access to implementations of complex cells. See also :py:func:`~kyupy.circuit.substitute` and
if n.kind.startswith('AO21X'): :py:func:`~kyupy.circuit.resolve_tlib_cells`.
n.remove() """
n_and = add_and_connect(circuit, name+'~and', 'AND2', ins[0], ins[1], None) def __init__(self, lib_src):
n_or = add_and_connect(circuit, name+'~or', 'OR2', None, ins[2], outs[0]) self.cells = dict()
Line(circuit, n_and, n_or) """A dictionary with pin definitions and circuits for each cell kind (type).
elif n.kind.startswith('AOI21X'): """
n.remove() for c_str in re.split(r';\s+', lib_src):
n_and = add_and_connect(circuit, name+'~and', 'AND2', ins[0], ins[1], None) c_str = re.sub(r'^\s+', '', c_str)
n_nor = add_and_connect(circuit, name+'~nor', 'NOR2', None, ins[2], outs[0]) name_len = c_str.find(' ')
Line(circuit, n_and, n_nor) if name_len <= 0: continue
elif n.kind.startswith('OA21X'): c = bench.parse(c_str[name_len:])
n.remove() c.name = c_str[:name_len]
n_or = add_and_connect(circuit, name+'~or', 'OR2', ins[0], ins[1], None) c.eliminate_1to1_forks()
n_and = add_and_connect(circuit, name+'~and', 'AND2', None, ins[2], outs[0]) i_idx, o_idx = 0, 0
Line(circuit, n_or, n_and) pin_dict = dict()
elif n.kind.startswith('OAI21X'): for n in c.io_nodes:
n.remove() if len(n.ins) == 0:
n_or = add_and_connect(circuit, name+'~or', 'OR2', ins[0], ins[1], None) pin_dict[n.name] = (i_idx, False)
n_nand = add_and_connect(circuit, name+'~nand', 'NAND2', None, ins[2], outs[0]) i_idx += 1
Line(circuit, n_or, n_nand) else:
elif n.kind.startswith('OA22X'): pin_dict[n.name] = (o_idx, True)
n.remove() o_idx += 1
n_or0 = add_and_connect(circuit, name+'~or0', 'OR2', ins[0], ins[1], None) parts = [s[1:-1].split(',') if s[0] == '{' else [s] for s in re.split(r'({[^}]+})', c.name) if len(s) > 0]
n_or1 = add_and_connect(circuit, name+'~or1', 'OR2', ins[2], ins[3], None) for name in [''.join(item) for item in product(*parts)]:
n_and = add_and_connect(circuit, name+'~and', 'AND2', None, None, outs[0]) self.cells[name] = (c, pin_dict)
Line(circuit, n_or0, n_and)
Line(circuit, n_or1, n_and) def pin_index(self, kind, pin):
elif n.kind.startswith('OAI22X'): """Returns a pin list position for a given node kind and pin name."""
n.remove() assert kind in self.cells, f'Unknown cell: {kind}'
n_or0 = add_and_connect(circuit, name+'~or0', 'OR2', ins[0], ins[1], None) assert pin in self.cells[kind][1], f'Unknown pin: {pin} for cell {kind}'
n_or1 = add_and_connect(circuit, name+'~or1', 'OR2', ins[2], ins[3], None) return self.cells[kind][1][pin][0]
n_nand = add_and_connect(circuit, name+'~nand', 'NAND2', None, None, outs[0])
Line(circuit, n_or0, n_nand) def pin_is_output(self, kind, pin):
Line(circuit, n_or1, n_nand) """Returns True, if given pin name of a node kind is an output."""
elif n.kind.startswith('AO22X'): assert kind in self.cells, f'Unknown cell: {kind}'
n.remove() assert pin in self.cells[kind][1], f'Unknown pin: {pin} for cell {kind}'
n_and0 = add_and_connect(circuit, name+'~and0', 'AND2', ins[0], ins[1], None) return self.cells[kind][1][pin][1]
n_and1 = add_and_connect(circuit, name+'~and1', 'AND2', ins[2], ins[3], None)
n_or = add_and_connect(circuit, name+'~or', 'OR2', None, None, outs[0])
Line(circuit, n_and0, n_or) GSC180 = TechLib(r"""
Line(circuit, n_and1, n_or) BUFX{1,3} input(A) output(Y) Y=BUF1(A) ;
elif n.kind.startswith('AOI22X'): CLKBUFX{1,2,3} input(A) output(Y) Y=BUF1(A) ;
n.remove() INVX{1,2,4,8} input(A) output(Y) Y=INV1(A) ;
n_and0 = add_and_connect(circuit, name+'~and0', 'AND2', ins[0], ins[1], None) TBUFX{1,2,4,8} input(A,OE) output(Y) Y=AND2(A,OE) ;
n_and1 = add_and_connect(circuit, name+'~and1', 'AND2', ins[2], ins[3], None) TINVX1 input(A,OE) output(Y) AB=INV1(A) Y=AND2(AB,OE) ;
n_nor = add_and_connect(circuit, name+'~nor', 'NOR2', None, None, outs[0])
Line(circuit, n_and0, n_nor) AND2X1 input(A,B) output(Y) Y=AND2(A,B) ;
Line(circuit, n_and1, n_nor) NAND2X{1,2} input(A,B) output(Y) Y=NAND2(A,B) ;
elif n.kind.startswith('AO221X'): NAND3X1 input(A,B,C) output(Y) Y=NAND3(A,B,C) ;
n.remove() NAND4X1 input(A,B,C,D) output(Y) Y=NAND4(A,B,C,D) ;
n_and0 = add_and_connect(circuit, name+'~and0', 'AND2', ins[0], ins[1], None) OR2X1 input(A,B) output(Y) Y=OR2(A,B) ;
n_and1 = add_and_connect(circuit, name+'~and1', 'AND2', ins[2], ins[3], None) OR4X1 input(A,B,C,D) output(Y) Y=OR4(A,B,C,D) ;
n_or0 = add_and_connect(circuit, name+'~or0', 'OR2', None, None, None) NOR2X1 input(A,B) output(Y) Y=NOR2(A,B) ;
n_or1 = add_and_connect(circuit, name+'~or1', 'OR2', None, ins[4], outs[0]) NOR3X1 input(A,B,C) output(Y) Y=NOR3(A,B,C) ;
Line(circuit, n_and0, n_or0) NOR4X1 input(A,B,C,D) output(Y) Y=NOR4(A,B,C,D) ;
Line(circuit, n_and1, n_or0) XOR2X1 input(A,B) output(Y) Y=XOR2(A,B) ;
Line(circuit, n_or0, n_or1)
elif n.kind.startswith('AOI221X'): MX2X1 input(A,B,S0) output(Y) Y=MUX21(A,B,S0) ;
n.remove() AOI21X1 input(A0,A1,B0) output(Y) Y=AOI21(A0,A1,B0) ;
n_and0 = add_and_connect(circuit, name+'~and0', 'AND2', ins[0], ins[1], None) AOI22X1 input(A0,A1,B0,B1) output(Y) Y=AOI22(A0,A1,B0,B1) ;
n_and1 = add_and_connect(circuit, name+'~and1', 'AND2', ins[2], ins[3], None) OAI21X1 input(A0,A1,B0) output(Y) Y=OAI21(A0,A1,B0) ;
n_or = add_and_connect(circuit, name+'~or', 'OR2', None, None, None) OAI22X1 input(A0,A1,B0,B1) output(Y) Y=OAI22(A0,A1,B0,B1) ;
n_nor = add_and_connect(circuit, name+'~nor', 'NOR2', None, ins[4], outs[0]) OAI33X1 input(A0,A1,A2,B0,B1,B2) output(Y) AA=OR2(A0,A1) BB=OR2(B0,B1) Y=OAI22(AA,A2,BB,B2) ;
Line(circuit, n_and0, n_or) ADDFX1 input(A,B,CI) output(CO,S) AB=XOR2(A,B) CO=XOR2(AB,CI) S=AO22(AB,CI,A,B) ;
Line(circuit, n_and1, n_or) ADDHX1 input(A,B) output(CO,S) CO=XOR2(A,B) S=AND2(A,B) ;
Line(circuit, n_or, n_nor)
elif n.kind.startswith('OA221X'): DFFX1 input(CK,D) output(Q,QN) Q=DFF(D,CK) QN=INV1(Q) ;
n.remove() DFFSRX1 input(CK,D,RN,SN) output(Q,QN) DR=AND2(D,RN) SET=INV1(SN) DRS=OR2(DR,SET) Q=DFF(DRS,CK) QN=INV1(Q) ;
n_or0 = add_and_connect(circuit, name+'~or0', 'OR2', ins[0], ins[1], None) SDFFSRX1 input(CK,D,RN,SE,SI,SN) output(Q,QN) DR=AND2(D,RN) SET=INV1(SN) DRS=OR2(DR,SET) DI=MUX21(DRS,SI,SE) Q=DFF(DI,CK) QN=INV1(Q) ;
n_or1 = add_and_connect(circuit, name+'~or1', 'OR2', ins[2], ins[3], None)
n_and0 = add_and_connect(circuit, name+'~and0', 'AND2', None, None, None) TLATSRX1 input(D,G,RN,SN) output(Q,QN) DR=AND2(D,RN) SET=INV1(SN) DRS=OR2(DR,SET) Q=LATCH(DRS,G) QN=INV1(Q) ;
n_and1 = add_and_connect(circuit, name+'~and1', 'AND2', None, ins[4], outs[0]) TLATX1 input(C,D) output(Q,QN) Q=LATCH(D,C) QN=INV1(Q) ;
Line(circuit, n_or0, n_and0) """)
Line(circuit, n_or1, n_and0) """The GSC 180nm generic standard cell library.
Line(circuit, n_and0, n_and1) """
elif n.kind.startswith('OAI221X'):
n.remove()
n_or0 = add_and_connect(circuit, name+'~or0', 'OR2', ins[0], ins[1], None) _nangate_common = r"""
n_or1 = add_and_connect(circuit, name+'~or1', 'OR2', ins[2], ins[3], None) FILLCELL_X{1,2,4,8,16,32} ;
n_and0 = add_and_connect(circuit, name+'~and0', 'AND2', None, None, None)
n_nand1 = add_and_connect(circuit, name+'~nand1', 'NAND2', None, ins[4], outs[0]) LOGIC0_X1 output(Z) Z=__const0__() ;
Line(circuit, n_or0, n_and0) LOGIC1_X1 output(Z) Z=__const1__() ;
Line(circuit, n_or1, n_and0)
Line(circuit, n_and0, n_nand1) BUF_X{1,2,4,8,16,32} input(A) output(Z) Z=BUF1(A) ;
elif n.kind.startswith('AO222X'): CLKBUF_X{1,2,3} input(A) output(Z) Z=BUF1(A) ;
n.remove()
n_and0 = add_and_connect(circuit, name+'~and0', 'AND2', ins[0], ins[1], None) NAND2_X{1,2,4} input(A1,A2) output(ZN) ZN=NAND2(A1,A2) ;
n_and1 = add_and_connect(circuit, name+'~and1', 'AND2', ins[2], ins[3], None) NAND3_X{1,2,4} input(A1,A2,A3) output(ZN) ZN=NAND3(A1,A2,A3) ;
n_and2 = add_and_connect(circuit, name+'~and2', 'AND2', ins[4], ins[5], None) NAND4_X{1,2,4} input(A1,A2,A3,A4) output(ZN) ZN=NAND4(A1,A2,A3,A4) ;
n_or0 = add_and_connect(circuit, name+'~or0', 'OR2', None, None, None) NOR2_X{1,2,4} input(A1,A2) output(ZN) ZN=NOR2(A1,A2) ;
n_or1 = add_and_connect(circuit, name+'~or1', 'OR2', None, None, outs[0]) NOR3_X{1,2,4} input(A1,A2,A3) output(ZN) ZN=NOR3(A1,A2,A3) ;
Line(circuit, n_and0, n_or0) NOR4_X{1,2,4} input(A1,A2,A3,A4) output(ZN) ZN=NOR4(A1,A2,A3,A4) ;
Line(circuit, n_and1, n_or0)
Line(circuit, n_and2, n_or1) AOI21_X{1,2,4} input(A,B1,B2) output(ZN) ZN=AOI21(B1,B2,A) ;
Line(circuit, n_or0, n_or1) OAI21_X{1,2,4} input(A,B1,B2) output(ZN) ZN=OAI21(B1,B2,A) ;
elif n.kind.startswith('AOI222X'): AOI22_X{1,2,4} input(A1,A2,B1,B2) output(ZN) ZN=AOI22(A1,A2,B1,B2) ;
n.remove() OAI22_X{1,2,4} input(A1,A2,B1,B2) output(ZN) ZN=OAI22(A1,A2,B1,B2) ;
n_and0 = add_and_connect(circuit, name+'~and0', 'AND2', ins[0], ins[1], None)
n_and1 = add_and_connect(circuit, name+'~and1', 'AND2', ins[2], ins[3], None) OAI211_X{1,2,4} input(A,B,C1,C2) output(ZN) ZN=OAI211(C1,C2,A,B) ;
n_and2 = add_and_connect(circuit, name+'~and2', 'AND2', ins[4], ins[5], None) AOI211_X{1,2,4} input(A,B,C1,C2) output(ZN) ZN=AOI211(C1,C2,A,B) ;
n_or0 = add_and_connect(circuit, name+'~or0', 'OR2', None, None, None)
n_nor1 = add_and_connect(circuit, name+'~nor1', 'NOR2', None, None, outs[0]) MUX2_X{1,2} input(A,B,S) output(Z) Z=MUX21(A,B,S) ;
Line(circuit, n_and0, n_or0)
Line(circuit, n_and1, n_or0) AOI221_X{1,2,4} input(A,B1,B2,C1,C2) output(ZN) BC=AO22(B1,B2,C1,C2) ZN=NOR2(BC,A) ;
Line(circuit, n_and2, n_nor1) OAI221_X{1,2,4} input(A,B1,B2,C1,C2) output(ZN) BC=OA22(B1,B2,C1,C2) ZN=NAND2(BC,A) ;
Line(circuit, n_or0, n_nor1)
elif n.kind.startswith('OA222X'): AOI222_X{1,2,4} input(A1,A2,B1,B2,C1,C2) output(ZN) BC=AO22(B1,B2,C1,C2) ZN=AOI21(A1,A2,BC) ;
n.remove() OAI222_X{1,2,4} input(A1,A2,B1,B2,C1,C2) output(ZN) BC=OA22(B1,B2,C1,C2) ZN=OAI21(A1,A2,BC) ;
n_or0 = add_and_connect(circuit, name+'~or0', 'OR2', ins[0], ins[1], None)
n_or1 = add_and_connect(circuit, name+'~or1', 'OR2', ins[2], ins[3], None) OAI33_X1 input(A1,A2,A3,B1,B2,B3) output(ZN) AA=OR2(A1,A2) BB=OR2(B1,B2) ZN=OAI22(AA,A3,BB,B3) ;
n_or2 = add_and_connect(circuit, name+'~or2', 'OR2', ins[4], ins[5], None)
n_and0 = add_and_connect(circuit, name+'~and0', 'AND2', None, None, None) HA_X1 input(A,B) output(CO,S) CO=XOR2(A,B) S=AND2(A,B) ;
n_and1 = add_and_connect(circuit, name+'~and1', 'AND2', None, None, outs[0])
Line(circuit, n_or0, n_and0) FA_X1 input(A,B,CI) output(CO,S) AB=XOR2(A,B) CO=XOR2(AB,CI) S=AO22(CI,A,B) ;
Line(circuit, n_or1, n_and0)
Line(circuit, n_or2, n_and1) CLKGATE_X{1,2,4,8} input(CK,E) output(GCK) GCK=AND2(CK,E) ;
Line(circuit, n_and0, n_and1)
elif n.kind.startswith('OAI222X'): CLKGATETST_X{1,2,4,8} input(CK,E,SE) output(GCK) GCK=OA21(CK,E,SE) ;
n.remove()
n0 = add_and_connect(circuit, name+'~or0', 'OR2', ins[0], ins[1], None) DFF_X{1,2} input(D,CK) output(Q,QN) Q=DFF(D,CK) QN=INV1(Q) ;
n1 = add_and_connect(circuit, name+'~or1', 'OR2', ins[2], ins[3], None) DFFR_X{1,2} input(D,RN,CK) output(Q,QN) DR=AND2(D,RN) Q=DFF(DR,CK) QN=INV1(Q) ;
n2 = add_and_connect(circuit, name+'~or2', 'OR2', ins[4], ins[5], None) DFFS_X{1,2} input(D,SN,CK) output(Q,QN) S=INV1(SN) DS=OR2(D,S) Q=DFF(DS,CK) QN=INV1(Q) ;
n3 = add_and_connect(circuit, name+'~and0', 'AND2', None, None, None) DFFRS_X{1,2} input(D,RN,SN,CK) output(Q,QN) S=INV1(SN) DS=OR2(D,S) DRS=AND2(DS,RN) Q=DFF(DRS,CK) QN=INV1(Q) ;
n4 = add_and_connect(circuit, name+'~nand1', 'NAND2', None, None, outs[0])
Line(circuit, n0, n3) SDFF_X{1,2} input(D,SE,SI,CK) output(Q,QN) DI=MUX21(D,SI,SE) Q=DFF(DI,CK) QN=INV1(Q) ;
Line(circuit, n1, n3) SDFFR_X{1,2} input(D,RN,SE,SI,CK) output(Q,QN) DR=AND2(D,RN) DI=MUX21(DR,SI,SE) Q=DFF(DI,CK) QN=INV1(Q) ;
Line(circuit, n2, n4) SDFFS_X{1,2} input(D,SE,SI,SN,CK) output(Q,QN) S=INV1(SN) DS=OR2(D,S) DI=MUX21(DS,SI,SE) Q=DFF(DI,CK) QN=INV1(Q) ;
Line(circuit, n3, n4) SDFFRS_X{1,2} input(D,RN,SE,SI,SN,CK) output(Q,QN) S=INV1(SN) DS=OR2(D,S) DRS=AND2(DS,RN) DI=MUX21(DRS,SI,SE) Q=DFF(DI,CK) QN=INV1(Q) ;
elif n.kind.startswith('AND3X'):
n.remove() TBUF_X{1,2,4,8,16} input(A,EN) output(Z) Z=BUF1(A) ;
n0 = add_and_connect(circuit, name+'~and0', 'AND2', ins[0], ins[1], None) TINV_X1 input(I,EN) output(ZN) ZN=INV1(I) ;
n1 = add_and_connect(circuit, name+'~and1', 'AND2', None, ins[2], outs[0]) TLAT_X1 input(D,G,OE) output(Q) Q=LATCH(D,G) ;
Line(circuit, n0, n1)
elif n.kind.startswith('OR3X'): DLH_X{1,2} input(D,G) output(Q) Q=LATCH(D,G) ;
n.remove() DLL_X{1,2} input(D,GN) output(Q) G=INV1(GN) Q=LATCH(D,G) ;
n0 = add_and_connect(circuit, name+'~or0', 'OR2', ins[0], ins[1], None) """
n1 = add_and_connect(circuit, name+'~or1', 'OR2', None, ins[2], outs[0])
Line(circuit, n0, n1)
elif n.kind.startswith('XOR3X'): NANGATE = TechLib(_nangate_common + r"""
n.remove() INV_X{1,2,4,8,16,32} input(I) output(ZN) ZN=INV1(I) ;
n0 = add_and_connect(circuit, name+'~xor0', 'XOR2', ins[0], ins[1], None)
n1 = add_and_connect(circuit, name+'~xor1', 'XOR2', None, ins[2], outs[0]) AND2_X{1,2,4} input(A1,A2) output(Z) Z=AND2(A1,A2) ;
Line(circuit, n0, n1) AND3_X{1,2,4} input(A1,A2,A3) output(Z) Z=AND3(A1,A2,A3) ;
elif n.kind.startswith('NAND3X'): AND4_X{1,2,4} input(A1,A2,A3,A4) output(Z) Z=AND4(A1,A2,A3,A4) ;
n.remove() OR2_X{1,2,4} input(A1,A2) output(Z) Z=OR2(A1,A2) ;
n0 = add_and_connect(circuit, name+'~and', 'AND2', ins[0], ins[1], None) OR3_X{1,2,4} input(A1,A2,A3) output(Z) Z=OR3(A1,A2,A3) ;
n1 = add_and_connect(circuit, name+'~nand', 'NAND2', None, ins[2], outs[0]) OR4_X{1,2,4} input(A1,A2,A3,A4) output(Z) Z=OR4(A1,A2,A3,A4) ;
Line(circuit, n0, n1) XOR2_X{1,2} input(A1,A2) output(Z) Z=XOR2(A1,A2) ;
elif n.kind.startswith('NOR3X'): XNOR2_X{1,2} input(A1,A2) output(ZN) ZN=XNOR2(A1,A2) ;
n.remove() """)
n0 = add_and_connect(circuit, name+'~or', 'OR2', ins[0], ins[1], None) """An newer NANGATE-variant that uses 'Z' as output pin names for AND and OR gates.
n1 = add_and_connect(circuit, name+'~nor', 'NOR2', None, ins[2], outs[0]) """
Line(circuit, n0, n1)
elif n.kind.startswith('XNOR3X'):
n.remove() NANGATE_ZN = TechLib(_nangate_common + r"""
n0 = add_and_connect(circuit, name+'~xor', 'XOR2', ins[0], ins[1], None) INV_X{1,2,4,8,16,32} input(A) output(ZN) ZN=INV1(A) ;
n1 = add_and_connect(circuit, name+'~xnor', 'XNOR2', None, ins[2], outs[0])
Line(circuit, n0, n1) AND2_X{1,2,4} input(A1,A2) output(ZN) ZN=AND2(A1,A2) ;
elif n.kind.startswith('AND4X'): AND3_X{1,2,4} input(A1,A2,A3) output(ZN) ZN=AND3(A1,A2,A3) ;
n.remove() AND4_X{1,2,4} input(A1,A2,A3,A4) output(ZN) ZN=AND4(A1,A2,A3,A4) ;
n0 = add_and_connect(circuit, name+'~and0', 'AND2', ins[0], ins[1], None) OR2_X{1,2,4} input(A1,A2) output(ZN) ZN=OR2(A1,A2) ;
n1 = add_and_connect(circuit, name+'~and1', 'AND2', ins[2], ins[3], None) OR3_X{1,2,4} input(A1,A2,A3) output(ZN) ZN=OR3(A1,A2,A3) ;
n2 = add_and_connect(circuit, name+'~and2', 'AND2', None, None, outs[0]) OR4_X{1,2,4} input(A1,A2,A3,A4) output(ZN) ZN=OR4(A1,A2,A3,A4) ;
Line(circuit, n0, n2) XOR2_X{1,2} input(A,B) output(Z) Z=XOR2(A,B) ;
Line(circuit, n1, n2) XNOR2_X{1,2} input(A,B) output(ZN) ZN=XNOR2(A,B) ;
elif n.kind.startswith('OR4X'): """)
n.remove() """An older NANGATE-variant that uses 'ZN' as output pin names for AND and OR gates.
n0 = add_and_connect(circuit, name+'~or0', 'OR2', ins[0], ins[1], None) """
n1 = add_and_connect(circuit, name+'~or1', 'OR2', ins[2], ins[3], None)
n2 = add_and_connect(circuit, name+'~or2', 'OR2', None, None, outs[0])
Line(circuit, n0, n2) SAED32 = TechLib(r"""
Line(circuit, n1, n2) NBUFFX{2,4,8,16,32}$ input(A) output(Y) Y=BUF1(A) ;
elif n.kind.startswith('NAND4X'): AOBUFX{1,2,4}$ input(A) output(Y) Y=BUF1(A) ;
n.remove() DELLN{1,2,3}X2$ input(A) output(Y) Y=BUF1(A) ;
n0 = add_and_connect(circuit, name+'~and0', 'AND2', ins[0], ins[1], None)
n1 = add_and_connect(circuit, name+'~and1', 'AND2', ins[2], ins[3], None) INVX{0,1,2,4,8,16,32}$ input(A) output(Y) Y=INV1(A) ;
n2 = add_and_connect(circuit, name+'~nand2', 'NAND2', None, None, outs[0]) AOINVX{1,2,4}$ input(A) output(Y) Y=INV1(A) ;
Line(circuit, n0, n2) IBUFFX{2,4,8,16,32}$ input(A) output(Y) Y=INV1(A) ;
Line(circuit, n1, n2)
elif n.kind.startswith('NOR4X'): TIEH$ output(Y) Y=__const1__() ;
n.remove() TIEL$ output(Y) Y=__const0__() ;
n0 = add_and_connect(circuit, name+'~or0', 'OR2', ins[0], ins[1], None)
n1 = add_and_connect(circuit, name+'~or1', 'OR2', ins[2], ins[3], None) HEAD2X{2,4,8,16,32}$ input(SLEEP) output(SLEEPOUT) SLEEPOUT=BUF1(SLEEP) ;
n2 = add_and_connect(circuit, name+'~nor2', 'NOR2', None, None, outs[0]) HEADX{2,4,8,16,32}$ input(SLEEP) ;
Line(circuit, n0, n2)
Line(circuit, n1, n2) FOOT2X{2,4,8,16,32}$ input(SLEEP) output(SLEEPOUT) SLEEPOUT=BUF1(SLEEP) ;
elif n.kind.startswith('FADDX'): FOOTX{2,4,8,16,32}$ input(SLEEP) ;
n.remove()
# forks for fan-outs ANTENNA$ input(INP) ;
f_a = add_and_connect(circuit, name + '~fork0', '__fork__', ins[0]) CLOAD1$ input(A) ;
f_b = add_and_connect(circuit, name + '~fork1', '__fork__', ins[1]) DCAP$ ;
f_ci = add_and_connect(circuit, name + '~fork2', '__fork__', ins[2]) DHFILLH2$ ;
f_ab = Node(circuit, name + '~fork3') DHFILLHL2$ ;
# sum-block DHFILLHLHLS11$ ;
n_xor0 = Node(circuit, name + '~xor0', 'XOR2') SHFILL{1,2,3,64,128}$ ;
Line(circuit, f_a, n_xor0)
Line(circuit, f_b, n_xor0) AND2X{1,2,4}$ input(A1,A2) output(Y) Y=AND2(A1,A2) ;
Line(circuit, n_xor0, f_ab) AND3X{1,2,4}$ input(A1,A2,A3) output(Y) Y=AND3(A1,A2,A3) ;
if len(outs) > 0 and outs[0] is not None: AND4X{1,2,4}$ input(A1,A2,A3,A4) output(Y) Y=AND4(A1,A2,A3,A4) ;
n_xor1 = add_and_connect(circuit, name + '~xor1', 'XOR2', None, None, outs[0]) OR2X{1,2,4}$ input(A1,A2) output(Y) Y=OR2(A1,A2) ;
Line(circuit, f_ab, n_xor1) OR3X{1,2,4}$ input(A1,A2,A3) output(Y) Y=OR3(A1,A2,A3) ;
Line(circuit, f_ci, n_xor1) OR4X{1,2,4}$ input(A1,A2,A3,A4) output(Y) Y=OR4(A1,A2,A3,A4) ;
# carry-block XOR2X{1,2}$ input(A1,A2) output(Y) Y=XOR2(A1,A2) ;
if len(outs) > 1 and outs[1] is not None: XOR3X{1,2}$ input(A1,A2,A3) output(Y) Y=XOR3(A1,A2,A3) ;
n_and0 = Node(circuit, name + '~and0', 'AND2') NAND2X{0,1,2,4}$ input(A1,A2) output(Y) Y=NAND2(A1,A2) ;
Line(circuit, f_ab, n_and0) NAND3X{0,1,2,4}$ input(A1,A2,A3) output(Y) Y=NAND3(A1,A2,A3) ;
Line(circuit, f_ci, n_and0) NAND4X{0,1}$ input(A1,A2,A3,A4) output(Y) Y=NAND4(A1,A2,A3,A4) ;
n_and1 = Node(circuit, name + '~and1', 'AND2') NOR2X{0,1,2,4}$ input(A1,A2) output(Y) Y=NOR2(A1,A2) ;
Line(circuit, f_a, n_and1) NOR3X{0,1,2,4}$ input(A1,A2,A3) output(Y) Y=NOR3(A1,A2,A3) ;
Line(circuit, f_b, n_and1) NOR4X{0,1}$ input(A1,A2,A3,A4) output(Y) Y=NOR4(A1,A2,A3,A4) ;
n_or = add_and_connect(circuit, name + '~or0', 'OR2', None, None, outs[1]) XNOR2X{1,2}$ input(A1,A2) output(Y) Y=XNOR2(A1,A2) ;
Line(circuit, n_and0, n_or) XNOR3X{1,2}$ input(A1,A2,A3) output(Y) Y=XNOR3(A1,A2,A3) ;
Line(circuit, n_and1, n_or)
elif n.kind.startswith('HADDX'): ISOLAND{,AO}X{1,2,4,8}$ input(ISO,D) output(Q) ISOB=NOT1(ISO) Q=AND2(ISOB,D) ;
n.remove() ISOLOR{,AO}X{1,2,4,8}$ input(ISO,D) output(Q) Q=OR2(ISO,D) ;
# forks for fan-outs
f_a = add_and_connect(circuit, name + '~fork0', '__fork__', ins[0]) AO21X{1,2}$ input(A1,A2,A3) output(Y) Y=AO21(A1,A2,A3) ;
f_b = add_and_connect(circuit, name + '~fork1', '__fork__', ins[1]) OA21X{1,2}$ input(A1,A2,A3) output(Y) Y=OA21(A1,A2,A3) ;
n_xor0 = add_and_connect(circuit, name + '~xor0', 'XOR2', None, None, outs[1]) AOI21X{1,2}$ input(A1,A2,A3) output(Y) Y=AOI21(A1,A2,A3) ;
Line(circuit, f_a, n_xor0) OAI21X{1,2}$ input(A1,A2,A3) output(Y) Y=OAI21(A1,A2,A3) ;
Line(circuit, f_b, n_xor0)
n_and0 = add_and_connect(circuit, name + '~and0', 'AND2', None, None, outs[0]) AO22X{1,2}$ input(A1,A2,A3,A4) output(Y) Y=AO22(A1,A2,A3,A4) ;
Line(circuit, f_a, n_and0) OA22X{1,2}$ input(A1,A2,A3,A4) output(Y) Y=OA22(A1,A2,A3,A4) ;
Line(circuit, f_b, n_and0) AOI22X{1,2}$ input(A1,A2,A3,A4) output(Y) Y=AOI22(A1,A2,A3,A4) ;
elif n.kind.startswith('MUX21X'): OAI22X{1,2}$ input(A1,A2,A3,A4) output(Y) Y=OAI22(A1,A2,A3,A4) ;
n.remove()
f_s = add_and_connect(circuit, name + '~fork0', '__fork__', ins[2]) MUX21X{1,2}$ input(A1,A2,S0) output(Y) Y=MUX21(A1,A2,S0) ;
n_not = Node(circuit, name + '~not', 'INV')
Line(circuit, f_s, n_not) AO221X{1,2}$ input(A1,A2,A3,A4,A5) output(Y) A=AO22(A1,A2,A3,A4) Y=OR2(A5,A) ;
n_and0 = add_and_connect(circuit, name + '~and0', 'AND2', ins[0]) OA221X{1,2}$ input(A1,A2,A3,A4,A5) output(Y) A=OA22(A1,A2,A3,A4) Y=AND2(A5,A) ;
n_and1 = add_and_connect(circuit, name + '~and1', 'AND2', ins[1]) AOI221X{1,2}$ input(A1,A2,A3,A4,A5) output(Y) A=AO22(A1,A2,A3,A4) Y=NOR2(A5,A) ;
n_or0 = add_and_connect(circuit, name + '~or0', 'OR2', None, None, outs[0]) OAI221X{1,2}$ input(A1,A2,A3,A4,A5) output(Y) A=OA22(A1,A2,A3,A4) Y=NAND2(A5,A) ;
Line(circuit, n_not, n_and0)
Line(circuit, f_s, n_and1) AO222X{1,2}$ input(A1,A2,A3,A4,A5,A6) output(Y) A=AO22(A1,A2,A3,A4) Y=AO21(A5,A6,A) ;
Line(circuit, n_and0, n_or0) OA222X{1,2}$ input(A1,A2,A3,A4,A5,A6) output(Y) A=OA22(A1,A2,A3,A4) Y=OA21(A5,A6,A) ;
Line(circuit, n_and1, n_or0) AOI222X{1,2}$ input(A1,A2,A3,A4,A5,A6) output(Y) A=AO22(A1,A2,A3,A4) Y=AOI21(A5,A6,A) ;
elif n.kind.startswith('DFFSSR'): OAI222X{1,2}$ input(A1,A2,A3,A4,A5,A6) output(Y) A=OA22(A1,A2,A3,A4) Y=OAI21(A5,A6,A) ;
n.kind = 'DFFX1'
n_and0 = add_and_connect(circuit, name + '~and0', 'AND2', ins[0], ins[2], None) MUX41X{1,2}$ input(A1,A2,A3,A4,S0,S1) output(Y) A=MUX21(A1,A2,S0) B=MUX21(A3,A4,S0) Y=MUX21(A,B,S1) ;
Line(circuit, n_and0, (n, 0))
DEC24X{1,2}$ input(A0,A1) output(Y0,Y1,Y2,Y3) A0B=INV1(A0) A1B=INV1(A1) Y0=NOR2(A0,A1) Y1=AND(A0,A1B) Y2=AND(A0B,A1) Y3=AND(A0,A1) ;
FADDX{1,2}$ input(A,B,CI) output(S,CO) AB=XOR2(A,B) CO=XOR2(AB,CI) S=AO22(AB,CI,A,B) ;
HADDX{1,2}$ input(A0,B0) output(SO,C1) C1=XOR2(A0,B0) SO=AND2(A0,B0) ;
{,AO}DFFARX{1,2}$ input(D,CLK,RSTB) output(Q,QN) DR=AND2(D,RSTB) Q=DFF(DR,CLK) QN=INV1(Q) ;
DFFASRX{1,2}$ input(D,CLK,RSTB,SETB) output(Q,QN) DR=AND2(D,RSTB) SET=INV1(SETB) DRS=OR2(DR,SET) Q=DFF(DRS,CLK) QN=INV1(Q) ;
DFFASX{1,2}$ input(D,CLK,SETB) output(Q,QN) SET=INV1(SETB) DS=OR2(D,SET) Q=DFF(DS,CLK) QN=INV1(Q) ;
DFFSSRX{1,2}$ input(CLK,D,RSTB,SETB) output(Q,QN) DR=AND2(D,RSTB) SET=INV1(SETB) DRS=OR2(DR,SET) Q=DFF(DRS,CLK) QN=INV1(Q) ;
DFFX{1,2}$ input(D,CLK) output(Q,QN) Q=DFF(D,CLK) QN=INV1(Q) ;
SDFFARX{1,2}$ input(D,CLK,RSTB,SE,SI) output(Q,QN) DR=AND2(D,RSTB) DI=MUX21(DR,SI,SE) Q=DFF(DI,CLK) QN=INV1(Q) ;
SDFFASRSX{1,2}$ input(D,CLK,RSTB,SETB,SE,SI) output(Q,QN,SO) DR=AND2(D,RSTB) SET=INV1(SETB) DRS=OR2(DR,SET) DI=MUX21(DRS,SI,SE) Q=DFF(DI,CLK) QN=INV1(Q) SO=BUF1(Q) ;
SDFFASRX{1,2}$ input(D,CLK,RSTB,SETB,SE,SI) output(Q,QN) DR=AND2(D,RSTB) SET=INV1(SETB) DRS=OR2(DR,SET) DI=MUX21(DRS,SI,SE) Q=DFF(DI,CLK) QN=INV1(Q) ;
SDFFASX{1,2}$ input(D,CLK,SETB,SE,SI) output(Q,QN) SET=INV1(SETB) DS=OR2(D,SET) DI=MUX21(DS,SI,SE) Q=DFF(DI,CLK) QN=INV1(Q) ;
SDFFSSRX{1,2}$ input(CLK,D,RSTB,SETB,SI,SE) output(Q,QN) DR=AND2(D,RSTB) SET=INV1(SETB) DRS=OR2(DR,SET) DI=MUX21(DRS,SI,SE) Q=DFF(DI,CLK) QN=INV1(Q) ;
SDFFX{1,2}$ input(D,CLK,SE,SI) output(Q,QN) DI=MUX21(D,SI,SE) Q=DFF(DI,CLK) QN=INV1(Q) ;
LATCHX{1,2}$ input(D,CLK) output(Q,QN) Q=LATCH(D,CLK) QN=INV1(Q) ;
""".replace('$','_RVT'))
"""The SAED 32nm educational technology library.
It defines all cells except: negative-edge flip-flops, tri-state, latches, clock gating, level shifters
"""
SAED90 = TechLib(r"""
NBUFFX{2,4,8,16,32}$ input(INP) output(Z) Z=BUF1(INP) ;
AOBUFX{1,2,4}$ input(INP) output(Z) Z=BUF1(INP) ;
DELLN{1,2,3}X2$ input(INP) output(Z)Z=BUF1(INP) ;
INVX{0,1,2,4,8,16,32}$ input(INP) output(ZN) ZN=INV1(INP) ;
AOINVX{1,2,4}$ input(INP) output(ZN) ZN=INV1(INP) ;
IBUFFX{2,4,8,16,32}$ input(INP) output(ZN) ZN=INV1(INP) ;
TIEH$ output(Z) Z=__const1__() ;
TIEL$ output(ZN) ZN=__const0__() ;
HEAD2X{2,4,8,16,32}$ input(SLEEP) output(SLEEPOUT) SLEEPOUT=BUF1(SLEEP) ;
HEADX{2,4,8,16,32}$ input(SLEEP) ;
ANTENNA$ input(INP) ;
CLOAD1$ input(INP) ;
DCAP$ ;
DHFILL{HLH,LHL}2 ;
DHFILLHLHLS11$ ;
SHFILL{1,2,3,64,128}$ ;
AND2X{1,2,4}$ input(IN1,IN2) output(Q) Q=AND2(IN1,IN2) ;
AND3X{1,2,4}$ input(IN1,IN2,IN3) output(Q) Q=AND3(IN1,IN2,IN3) ;
AND4X{1,2,4}$ input(IN1,IN2,IN3,IN4) output(Q) Q=AND4(IN1,IN2,IN3,IN4) ;
OR2X{1,2,4}$ input(IN1,IN2) output(Q) Q=OR2(IN1,IN2) ;
OR3X{1,2,4}$ input(IN1,IN2,IN3) output(Q) Q=OR3(IN1,IN2,IN3) ;
OR4X{1,2,4}$ input(IN1,IN2,IN3,IN4) output(Q) Q=OR4(IN1,IN2,IN3,IN4) ;
XOR2X{1,2}$ input(IN1,IN2) output(Q) Q=XOR2(IN1,IN2) ;
XOR3X{1,2}$ input(IN1,IN2,IN3) output(Q) Q=XOR3(IN1,IN2,IN3) ;
NAND2X{0,1,2,4}$ input(IN1,IN2) output(QN) QN=NAND2(IN1,IN2) ;
NAND3X{0,1,2,4}$ input(IN1,IN2,IN3) output(QN) QN=NAND3(IN1,IN2,IN3) ;
NAND4X{0,1}$ input(IN1,IN2,IN3,IN4) output(QN) QN=NAND4(IN1,IN2,IN3,IN4) ;
NOR2X{0,1,2,4}$ input(IN1,IN2) output(QN) QN=NOR2(IN1,IN2) ;
NOR3X{0,1,2,4}$ input(IN1,IN2,IN3) output(QN) QN=NOR3(IN1,IN2,IN3) ;
NOR4X{0,1}$ input(IN1,IN2,IN3,IN4) output(QN) QN=NOR4(IN1,IN2,IN3,IN4) ;
XNOR2X{1,2}$ input(IN1,IN2) output(Q) Q=XNOR2(IN1,IN2) ;
XNOR3X{1,2}$ input(IN1,IN2,IN3) output(Q) Q=XNOR3(IN1,IN2,IN3) ;
ISOLAND{,AO}X{1,2,4,8}$ input(ISO,D) output(Q) ISOB=NOT1(ISO) Q=AND2(ISOB,D) ;
ISOLOR{,AO}X{1,2,4,8}$ input(ISO,D) output(Q) Q=OR2(ISO,D) ;
AO21X{1,2}$ input(IN1,IN2,IN3) output(Q) Q=AO21(IN1,IN2,IN3) ;
OA21X{1,2}$ input(IN1,IN2,IN3) output(Q) Q=OA21(IN1,IN2,IN3) ;
AOI21X{1,2}$ input(IN1,IN2,IN3) output(QN) QN=AOI21(IN1,IN2,IN3) ;
OAI21X{1,2}$ input(IN1,IN2,IN3) output(QN) QN=OAI21(IN1,IN2,IN3) ;
AO22X{1,2}$ input(IN1,IN2,IN3,IN4) output(Q) Q=AO22(IN1,IN2,IN3,IN4) ;
OA22X{1,2}$ input(IN1,IN2,IN3,IN4) output(Q) Q=OA22(IN1,IN2,IN3,IN4) ;
AOI22X{1,2}$ input(IN1,IN2,IN3,IN4) output(QN) QN=AOI22(IN1,IN2,IN3,IN4) ;
OAI22X{1,2}$ input(IN1,IN2,IN3,IN4) output(QN) QN=OAI22(IN1,IN2,IN3,IN4) ;
MUX21X{1,2}$ input(IN1,IN2,S) output(Q) Q=MUX21(IN1,IN2,S) ;
AO221X{1,2}$ input(IN1,IN2,IN3,IN4,IN5) output(Q) A=AO22(IN1,IN2,IN3,IN4) Q=OR2(IN5,A) ;
OA221X{1,2}$ input(IN1,IN2,IN3,IN4,IN5) output(Q) A=OA22(IN1,IN2,IN3,IN4) Q=AND2(IN5,A) ;
AOI221X{1,2}$ input(IN1,IN2,IN3,IN4,IN5) output(QN) A=AO22(IN1,IN2,IN3,IN4) QN=NOR2(IN5,A) ;
OAI221X{1,2}$ input(IN1,IN2,IN3,IN4,IN5) output(QN) A=OA22(IN1,IN2,IN3,IN4) QN=NAND2(IN5,A) ;
AO222X{1,2}$ input(IN1,IN2,IN3,IN4,IN5,IN6) output(Q) A=AO22(IN1,IN2,IN3,IN4) Q=AO21(IN5,IN6,A) ;
OA222X{1,2}$ input(IN1,IN2,IN3,IN4,IN5,IN6) output(Q) A=OA22(IN1,IN2,IN3,IN4) Q=OA21(IN5,IN6,A) ;
AOI222X{1,2}$ input(IN1,IN2,IN3,IN4,IN5,IN6) output(QN) A=AO22(IN1,IN2,IN3,IN4) QN=AOI21(IN5,IN6,A) ;
OAI222X{1,2}$ input(IN1,IN2,IN3,IN4,IN5,IN6) output(QN) A=OA22(IN1,IN2,IN3,IN4) QN=OAI21(IN5,IN6,A) ;
MUX41X{1,2}$ input(IN1,IN2,IN3,IN4,S0,S1) output(Q) A=MUX21(IN1,IN2,S0) B=MUX21(IN3,IN4,S0) Q=MUX21(A,B,S1) ;
DEC24X{1,2}$ input(IN1,IN2) output(Q0,Q1,Q2,Q3) IN1B=INV1(IN1) IN2B=INV1(IN2) Q0=NOR2(IN1,IN2) Q1=AND(IN1,IN2B) Q2=AND(IN1B,IN2) Q3=AND(IN1,IN2) ;
FADDX{1,2}$ input(A,B,CI) output(S,CO) AB=XOR2(A,B) CO=XOR2(AB,CI) S=AO22(AB,CI,A,B) ;
HADDX{1,2}$ input(A0,B0) output(SO,C1) C1=XOR2(A0,B0) SO=AND2(A0,B0) ;
{,AO}DFFARX{1,2}$ input(D,CLK,RSTB) output(Q,QN) DR=AND2(D,RSTB) Q=DFF(DR,CLK) QN=INV1(Q) ;
DFFASRX{1,2}$ input(D,CLK,RSTB,SETB) output(Q,QN) DR=AND2(D,RSTB) SET=INV1(SETB) DRS=OR2(DR,SET) Q=DFF(DRS,CLK) QN=INV1(Q) ;
DFFASX{1,2}$ input(D,CLK,SETB) output(Q,QN) SET=INV1(SETB) DS=OR2(D,SET) Q=DFF(DS,CLK) QN=INV1(Q) ;
DFFSSRX{1,2}$ input(CLK,D,RSTB,SETB) output(Q,QN) DR=AND2(D,RSTB) SET=INV1(SETB) DRS=OR2(DR,SET) Q=DFF(DRS,CLK) QN=INV1(Q) ;
DFFX{1,2}$ input(D,CLK) output(Q,QN) Q=DFF(D,CLK) QN=INV1(Q) ;
SDFFARX{1,2}$ input(D,CLK,RSTB,SE,SI) output(Q,QN) DR=AND2(D,RSTB) DI=MUX21(DR,SI,SE) Q=DFF(DI,CLK) QN=INV1(Q) ;
SDFFASRSX{1,2}$ input(D,CLK,RSTB,SETB,SE,SI) output(Q,QN,S0) DR=AND2(D,RSTB) SET=INV1(SETB) DRS=OR2(DR,SET) DI=MUX21(DRS,SI,SE) Q=DFF(DI,CLK) QN=INV1(Q) S0=BUF1(Q) ;
SDFFASRX{1,2}$ input(D,CLK,RSTB,SETB,SE,SI) output(Q,QN) DR=AND2(D,RSTB) SET=INV1(SETB) DRS=OR2(DR,SET) DI=MUX21(DRS,SI,SE) Q=DFF(DI,CLK) QN=INV1(Q) ;
SDFFASX{1,2}$ input(D,CLK,SETB,SE,SI) output(Q,QN) SET=INV1(SETB) DS=OR2(D,SET) DI=MUX21(DS,SI,SE) Q=DFF(DI,CLK) QN=INV1(Q) ;
SDFFSSRX{1,2}$ input(CLK,D,RSTB,SETB,SI,SE) output(Q,QN) DR=AND2(D,RSTB) SET=INV1(SETB) DRS=OR2(DR,SET) DI=MUX21(DRS,SI,SE) Q=DFF(DI,CLK) QN=INV1(Q) ;
SDFFX{1,2}$ input(D,CLK,SE,SI) output(Q,QN) DI=MUX21(D,SI,SE) Q=DFF(DI,CLK) QN=INV1(Q) ;
LATCHX{1,2}$ input(D,CLK) output(Q,QN) Q=LATCH(D,CLK) QN=INV1(Q) ;
""".replace('$','{,_LVT,_HVT}'))
"""The SAED 90nm educational technology library.
It defines all cells except: negative-edge flip-flops, tri-state, latches, clock gating, level shifters
"""

186
src/kyupy/verilog.py

@ -1,16 +1,16 @@
"""A simple and incomplete parser for Verilog files. """A simple and incomplete parser for Verilog files.
The main purpose of this parser is to load synthesized, non-hierarchical (flat) gate-level netlists. The main purpose of this parser is to load synthesized, non-hierarchical (flat) gate-level netlists.
It supports only a very limited subset of Verilog. It supports only a subset of Verilog.
""" """
from collections import namedtuple from collections import namedtuple
from lark import Lark, Transformer from lark import Lark, Transformer, Tree
from . import log, readtext from . import log, readtext
from .circuit import Circuit, Node, Line from .circuit import Circuit, Node, Line
from .techlib import TechLib from .techlib import NANGATE
Instantiation = namedtuple('Instantiation', ['type', 'name', 'pins']) Instantiation = namedtuple('Instantiation', ['type', 'name', 'pins'])
@ -35,51 +35,89 @@ class SignalDeclaration:
class VerilogTransformer(Transformer): class VerilogTransformer(Transformer):
def __init__(self, branchforks=False, tlib=TechLib()): def __init__(self, branchforks=False, tlib=NANGATE):
super().__init__() super().__init__()
self._signal_declarations = {}
self.branchforks = branchforks self.branchforks = branchforks
self.tlib = tlib self.tlib = tlib
@staticmethod @staticmethod
def name(args): def name(args):
s = args[0].value s = args[0].value
if s[0] == '\\': return s[1:-1] if s[0] == '\\' else s
s = s[1:-1]
return s @staticmethod
def namedpin(args):
return tuple(args) if len(args) > 1 else (args[0], None)
@staticmethod @staticmethod
def instantiation(args): def instantiation(args):
return Instantiation(args[0], args[1], pinmap = {}
dict((pin.children[0], for idx, pin in enumerate(args[2:]):
pin.children[1]) for pin in args[2:] if len(pin.children) > 1)) p = pin.children[0]
if isinstance(p, tuple): # named pin
if p[1] is not None:
pinmap[p[0]] = p[1]
else: # unnamed pin
pinmap[idx] = p
return Instantiation(args[0], args[1], pinmap)
def range(self, args): def range(self, args):
left = int(args[0].value) left = int(args[0].value)
right = int(args[1].value) right = int(args[1].value) if len(args) > 1 else left
return range(left, right+1) if left <= right else range(left, right-1, -1) return range(left, right+1) if left <= right else range(left, right-1, -1)
def sigsel(self, args):
if len(args) > 1 and isinstance(args[1], range):
l = [f'{args[0]}[{i}]' for i in args[1]]
return l if len(l) > 1 else l[0]
elif "'" in args[0]:
width, rest = args[0].split("'")
width = int(width)
base, const = rest[0], rest[1:]
const = int(const, {'b': 2, 'd':10, 'h':16}[base.lower()])
l = []
for _ in range(width):
l.insert(0, "1'b1" if (const & 1) else "1'b0")
const >>= 1
return l if len(l) > 1 else l[0]
else:
return args[0]
def concat(self, args):
sigs = []
for a in args:
if isinstance(a, list):
sigs += a
else:
sigs.append(a)
return sigs
def declaration(self, kind, args): def declaration(self, kind, args):
rnge = None rnge = None
if isinstance(args[0], range): if isinstance(args[0], range):
rnge = args[0] rnge = args[0]
args = args[1:] args = args[1:]
for sd in [SignalDeclaration(kind, signal, rnge) for signal in args]: return [SignalDeclaration(kind, signal, rnge) for signal in args]
if kind != 'wire' or sd.basename not in self._signal_declarations:
self._signal_declarations[sd.basename] = sd
def input(self, args): self.declaration("input", args) def input(self, args): return self.declaration("input", args)
def output(self, args): self.declaration("output", args) def output(self, args): return self.declaration("output", args)
def inout(self, args): self.declaration("input", args) # just treat as input def inout(self, args): return self.declaration("input", args) # just treat as input
def wire(self, args): self.declaration("wire", args) def wire(self, args): return self.declaration("wire", args)
def module(self, args): def module(self, args):
c = Circuit(args[0]) c = Circuit(args[0])
positions = {} positions = {}
pos = 0 pos = 0
const_count = 0 const_count = 0
sig_decls = {}
for decls in args[2:]: # pass 0: collect signal declarations
if isinstance(decls, list):
if len(decls) > 0 and isinstance(decls[0], SignalDeclaration):
for decl in decls:
if decl.basename not in sig_decls or sig_decls[decl.basename].kind == 'wire':
sig_decls[decl.basename] = decl
for intf_sig in args[1].children: for intf_sig in args[1].children:
for name in self._signal_declarations[intf_sig].names: for name in sig_decls[intf_sig].names:
positions[name] = pos positions[name] = pos
pos += 1 pos += 1
assignments = [] assignments = []
@ -88,28 +126,47 @@ class VerilogTransformer(Transformer):
n = Node(c, stmt.name, kind=stmt.type) n = Node(c, stmt.name, kind=stmt.type)
for p, s in stmt.pins.items(): for p, s in stmt.pins.items():
if self.tlib.pin_is_output(n.kind, p): if self.tlib.pin_is_output(n.kind, p):
if s in sig_decls:
s = sig_decls[s].names
if isinstance(s, list) and len(s) == 1:
s = s[0]
Line(c, (n, self.tlib.pin_index(stmt.type, p)), Node(c, s)) Line(c, (n, self.tlib.pin_index(stmt.type, p)), Node(c, s))
elif stmt is not None and stmt.data == 'assign': elif hasattr(stmt, 'data') and stmt.data == 'assign':
assignments.append((stmt.children[0], stmt.children[1])) assignments.append((stmt.children[0], stmt.children[1]))
for sd in self._signal_declarations.values(): for sd in sig_decls.values():
if sd.kind == 'output' or sd.kind == 'input': if sd.kind == 'output' or sd.kind == 'input':
for name in sd.names: for name in sd.names:
n = Node(c, name, kind=sd.kind) n = Node(c, name, kind=sd.kind)
if name in positions: if name in positions:
c.interface[positions[name]] = n c.io_nodes[positions[name]] = n
if sd.kind == 'input': if sd.kind == 'input':
Line(c, n, Node(c, name)) Line(c, n, Node(c, name))
for s1, s2 in assignments: # pass 1.5: process signal assignments for target, source in assignments: # pass 1.5: process signal assignments
if s1 in c.forks: target_sigs = []
assert s2 not in c.forks, 'assignment between two driven signals' if not isinstance(target, list): target = [target]
Line(c, c.forks[s1], Node(c, s2)) for s in target:
elif s2 in c.forks: if s in sig_decls:
assert s1 not in c.forks, 'assignment between two driven signals' target_sigs += sig_decls[s].names
Line(c, c.forks[s2], Node(c, s1)) else:
elif s2.startswith("1'b"): target_sigs.append(s)
cnode = Node(c, f'__const{s2[3]}_{const_count}__', f'__const{s2[3]}__') source_sigs = []
const_count += 1 if not isinstance(source, list): source = [source]
Line(c, cnode, Node(c, s1)) for s in source:
if s in sig_decls:
source_sigs += sig_decls[s].names
else:
source_sigs.append(s)
for t, s in zip(target_sigs, source_sigs):
if t in c.forks:
assert s not in c.forks, 'assignment between two driven signals'
Line(c, c.forks[t], Node(c, s))
elif s in c.forks:
assert t not in c.forks, 'assignment between two driven signals'
Line(c, c.forks[s], Node(c, t))
elif s.startswith("1'b"):
cnode = Node(c, f'__const{s[3]}_{const_count}__', f'__const{s[3]}__')
const_count += 1
Line(c, cnode, Node(c, t))
for stmt in args[2:]: # pass 2: connect signals to readers for stmt in args[2:]: # pass 2: connect signals to readers
if isinstance(stmt, Instantiation): if isinstance(stmt, Instantiation):
for p, s in stmt.pins.items(): for p, s in stmt.pins.items():
@ -122,28 +179,34 @@ class VerilogTransformer(Transformer):
s = cname s = cname
Line(c, cnode, Node(c, s)) Line(c, cnode, Node(c, s))
if s not in c.forks: if s not in c.forks:
log.warn(f'Signal not driven: {s}') if f'{s}[0]' in c.forks: # actually a 1-bit bus?
Node(c, s) # generate fork here s = f'{s}[0]'
else:
log.warn(f'Signal not driven: {s}')
Node(c, s) # generate fork here
fork = c.forks[s] fork = c.forks[s]
if self.branchforks: if self.branchforks:
branchfork = Node(c, fork.name + "~" + n.name + "/" + p) branchfork = Node(c, fork.name + "~" + n.name + "/" + p)
Line(c, fork, branchfork) Line(c, fork, branchfork)
fork = branchfork fork = branchfork
Line(c, fork, (n, self.tlib.pin_index(stmt.type, p))) Line(c, fork, (n, self.tlib.pin_index(stmt.type, p)))
for sd in self._signal_declarations.values(): for sd in sig_decls.values():
if sd.kind == 'output': if sd.kind == 'output':
for name in sd.names: for name in sd.names:
if name not in c.forks: if name not in c.forks:
log.warn(f'Output not driven: {name}') if f'{name}[0]' in c.forks: # actually a 1-bit bus?
else: name = f'{name}[0]'
Line(c, c.forks[name], c.cells[name]) else:
log.warn(f'Output not driven: {name}')
continue
Line(c, c.forks[name], c.cells[name])
return c return c
@staticmethod @staticmethod
def start(args): return args[0] if len(args) == 1 else args def start(args): return args[0] if len(args) == 1 else args
GRAMMAR = """ GRAMMAR = r"""
start: (module)* start: (module)*
module: "module" name parameters ";" (_statement)* "endmodule" module: "module" name parameters ";" (_statement)* "endmodule"
parameters: "(" [ _namelist ] ")" parameters: "(" [ _namelist ] ")"
@ -153,36 +216,45 @@ GRAMMAR = """
inout: "inout" range? _namelist ";" inout: "inout" range? _namelist ";"
tri: "tri" range? _namelist ";" tri: "tri" range? _namelist ";"
wire: "wire" range? _namelist ";" wire: "wire" range? _namelist ";"
assign: "assign" name "=" name ";" assign: "assign" sigsel "=" sigsel ";"
instantiation: name name "(" [ pin ( "," pin )* ] ")" ";" instantiation: name name "(" [ pin ( "," pin )* ] ")" ";"
pin: "." name "(" name? ")" pin: namedpin | sigsel
range: "[" /[0-9]+/ ":" /[0-9]+/ "]" namedpin: "." name "(" sigsel? ")"
range: "[" /[0-9]+/ (":" /[0-9]+/)? "]"
sigsel: name range? | concat
concat: "{" sigsel ( "," sigsel )* "}"
_namelist: name ( "," name )* _namelist: name ( "," name )*
name: ( /[a-z_][a-z0-9_\\[\\]]*/i | /\\\\[^\\t \\r\\n]+[\\t \\r\\n](\\[[0-9]+\\])?/i | /1'b0/i | /1'b1/i ) name: ( /[a-z_][a-z0-9_]*/i | /\\[^\t \r\n]+[\t \r\n]/i | /[0-9]+'[bdh][0-9a-f]+/i )
COMMENT: "//" /[^\\n]*/ %import common.NEWLINE
%ignore ( /\\r?\\n/ | COMMENT )+ COMMENT: /\/\*(\*(?!\/)|[^*])*\*\// | /\(\*(\*(?!\))|[^*])*\*\)/ | "//" /(.)*/ NEWLINE
%ignore /[\\t \\f]+/ %ignore ( /\r?\n/ | COMMENT )+
%ignore /[\t \f]+/
""" """
def parse(text, *, branchforks=False, tlib=TechLib()): def parse(text, tlib=NANGATE, branchforks=False):
"""Parses the given ``text`` as Verilog code. """Parses the given ``text`` as Verilog code.
:param text: A string with Verilog code. :param text: A string with Verilog code.
:param tlib: A technology library object that defines all known cells.
:type tlib: :py:class:`~kyupy.techlib.TechLib`
:param branchforks: If set to ``True``, the returned circuit will include additional `forks` on each fanout branch. :param branchforks: If set to ``True``, the returned circuit will include additional `forks` on each fanout branch.
These forks are needed to correctly annotate interconnect delays These forks are needed to correctly annotate interconnect delays
(see :py:func:`kyupy.sdf.DelayFile.annotation`). (see :py:func:`~kyupy.sdf.DelayFile.interconnects()`).
:param tlib: A technology library object that provides pin name mappings. :return: A :py:class:`~kyupy.circuit.Circuit` object.
:type tlib: :py:class:`~kyupy.techlib.TechLib`
:return: A :class:`~kyupy.circuit.Circuit` object.
""" """
return Lark(GRAMMAR, parser="lalr", transformer=VerilogTransformer(branchforks, tlib)).parse(text) return Lark(GRAMMAR, parser="lalr", transformer=VerilogTransformer(branchforks, tlib)).parse(text)
def load(file, *args, **kwargs): def load(file, tlib=NANGATE, branchforks=False):
"""Parses the contents of ``file`` as Verilog code. """Parses the contents of ``file`` as Verilog code.
The given file may be gzip compressed. Takes the same keyword arguments as :py:func:`parse`. :param file: A file name or a file handle. Files with `.gz`-suffix are decompressed on-the-fly.
:param tlib: A technology library object that defines all known cells.
:type tlib: :py:class:`~kyupy.techlib.TechLib`
:param branchforks: If set to ``True``, the returned circuit will include additional `forks` on each fanout branch.
These forks are needed to correctly annotate interconnect delays
(see :py:func:`~kyupy.sdf.DelayFile.interconnects()`).
:return: A :py:class:`~kyupy.circuit.Circuit` object.
""" """
return parse(readtext(file), *args, **kwargs) return parse(readtext(file), tlib, branchforks)

1136
src/kyupy/wave_sim.py

File diff suppressed because it is too large Load Diff

BIN
tests/b14.sdf.gz

Binary file not shown.

BIN
tests/b14.stuck.stil.gz

Binary file not shown.

BIN
tests/b14.transition.stil.gz

Binary file not shown.

BIN
tests/b14.v.gz

Binary file not shown.

BIN
tests/b15_2ig.sa_nf.stil.gz

Binary file not shown.

BIN
tests/b15_2ig.sdf.gz

Binary file not shown.

BIN
tests/b15_2ig.tf_nf.stil.gz

Binary file not shown.

BIN
tests/b15_2ig.v.gz

Binary file not shown.

BIN
tests/b15_4ig.sdf.gz

Binary file not shown.

BIN
tests/b15_4ig.v.gz

Binary file not shown.

14
tests/conftest.py

@ -1,8 +1,20 @@
import pytest import pytest
@pytest.fixture @pytest.fixture(scope='session')
def mydir(): def mydir():
import os import os
from pathlib import Path from pathlib import Path
return Path(os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))) return Path(os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))))
@pytest.fixture(scope='session')
def b15_2ig_circuit(mydir):
from kyupy import verilog
from kyupy.techlib import SAED32
return verilog.load(mydir / 'b15_2ig.v.gz', branchforks=True, tlib=SAED32)
@pytest.fixture(scope='session')
def b15_2ig_delays(mydir, b15_2ig_circuit):
from kyupy import sdf
from kyupy.techlib import SAED32
return sdf.load(mydir / 'b15_2ig.sdf.gz').iopaths(b15_2ig_circuit, tlib=SAED32)[1:2]

335
tests/rng_haltonBase2.synth_yosys.v

@ -0,0 +1,335 @@
/* Generated by Yosys 0.9 (git sha1 UNKNOWN, gcc 4.8.5 -fPIC -Os) */
(* top = 1 *)
(* src = "rng_haltonBase2.v:1" *)
module rng1(clk, reset, o_output);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
wire [11:0] _00_;
wire _01_;
wire _02_;
wire _03_;
wire _04_;
wire _05_;
wire _06_;
wire _07_;
wire _08_;
wire _09_;
wire _10_;
wire _11_;
wire _12_;
wire _13_;
wire _14_;
wire _15_;
wire _16_;
wire _17_;
wire _18_;
wire _19_;
wire _20_;
wire _21_;
wire _22_;
wire _23_;
wire _24_;
wire _25_;
wire _26_;
wire _27_;
wire _28_;
wire _29_;
wire _30_;
wire _31_;
wire _32_;
wire _33_;
wire _34_;
(* src = "rng_haltonBase2.v:2" *)
input clk;
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:12" *)
wire \halton.clk ;
(* init = 12'h000 *)
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:17" *)
wire [11:0] \halton.counter ;
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:14" *)
wire [11:0] \halton.o_output ;
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:13" *)
wire \halton.reset ;
(* src = "rng_haltonBase2.v:4" *)
output [11:0] o_output;
(* src = "rng_haltonBase2.v:3" *)
input reset;
AND2X1 _35_ (
.IN1(\halton.counter [1]),
.IN2(\halton.counter [0]),
.Q(_01_)
);
NOR2X0 _36_ (
.IN1(\halton.counter [1]),
.IN2(\halton.counter [0]),
.QN(_02_)
);
NOR3X0 _37_ (
.IN1(reset),
.IN2(_01_),
.IN3(_02_),
.QN(_00_[1])
);
AND2X1 _38_ (
.IN1(\halton.counter [2]),
.IN2(_01_),
.Q(_03_)
);
NOR2X0 _39_ (
.IN1(\halton.counter [2]),
.IN2(_01_),
.QN(_04_)
);
NOR3X0 _40_ (
.IN1(reset),
.IN2(_03_),
.IN3(_04_),
.QN(_00_[2])
);
AND4X1 _41_ (
.IN1(\halton.counter [1]),
.IN2(\halton.counter [0]),
.IN3(\halton.counter [2]),
.IN4(\halton.counter [3]),
.Q(_05_)
);
NOR2X0 _42_ (
.IN1(\halton.counter [3]),
.IN2(_03_),
.QN(_06_)
);
NOR3X0 _43_ (
.IN1(reset),
.IN2(_05_),
.IN3(_06_),
.QN(_00_[3])
);
AND2X1 _44_ (
.IN1(\halton.counter [4]),
.IN2(_05_),
.Q(_07_)
);
NOR2X0 _45_ (
.IN1(\halton.counter [4]),
.IN2(_05_),
.QN(_08_)
);
NOR3X0 _46_ (
.IN1(reset),
.IN2(_07_),
.IN3(_08_),
.QN(_00_[4])
);
AND2X1 _47_ (
.IN1(\halton.counter [5]),
.IN2(_07_),
.Q(_09_)
);
NOR2X0 _48_ (
.IN1(\halton.counter [5]),
.IN2(_07_),
.QN(_10_)
);
NOR3X0 _49_ (
.IN1(reset),
.IN2(_09_),
.IN3(_10_),
.QN(_00_[5])
);
AND4X1 _50_ (
.IN1(\halton.counter [4]),
.IN2(\halton.counter [5]),
.IN3(\halton.counter [6]),
.IN4(_05_),
.Q(_11_)
);
NOR2X0 _51_ (
.IN1(\halton.counter [6]),
.IN2(_09_),
.QN(_12_)
);
NOR3X0 _52_ (
.IN1(reset),
.IN2(_11_),
.IN3(_12_),
.QN(_00_[6])
);
AND2X1 _53_ (
.IN1(\halton.counter [7]),
.IN2(_11_),
.Q(_13_)
);
NOR2X0 _54_ (
.IN1(\halton.counter [7]),
.IN2(_11_),
.QN(_14_)
);
NOR3X0 _55_ (
.IN1(reset),
.IN2(_13_),
.IN3(_14_),
.QN(_00_[7])
);
AND3X1 _56_ (
.IN1(\halton.counter [7]),
.IN2(\halton.counter [8]),
.IN3(_11_),
.Q(_15_)
);
NOR2X0 _57_ (
.IN1(\halton.counter [8]),
.IN2(_13_),
.QN(_16_)
);
NOR3X0 _58_ (
.IN1(reset),
.IN2(_15_),
.IN3(_16_),
.QN(_00_[8])
);
AND4X1 _59_ (
.IN1(\halton.counter [7]),
.IN2(\halton.counter [8]),
.IN3(\halton.counter [9]),
.IN4(_11_),
.Q(_17_)
);
NOR2X0 _60_ (
.IN1(\halton.counter [9]),
.IN2(_15_),
.QN(_18_)
);
NOR3X0 _61_ (
.IN1(reset),
.IN2(_17_),
.IN3(_18_),
.QN(_00_[9])
);
AND2X1 _62_ (
.IN1(\halton.counter [10]),
.IN2(_17_),
.Q(_19_)
);
NOR2X0 _63_ (
.IN1(\halton.counter [10]),
.IN2(_17_),
.QN(_20_)
);
NOR3X0 _64_ (
.IN1(reset),
.IN2(_19_),
.IN3(_20_),
.QN(_00_[10])
);
AND3X1 _65_ (
.IN1(\halton.counter [10]),
.IN2(\halton.counter [11]),
.IN3(_17_),
.Q(_21_)
);
AOI21X1 _66_ (
.IN1(\halton.counter [10]),
.IN2(_17_),
.IN3(\halton.counter [11]),
.QN(_22_)
);
NOR3X0 _67_ (
.IN1(reset),
.IN2(_21_),
.IN3(_22_),
.QN(_00_[11])
);
NOR2X0 _68_ (
.IN1(reset),
.IN2(\halton.counter [0]),
.QN(_00_[0])
);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
DFFX1 _69_ (
.CLK(clk),
.D(_00_[0]),
.Q(\halton.counter [0]),
.QN(_23_)
);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
DFFX1 _70_ (
.CLK(clk),
.D(_00_[1]),
.Q(\halton.counter [1]),
.QN(_24_)
);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
DFFX1 _71_ (
.CLK(clk),
.D(_00_[2]),
.Q(\halton.counter [2]),
.QN(_25_)
);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
DFFX1 _72_ (
.CLK(clk),
.D(_00_[3]),
.Q(\halton.counter [3]),
.QN(_26_)
);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
DFFX1 _73_ (
.CLK(clk),
.D(_00_[4]),
.Q(\halton.counter [4]),
.QN(_27_)
);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
DFFX1 _74_ (
.CLK(clk),
.D(_00_[5]),
.Q(\halton.counter [5]),
.QN(_28_)
);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
DFFX1 _75_ (
.CLK(clk),
.D(_00_[6]),
.Q(\halton.counter [6]),
.QN(_29_)
);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
DFFX1 _76_ (
.CLK(clk),
.D(_00_[7]),
.Q(\halton.counter [7]),
.QN(_30_)
);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
DFFX1 _77_ (
.CLK(clk),
.D(_00_[8]),
.Q(\halton.counter [8]),
.QN(_31_)
);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
DFFX1 _78_ (
.CLK(clk),
.D(_00_[9]),
.Q(\halton.counter [9]),
.QN(_32_)
);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
DFFX1 _79_ (
.CLK(clk),
.D(_00_[10]),
.Q(\halton.counter [10]),
.QN(_33_)
);
(* src = "rng_haltonBase2.v:7|rng_haltonBase2.v:19" *)
DFFX1 _80_ (
.CLK(clk),
.D(_00_[11]),
.Q(\halton.counter [11]),
.QN(_34_)
);
assign \halton.clk = clk;
assign \halton.o_output = { \halton.counter [0], \halton.counter [1], \halton.counter [2], \halton.counter [3], \halton.counter [4], \halton.counter [5], \halton.counter [6], \halton.counter [7], \halton.counter [8], \halton.counter [9], \halton.counter [10], \halton.counter [11] };
assign \halton.reset = reset;
assign o_output = { \halton.counter [0], \halton.counter [1], \halton.counter [2], \halton.counter [3], \halton.counter [4], \halton.counter [5], \halton.counter [6], \halton.counter [7], \halton.counter [8], \halton.counter [9], \halton.counter [10], \halton.counter [11] };
endmodule

2
tests/test_bench.py

@ -12,4 +12,4 @@ def test_b01(mydir):
def test_simple(): def test_simple():
c = bench.parse('input(a, b) output(z) z=and(a,b)') c = bench.parse('input(a, b) output(z) z=and(a,b)')
assert len(c.nodes) == 4 assert len(c.nodes) == 4
assert len(c.interface) == 3 assert len(c.io_nodes) == 3

33
tests/test_circuit.py

@ -1,7 +1,8 @@
import pickle import pickle
from kyupy.circuit import Circuit, Node, Line from kyupy.circuit import Circuit, Node, Line
from kyupy import verilog from kyupy import verilog, bench
from kyupy.techlib import SAED32
def test_lines(): def test_lines():
c = Circuit() c = Circuit()
@ -43,7 +44,7 @@ def test_lines():
assert c.lines[0].index == 0 assert c.lines[0].index == 0
assert c.lines[1].index == 1 assert c.lines[1].index == 1
assert n1.outs[2] is None assert len(n1.outs) == 2
assert n2.ins[1] is None assert n2.ins[1] is None
assert n2.ins[2] == line2 assert n2.ins[2] == line2
@ -57,9 +58,9 @@ def test_circuit():
assert 'in1' in c.cells assert 'in1' in c.cells
assert 'and1' not in c.cells assert 'and1' not in c.cells
c.interface[0] = in1 c.io_nodes[0] = in1
c.interface[1] = in2 c.io_nodes[1] = in2
c.interface[2] = out1 c.io_nodes[2] = out1
and1 = Node(c, 'and1', kind='and') and1 = Node(c, 'and1', kind='and')
Line(c, in1, and1) Line(c, in1, and1)
@ -104,9 +105,29 @@ def test_circuit():
def test_pickle(mydir): def test_pickle(mydir):
c = verilog.load(mydir / 'b14.v.gz') c = verilog.load(mydir / 'b15_4ig.v.gz', tlib=SAED32)
assert c is not None assert c is not None
cs = pickle.dumps(c) cs = pickle.dumps(c)
assert cs is not None assert cs is not None
c2 = pickle.loads(cs) c2 = pickle.loads(cs)
assert c == c2 assert c == c2
def test_substitute():
c = bench.parse('input(i1, i2, i3, i4, i5) output(o1) aoi=AOI221(i1, i2, i3, i4, i5) o1=not(aoi)')
assert len(c.cells) == 2
assert len(c.io_nodes) == 6
aoi221_impl = bench.parse('input(in1, in2, in3, in4, in5) output(q) a1=and(in1, in2) a2=and(in3, in4) q=or(a1, a2, in5)')
assert len(aoi221_impl.cells) == 3
assert len(aoi221_impl.io_nodes) == 6
c.substitute(c.cells['aoi'], aoi221_impl)
assert len(c.cells) == 4
assert len(c.io_nodes) == 6
def test_resolve(mydir):
c = verilog.load(mydir / 'b15_4ig.v.gz', tlib=SAED32)
s_names = [n.name for n in c.s_nodes]
c.resolve_tlib_cells(SAED32)
s_names_prim = [n.name for n in c.s_nodes]
assert s_names == s_names_prim, 'resolve_tlib_cells does not preserve names or order of s_nodes'

313
tests/test_logic.py

@ -1,252 +1,75 @@
import numpy as np
import kyupy.logic as lg import kyupy.logic as lg
from kyupy.logic import mvarray, bparray, bp_to_mv, mv_to_bp
def test_mvarray(): def assert_equal_shape_and_contents(actual, desired):
desired = np.array(desired, dtype=np.uint8)
# instantiation with shape assert actual.shape == desired.shape
np.testing.assert_allclose(actual, desired)
ary = lg.MVArray(4)
assert ary.length == 1
assert len(ary) == 1 def test_mvarray_single_vector():
assert ary.width == 4 assert_equal_shape_and_contents(mvarray(1, 0, 1), [lg.ONE, lg.ZERO, lg.ONE])
assert_equal_shape_and_contents(mvarray([1, 0, 1]), [lg.ONE, lg.ZERO, lg.ONE])
ary = lg.MVArray((3, 2)) assert_equal_shape_and_contents(mvarray('10X-RFPN'), [lg.ONE, lg.ZERO, lg.UNKNOWN, lg.UNASSIGNED, lg.RISE, lg.FALL, lg.PPULSE, lg.NPULSE])
assert ary.length == 2 assert_equal_shape_and_contents(mvarray(['1']), [lg.ONE])
assert len(ary) == 2 assert_equal_shape_and_contents(mvarray('1'), [lg.ONE])
assert ary.width == 3
# instantiation with single vector def test_mvarray_multi_vector():
assert_equal_shape_and_contents(mvarray([0, 0], [0, 1], [1, 0], [1, 1]), [[lg.ZERO, lg.ZERO, lg.ONE, lg.ONE], [lg.ZERO, lg.ONE, lg.ZERO, lg.ONE]])
ary = lg.MVArray([1, 0, 1]) assert_equal_shape_and_contents(mvarray('10X', '--1'), [[lg.ONE, lg.UNASSIGNED], [lg.ZERO, lg.UNASSIGNED], [lg.UNKNOWN, lg.ONE]])
assert ary.length == 1
assert ary.width == 3
assert str(ary) == "['101']" def test_mv_ops():
assert ary[0] == '101' x1_8v = mvarray('00000000XXXXXXXX--------11111111PPPPPPPPRRRRRRRRFFFFFFFFNNNNNNNN')
x2_8v = mvarray('0X-1PRFN'*8)
ary = lg.MVArray("10X-")
assert ary.length == 1 assert_equal_shape_and_contents(lg.mv_not(x1_8v), mvarray('11111111XXXXXXXXXXXXXXXX00000000NNNNNNNNFFFFFFFFRRRRRRRRPPPPPPPP'))
assert ary.width == 4 assert_equal_shape_and_contents(lg.mv_or(x1_8v, x2_8v), mvarray('0XX1PRFNXXX1XXXXXXX1XXXX11111111PXX1PRFNRXX1RRNNFXX1FNFNNXX1NNNN'))
assert str(ary) == "['10X-']" assert_equal_shape_and_contents(lg.mv_and(x1_8v, x2_8v), mvarray('000000000XXXXXXX0XXXXXXX0XX1PRFN0XXPPPPP0XXRPRPR0XXFPPFF0XXNPRFN'))
assert ary[0] == '10X-' assert_equal_shape_and_contents(lg.mv_xor(x1_8v, x2_8v), mvarray('0XX1PRFNXXXXXXXXXXXXXXXX1XX0NFRPPXXNPRFNRXXFRPNFFXXRFNPRNXXPNFRP'))
ary = lg.MVArray("1") # TODO
assert ary.length == 1 #assert_equal_shape_and_contents(lg.mv_transition(x1_8v, x2_8v), mvarray('0XXR PRFNXXXXXXXXXXXXXXXX1XX0NFRPPXXNPRFNRXXFRPNFFXXRFNPRNXXPNFRP'))
assert ary.width == 1
x30_8v = mvarray('0000000000000000000000000000000000000000000000000000000000000000')
ary = lg.MVArray(["1"]) x31_8v = mvarray('1111111111111111111111111111111111111111111111111111111111111111')
assert ary.length == 1
assert ary.width == 1 assert_equal_shape_and_contents(lg.mv_latch(x1_8v, x2_8v, x30_8v), mvarray('0XX000000XXXXXXX0XXXXXXX0XX10R110XX000000XXR0R0R0XXF001F0XX10R11'))
assert_equal_shape_and_contents(lg.mv_latch(x1_8v, x2_8v, x31_8v), mvarray('1XX01F001XXXXXXX1XXXXXXX1XX111111XX01F001XXR110R1XXF1F1F1XX11111'))
# instantiation with multiple vectors
ary = lg.MVArray([[0, 0], [0, 1], [1, 0], [1, 1]])
assert ary.length == 4
assert ary.width == 2
ary = lg.MVArray(["000", "001", "110", "---"])
assert ary.length == 4
assert ary.width == 3
assert str(ary) == "['000', '001', '110', '---']"
assert ary[2] == '110'
# casting to 2-valued logic
ary = lg.MVArray([0, 1, 2, None], m=2)
assert ary.data[0] == lg.ZERO
assert ary.data[1] == lg.ONE
assert ary.data[2] == lg.ZERO
assert ary.data[3] == lg.ZERO
ary = lg.MVArray("0-X1PRFN", m=2)
assert ary.data[0] == lg.ZERO
assert ary.data[1] == lg.ZERO
assert ary.data[2] == lg.ZERO
assert ary.data[3] == lg.ONE
assert ary.data[4] == lg.ZERO
assert ary.data[5] == lg.ONE
assert ary.data[6] == lg.ZERO
assert ary.data[7] == lg.ONE
# casting to 4-valued logic
ary = lg.MVArray([0, 1, 2, None, 'F'], m=4)
assert ary.data[0] == lg.ZERO
assert ary.data[1] == lg.ONE
assert ary.data[2] == lg.UNKNOWN
assert ary.data[3] == lg.UNASSIGNED
assert ary.data[4] == lg.ZERO
ary = lg.MVArray("0-X1PRFN", m=4)
assert ary.data[0] == lg.ZERO
assert ary.data[1] == lg.UNASSIGNED
assert ary.data[2] == lg.UNKNOWN
assert ary.data[3] == lg.ONE
assert ary.data[4] == lg.ZERO
assert ary.data[5] == lg.ONE
assert ary.data[6] == lg.ZERO
assert ary.data[7] == lg.ONE
# casting to 8-valued logic
ary = lg.MVArray([0, 1, 2, None, 'F'], m=8)
assert ary.data[0] == lg.ZERO
assert ary.data[1] == lg.ONE
assert ary.data[2] == lg.UNKNOWN
assert ary.data[3] == lg.UNASSIGNED
assert ary.data[4] == lg.FALL
ary = lg.MVArray("0-X1PRFN", m=8)
assert ary.data[0] == lg.ZERO
assert ary.data[1] == lg.UNASSIGNED
assert ary.data[2] == lg.UNKNOWN
assert ary.data[3] == lg.ONE
assert ary.data[4] == lg.PPULSE
assert ary.data[5] == lg.RISE
assert ary.data[6] == lg.FALL
assert ary.data[7] == lg.NPULSE
# copy constructor and casting
ary8 = lg.MVArray(ary, m=8)
assert ary8.length == 1
assert ary8.width == 8
assert ary8.data[7] == lg.NPULSE
ary4 = lg.MVArray(ary, m=4)
assert ary4.data[1] == lg.UNASSIGNED
assert ary4.data[7] == lg.ONE
ary2 = lg.MVArray(ary, m=2)
assert ary2.data[1] == lg.ZERO
assert ary2.data[7] == lg.ONE
def test_mv_operations():
x1_2v = lg.MVArray("0011", m=2)
x2_2v = lg.MVArray("0101", m=2)
x1_4v = lg.MVArray("0000XXXX----1111", m=4)
x2_4v = lg.MVArray("0X-10X-10X-10X-1", m=4)
x1_8v = lg.MVArray("00000000XXXXXXXX--------11111111PPPPPPPPRRRRRRRRFFFFFFFFNNNNNNNN", m=8)
x2_8v = lg.MVArray("0X-1PRFN0X-1PRFN0X-1PRFN0X-1PRFN0X-1PRFN0X-1PRFN0X-1PRFN0X-1PRFN", m=8)
assert lg.mv_not(x1_2v)[0] == '1100'
assert lg.mv_not(x1_4v)[0] == '1111XXXXXXXX0000'
assert lg.mv_not(x1_8v)[0] == '11111111XXXXXXXXXXXXXXXX00000000NNNNNNNNFFFFFFFFRRRRRRRRPPPPPPPP'
assert lg.mv_or(x1_2v, x2_2v)[0] == '0111'
assert lg.mv_or(x1_4v, x2_4v)[0] == '0XX1XXX1XXX11111'
assert lg.mv_or(x1_8v, x2_8v)[0] == '0XX1PRFNXXX1XXXXXXX1XXXX11111111PXX1PRFNRXX1RRNNFXX1FNFNNXX1NNNN'
assert lg.mv_and(x1_2v, x2_2v)[0] == '0001'
assert lg.mv_and(x1_4v, x2_4v)[0] == '00000XXX0XXX0XX1'
assert lg.mv_and(x1_8v, x2_8v)[0] == '000000000XXXXXXX0XXXXXXX0XX1PRFN0XXPPPPP0XXRPRPR0XXFPPFF0XXNPRFN'
assert lg.mv_xor(x1_2v, x2_2v)[0] == '0110'
assert lg.mv_xor(x1_4v, x2_4v)[0] == '0XX1XXXXXXXX1XX0'
assert lg.mv_xor(x1_8v, x2_8v)[0] == '0XX1PRFNXXXXXXXXXXXXXXXX1XX0NFRPPXXNPRFNRXXFRPNFFXXRFNPRNXXPNFRP'
x30_2v = lg.MVArray("0000", m=2)
x31_2v = lg.MVArray("1111", m=2)
x30_4v = lg.MVArray("0000000000000000", m=4)
x31_4v = lg.MVArray("1111111111111111", m=4)
x30_8v = lg.MVArray("0000000000000000000000000000000000000000000000000000000000000000", m=8)
x31_8v = lg.MVArray("1111111111111111111111111111111111111111111111111111111111111111", m=8)
assert lg.mv_latch(x1_2v, x2_2v, x30_2v)[0] == '0001'
assert lg.mv_latch(x1_2v, x2_2v, x31_2v)[0] == '1011'
assert lg.mv_latch(x1_4v, x2_4v, x30_4v)[0] == '0XX00XXX0XXX0XX1'
assert lg.mv_latch(x1_4v, x2_4v, x31_4v)[0] == '1XX01XXX1XXX1XX1'
assert lg.mv_latch(x1_8v, x2_8v, x30_8v)[0] == '0XX000000XXXXXXX0XXXXXXX0XX10R110XX000000XXR0R0R0XXF001F0XX10R11'
assert lg.mv_latch(x1_8v, x2_8v, x31_8v)[0] == '1XX01F001XXXXXXX1XXXXXXX1XX111111XX01F001XXR110R1XXF1F1F1XX11111'
def test_bparray(): def test_bparray():
ary = lg.BPArray(4) bpa = bparray('0X-1PRFN')
assert ary.length == 1 assert bpa.shape == (8, 3, 1)
assert len(ary) == 1
assert ary.width == 4 bpa = bparray('0X-1PRFN-')
assert bpa.shape == (9, 3, 1)
ary = lg.BPArray((3, 2))
assert ary.length == 2 bpa = bparray('000', '001', '010', '011', '100', '101', '110', '111')
assert len(ary) == 2 assert bpa.shape == (3, 3, 1)
assert ary.width == 3
bpa = bparray('000', '001', '010', '011', '100', '101', '110', '111', 'RFX')
assert lg.MVArray(lg.BPArray("01", m=2))[0] == '01' assert bpa.shape == (3, 3, 2)
assert lg.MVArray(lg.BPArray("0X-1", m=4))[0] == '0X-1'
assert lg.MVArray(lg.BPArray("0X-1PRFN", m=8))[0] == '0X-1PRFN' assert_equal_shape_and_contents(bp_to_mv(bparray('0X-1PRFN'))[:,0], mvarray('0X-1PRFN'))
assert_equal_shape_and_contents(bparray('0X-1PRFN'), mv_to_bp(mvarray('0X-1PRFN')))
x1_2v = lg.BPArray("0011", m=2)
x2_2v = lg.BPArray("0101", m=2) x1_8v = bparray('00000000XXXXXXXX--------11111111PPPPPPPPRRRRRRRRFFFFFFFFNNNNNNNN')
x1_4v = lg.BPArray("0000XXXX----1111", m=4) x2_8v = bparray('0X-1PRFN'*8)
x2_4v = lg.BPArray("0X-10X-10X-10X-1", m=4)
x1_8v = lg.BPArray("00000000XXXXXXXX--------11111111PPPPPPPPRRRRRRRRFFFFFFFFNNNNNNNN", m=8) out_8v = np.empty((64, 3, 1), dtype=np.uint8)
x2_8v = lg.BPArray("0X-1PRFN0X-1PRFN0X-1PRFN0X-1PRFN0X-1PRFN0X-1PRFN0X-1PRFN0X-1PRFN", m=8)
assert_equal_shape_and_contents(bp_to_mv(lg.bp8v_buf(out_8v, x1_8v))[:,0], mvarray('00000000XXXXXXXXXXXXXXXX11111111PPPPPPPPRRRRRRRRFFFFFFFFNNNNNNNN'))
out_2v = lg.BPArray((4, 1), m=2) assert_equal_shape_and_contents(bp_to_mv(lg.bp8v_or(out_8v, x1_8v, x2_8v))[:,0], mvarray('0XX1PRFNXXX1XXXXXXX1XXXX11111111PXX1PRFNRXX1RRNNFXX1FNFNNXX1NNNN'))
out_4v = lg.BPArray((16, 1), m=4) assert_equal_shape_and_contents(bp_to_mv(lg.bp8v_and(out_8v, x1_8v, x2_8v))[:,0], mvarray('000000000XXXXXXX0XXXXXXX0XX1PRFN0XXPPPPP0XXRPRPR0XXFPPFF0XXNPRFN'))
out_8v = lg.BPArray((64, 1), m=8) assert_equal_shape_and_contents(bp_to_mv(lg.bp8v_xor(out_8v, x1_8v, x2_8v))[:,0], mvarray('0XX1PRFNXXXXXXXXXXXXXXXX1XX0NFRPPXXNPRFNRXXFRPNFFXXRFNPRNXXPNFRP'))
lg.bp_buf(out_2v.data, x1_2v.data) x30_8v = bparray('0000000000000000000000000000000000000000000000000000000000000000')
lg.bp_buf(out_4v.data, x1_4v.data) x31_8v = bparray('1111111111111111111111111111111111111111111111111111111111111111')
lg.bp_buf(out_8v.data, x1_8v.data)
assert_equal_shape_and_contents(bp_to_mv(lg.bp8v_latch(out_8v, x1_8v, x2_8v, x30_8v))[:,0], mvarray('0XX000000XXXXXXX0XXXXXXX0XX10R110XX000000XXR0R0R0XXF001F0XX10R11'))
assert lg.MVArray(out_2v)[0] == '0011' assert_equal_shape_and_contents(bp_to_mv(lg.bp8v_latch(out_8v, x1_8v, x2_8v, x31_8v))[:,0], mvarray('1XX01F001XXXXXXX1XXXXXXX1XX111111XX01F001XXR110R1XXF1F1F1XX11111'))
assert lg.MVArray(out_4v)[0] == '0000XXXXXXXX1111'
assert lg.MVArray(out_8v)[0] == '00000000XXXXXXXXXXXXXXXX11111111PPPPPPPPRRRRRRRRFFFFFFFFNNNNNNNN'
lg.bp_not(out_2v.data, x1_2v.data)
lg.bp_not(out_4v.data, x1_4v.data)
lg.bp_not(out_8v.data, x1_8v.data)
assert lg.MVArray(out_2v)[0] == '1100'
assert lg.MVArray(out_4v)[0] == '1111XXXXXXXX0000'
assert lg.MVArray(out_8v)[0] == '11111111XXXXXXXXXXXXXXXX00000000NNNNNNNNFFFFFFFFRRRRRRRRPPPPPPPP'
lg.bp_or(out_2v.data, x1_2v.data, x2_2v.data)
lg.bp_or(out_4v.data, x1_4v.data, x2_4v.data)
lg.bp_or(out_8v.data, x1_8v.data, x2_8v.data)
assert lg.MVArray(out_2v)[0] == '0111'
assert lg.MVArray(out_4v)[0] == '0XX1XXX1XXX11111'
assert lg.MVArray(out_8v)[0] == '0XX1PRFNXXX1XXXXXXX1XXXX11111111PXX1PRFNRXX1RRNNFXX1FNFNNXX1NNNN'
lg.bp_and(out_2v.data, x1_2v.data, x2_2v.data)
lg.bp_and(out_4v.data, x1_4v.data, x2_4v.data)
lg.bp_and(out_8v.data, x1_8v.data, x2_8v.data)
assert lg.MVArray(out_2v)[0] == '0001'
assert lg.MVArray(out_4v)[0] == '00000XXX0XXX0XX1'
assert lg.MVArray(out_8v)[0] == '000000000XXXXXXX0XXXXXXX0XX1PRFN0XXPPPPP0XXRPRPR0XXFPPFF0XXNPRFN'
lg.bp_xor(out_2v.data, x1_2v.data, x2_2v.data)
lg.bp_xor(out_4v.data, x1_4v.data, x2_4v.data)
lg.bp_xor(out_8v.data, x1_8v.data, x2_8v.data)
assert lg.MVArray(out_2v)[0] == '0110'
assert lg.MVArray(out_4v)[0] == '0XX1XXXXXXXX1XX0'
assert lg.MVArray(out_8v)[0] == '0XX1PRFNXXXXXXXXXXXXXXXX1XX0NFRPPXXNPRFNRXXFRPNFFXXRFNPRNXXPNFRP'
x30_2v = lg.BPArray("0000", m=2)
x30_4v = lg.BPArray("0000000000000000", m=4)
x30_8v = lg.BPArray("0000000000000000000000000000000000000000000000000000000000000000", m=8)
lg.bp_latch(out_2v.data, x1_2v.data, x2_2v.data, x30_2v.data)
lg.bp_latch(out_4v.data, x1_4v.data, x2_4v.data, x30_4v.data)
lg.bp_latch(out_8v.data, x1_8v.data, x2_8v.data, x30_8v.data)
assert lg.MVArray(out_2v)[0] == '0001'
assert lg.MVArray(out_4v)[0] == '0XX00XXX0XXX0XX1'
assert lg.MVArray(out_8v)[0] == '0XX000000XXXXXXX0XXXXXXX0XX10R110XX000000XXR0R0R0XXF001F0XX10R11'
x31_2v = lg.BPArray("1111", m=2)
x31_4v = lg.BPArray("1111111111111111", m=4)
x31_8v = lg.BPArray("1111111111111111111111111111111111111111111111111111111111111111", m=8)
lg.bp_latch(out_2v.data, x1_2v.data, x2_2v.data, x31_2v.data)
lg.bp_latch(out_4v.data, x1_4v.data, x2_4v.data, x31_4v.data)
lg.bp_latch(out_8v.data, x1_8v.data, x2_8v.data, x31_8v.data)
assert lg.MVArray(out_2v)[0] == '1011'
assert lg.MVArray(out_4v)[0] == '1XX01XXX1XXX1XX1'
assert lg.MVArray(out_8v)[0] == '1XX01F001XXXXXXX1XXXXXXX1XX111111XX01F001XXR110R1XXF1F1F1XX11111'

238
tests/test_logic_sim.py

@ -1,135 +1,175 @@
import numpy as np
from kyupy.logic_sim import LogicSim from kyupy.logic_sim import LogicSim
from kyupy import bench from kyupy import bench, logic, sim
from kyupy.logic import MVArray, BPArray from kyupy.logic import mvarray, bparray, bp_to_mv, mv_to_bp
def assert_equal_shape_and_contents(actual, desired):
desired = np.array(desired, dtype=np.uint8)
assert actual.shape == desired.shape
np.testing.assert_allclose(actual, desired)
def test_2v(): def test_2v():
c = bench.parse('input(x, y) output(a, o, n) a=and(x,y) o=or(x,y) n=not(x)') c = bench.parse(f'''
s = LogicSim(c, 4, m=2) input(i3, i2, i1, i0)
assert len(s.interface) == 5 output({",".join([f"o{i:02d}" for i in range(33)])})
mva = MVArray(['00000', '01000', '10000', '11000'], m=2) o00=BUF1(i0)
bpa = BPArray(mva) o01=INV1(i0)
s.assign(bpa) o02=AND2(i0,i1)
s.propagate() o03=AND3(i0,i1,i2)
s.capture(bpa) o04=AND4(i0,i1,i2,i3)
mva = MVArray(bpa) o05=NAND2(i0,i1)
assert mva[0] == '00001' o06=NAND3(i0,i1,i2)
assert mva[1] == '01011' o07=NAND4(i0,i1,i2,i3)
assert mva[2] == '10010' o08=OR2(i0,i1)
assert mva[3] == '11110' o09=OR3(i0,i1,i2)
o10=OR4(i0,i1,i2,i3)
o11=NOR2(i0,i1)
o12=NOR3(i0,i1,i2)
o13=NOR4(i0,i1,i2,i3)
o14=XOR2(i0,i1)
o15=XOR3(i0,i1,i2)
o16=XOR4(i0,i1,i2,i3)
o17=XNOR2(i0,i1)
o18=XNOR3(i0,i1,i2)
o19=XNOR4(i0,i1,i2,i3)
o20=AO21(i0,i1,i2)
o21=OA21(i0,i1,i2)
o22=AO22(i0,i1,i2,i3)
o23=OA22(i0,i1,i2,i3)
o24=AOI21(i0,i1,i2)
o25=OAI21(i0,i1,i2)
o26=AOI22(i0,i1,i2,i3)
o27=OAI22(i0,i1,i2,i3)
o28=AO211(i0,i1,i2,i3)
o29=OA211(i0,i1,i2,i3)
o30=AOI211(i0,i1,i2,i3)
o31=OAI211(i0,i1,i2,i3)
o32=MUX21(i0,i1,i2)
''')
s = LogicSim(c, 16, m=2)
bpa = logic.bparray([f'{i:04b}'+('-'*(s.s_len-4)) for i in range(16)])
s.s[0] = bpa
s.s_to_c()
s.c_prop()
s.c_to_s()
mva = logic.bp_to_mv(s.s[1])
for res, exp in zip(logic.packbits(mva[4:], dtype=np.uint32), [
sim.BUF1, sim.INV1,
sim.AND2, sim.AND3, sim.AND4,
sim.NAND2, sim.NAND3, sim.NAND4,
sim.OR2, sim.OR3, sim.OR4,
sim.NOR2, sim.NOR3, sim.NOR4,
sim.XOR2, sim.XOR3, sim.XOR4,
sim.XNOR2, sim.XNOR3, sim.XNOR4,
sim.AO21, sim.OA21,
sim.AO22, sim.OA22,
sim.AOI21, sim.OAI21,
sim.AOI22, sim.OAI22,
sim.AO211, sim.OA211,
sim.AOI211, sim.OAI211,
sim.MUX21
]):
assert res == exp, f'Mismatch for SimPrim {sim.names[exp]} res={bin(res)} exp={bin(exp)}'
def test_4v(): def test_4v():
c = bench.parse('input(x, y) output(a, o, n) a=and(x,y) o=or(x,y) n=not(x)') c = bench.parse('input(x, y) output(a, o, n) a=and(x,y) o=or(x,y) n=not(x)')
s = LogicSim(c, 16, m=4) s = LogicSim(c, 16, m=8) # FIXME: m=4
assert len(s.interface) == 5 assert s.s_len == 5
mva = MVArray(['00000', '01000', '0-000', '0X000', bpa = bparray(
'10000', '11000', '1-000', '1X000', '00---', '01---', '0----', '0X---',
'-0000', '-1000', '--000', '-X000', '10---', '11---', '1----', '1X---',
'X0000', 'X1000', 'X-000', 'XX000'], m=4) '-0---', '-1---', '-----', '-X---',
bpa = BPArray(mva) 'X0---', 'X1---', 'X----', 'XX---')
s.assign(bpa) s.s[0] = bpa
s.propagate() s.s_to_c()
s.capture(bpa) s.c_prop()
mva = MVArray(bpa) s.c_to_s()
assert mva[0] == '00001' mva = bp_to_mv(s.s[1])
assert mva[1] == '01011' assert_equal_shape_and_contents(mva, mvarray(
assert mva[2] == '0-0X1' '--001', '--011', '--0X1', '--0X1',
assert mva[3] == '0X0X1' '--010', '--110', '--X10', '--X10',
assert mva[4] == '10010' '--0XX', '--X1X', '--XXX', '--XXX',
assert mva[5] == '11110' '--0XX', '--X1X', '--XXX', '--XXX'))
assert mva[6] == '1-X10'
assert mva[7] == '1XX10'
assert mva[8] == '-00XX'
assert mva[9] == '-1X1X'
assert mva[10] == '--XXX'
assert mva[11] == '-XXXX'
assert mva[12] == 'X00XX'
assert mva[13] == 'X1X1X'
assert mva[14] == 'X-XXX'
assert mva[15] == 'XXXXX'
def test_8v(): def test_8v():
c = bench.parse('input(x, y) output(a, o, n, xo) a=and(x,y) o=or(x,y) n=not(x) xo=xor(x,y)') c = bench.parse('input(x, y) output(a, o, n, xo) a=and(x,y) o=or(x,y) n=not(x) xo=xor(x,y)')
s = LogicSim(c, 64, m=8) s = LogicSim(c, 64, m=8)
assert len(s.interface) == 6 assert s.s_len == 6
mva = MVArray(['000010', '010111', '0-0X1X', '0X0X1X', '0R0R1R', '0F0F1F', '0P0P1P', '0N0N1N', mva = mvarray(
'100101', '111100', '1-X10X', '1XX10X', '1RR10F', '1FF10R', '1PP10N', '1NN10P', '000010', '010111', '0-0X1X', '0X0X1X', '0R0R1R', '0F0F1F', '0P0P1P', '0N0N1N',
'-00XXX', '-1X1XX', '--XXXX', '-XXXXX', '-RXXXX', '-FXXXX', '-PXXXX', '-NXXXX', '100101', '111100', '1-X10X', '1XX10X', '1RR10F', '1FF10R', '1PP10N', '1NN10P',
'X00XXX', 'X1X1XX', 'X-XXXX', 'XXXXXX', 'XRXXXX', 'XFXXXX', 'XPXXXX', 'XNXXXX', '-00XXX', '-1X1XX', '--XXXX', '-XXXXX', '-RXXXX', '-FXXXX', '-PXXXX', '-NXXXX',
'R00RFR', 'R1R1FF', 'R-XXFX', 'RXXXFX', 'RRRRFP', 'RFPNFN', 'RPPRFR', 'RNRNFF', 'X00XXX', 'X1X1XX', 'X-XXXX', 'XXXXXX', 'XRXXXX', 'XFXXXX', 'XPXXXX', 'XNXXXX',
'F00FRF', 'F1F1RR', 'F-XXRX', 'FXXXRX', 'FRPNRN', 'FFFFRP', 'FPPFRF', 'FNFNRR', 'R00RFR', 'R1R1FF', 'R-XXFX', 'RXXXFX', 'RRRRFP', 'RFPNFN', 'RPPRFR', 'RNRNFF',
'P00PNP', 'P1P1NN', 'P-XXNX', 'PXXXNX', 'PRPRNR', 'PFPFNF', 'PPPPNP', 'PNPNNN', 'F00FRF', 'F1F1RR', 'F-XXRX', 'FXXXRX', 'FRPNRN', 'FFFFRP', 'FPPFRF', 'FNFNRR',
'N00NPN', 'N1N1PP', 'N-XXPX', 'NXXXPX', 'NRRNPF', 'NFFNPR', 'NPPNPN', 'NNNNPP'], m=8) 'P00PNP', 'P1P1NN', 'P-XXNX', 'PXXXNX', 'PRPRNR', 'PFPFNF', 'PPPPNP', 'PNPNNN',
bpa = BPArray(mva) 'N00NPN', 'N1N1PP', 'N-XXPX', 'NXXXPX', 'NRRNPF', 'NFFNPR', 'NPPNPN', 'NNNNPP')
s.assign(bpa) tests = np.copy(mva)
s.propagate() tests[2:] = logic.UNASSIGNED
resp_bp = BPArray(bpa) bpa = mv_to_bp(tests)
s.capture(resp_bp) s.s[0] = bpa
resp = MVArray(resp_bp) s.s_to_c()
s.c_prop()
for i in range(64): s.c_to_s()
assert resp[i] == mva[i] resp = bp_to_mv(s.s[1])
exp_resp = np.copy(mva)
exp_resp[:2] = logic.UNASSIGNED
np.testing.assert_allclose(resp, exp_resp)
def test_loop(): def test_loop():
c = bench.parse('q=dff(d) d=not(q)') c = bench.parse('q=dff(d) d=not(q)')
s = LogicSim(c, 4, m=8) s = LogicSim(c, 4, m=8)
assert len(s.interface) == 1 assert s.s_len == 1
mva = MVArray([['0'], ['1'], ['R'], ['F']], m=8) mva = mvarray([['0'], ['1'], ['R'], ['F']])
s.assign(BPArray(mva)) # TODO
s.propagate() # s.assign(BPArray(mva))
resp_bp = BPArray((len(s.interface), s.sims)) # s.propagate()
s.capture(resp_bp) # resp_bp = BPArray((len(s.interface), s.sims))
resp = MVArray(resp_bp) # s.capture(resp_bp)
# resp = MVArray(resp_bp)
assert resp[0] == '1' # assert resp[0] == '1'
assert resp[1] == '0' # assert resp[1] == '0'
assert resp[2] == 'F' # assert resp[2] == 'F'
assert resp[3] == 'R' # assert resp[3] == 'R'
resp_bp = s.cycle(resp_bp) # resp_bp = s.cycle(resp_bp)
resp = MVArray(resp_bp) # resp = MVArray(resp_bp)
assert resp[0] == '0' # assert resp[0] == '0'
assert resp[1] == '1' # assert resp[1] == '1'
assert resp[2] == 'R' # assert resp[2] == 'R'
assert resp[3] == 'F' # assert resp[3] == 'F'
def test_latch(): def test_latch():
c = bench.parse('input(d, t) output(q) q=latch(d, t)') c = bench.parse('input(d, t) output(q) q=latch(d, t)')
s = LogicSim(c, 8, m=8) s = LogicSim(c, 8, m=8)
assert len(s.interface) == 4 assert s.s_len == 4
mva = MVArray(['00-0', '00-1', '01-0', '01-1', '10-0', '10-1', '11-0', '11-1'], m=8) mva = mvarray('00-0', '00-1', '01-0', '01-1', '10-0', '10-1', '11-0', '11-1')
exp = MVArray(['0000', '0011', '0100', '0100', '1000', '1011', '1111', '1111'], m=8) exp = mvarray('0000', '0011', '0100', '0100', '1000', '1011', '1111', '1111')
resp = MVArray(s.cycle(BPArray(mva))) # TODO
# resp = MVArray(s.cycle(BPArray(mva)))
for i in range(len(mva)): # for i in range(len(mva)):
assert resp[i] == exp[i] # assert resp[i] == exp[i]
def test_b01(mydir): def test_b01(mydir):
c = bench.load(mydir / 'b01.bench') c = bench.load(mydir / 'b01.bench')
# 2-valued
s = LogicSim(c, 8, m=2)
assert len(s.interface) == 9
mva = MVArray((len(s.interface), 8), m=2)
# mva.randomize()
bpa = BPArray(mva)
s.assign(bpa)
s.propagate()
s.capture(bpa)
# 8-valued # 8-valued
s = LogicSim(c, 8, m=8) s = LogicSim(c, 8, m=8)
mva = MVArray((len(s.interface), 8), m=8) mva = np.zeros((s.s_len, 8), dtype=np.uint8)
# mva.randomize() s.s[0] = mv_to_bp(mva)
bpa = BPArray(mva) s.s_to_c()
s.assign(bpa) s.c_prop()
s.propagate() s.c_to_s()
s.capture(bpa) bp_to_mv(s.s[1])

230
tests/test_sdf.py

@ -1,5 +1,8 @@
from kyupy import sdf, verilog import numpy as np
from kyupy import sdf, verilog, bench
from kyupy.wave_sim import WaveSim, TMAX, TMIN
from kyupy.techlib import SAED32, SAED90
def test_parse(): def test_parse():
test = ''' test = '''
@ -16,71 +19,70 @@ def test_parse():
(TEMPERATURE 25.00:25.00:25.00) (TEMPERATURE 25.00:25.00:25.00)
(TIMESCALE 1ns) (TIMESCALE 1ns)
(CELL (CELL
(CELLTYPE "b14") (CELLTYPE "b14")
(INSTANCE) (INSTANCE)
(DELAY (DELAY
(ABSOLUTE (ABSOLUTE
(INTERCONNECT U621/ZN U19246/IN1 (0.000:0.000:0.000)) (INTERCONNECT U621/ZN U19246/IN1 (0.000:0.000:0.000))
(INTERCONNECT U13292/QN U19246/IN2 (0.001:0.001:0.001)) (INTERCONNECT U13292/QN U19246/IN2 (0.001:0.001:0.001))
(INTERCONNECT U15050/QN U19247/IN1 (0.000:0.000:0.000)) (INTERCONNECT U15050/QN U19247/IN1 (0.000:0.000:0.000))
(INTERCONNECT U13293/QN U19247/IN2 (0.000:0.000:0.000) (0.000:0.000:0.000)) (INTERCONNECT U13293/QN U19247/IN2 (0.000:0.000:0.000) (0.000:0.000:0.000))
)
) )
)
) )
(CELL (CELL
(CELLTYPE "INVX2") (CELLTYPE "INVX2")
(INSTANCE U78) (INSTANCE U78)
(DELAY (DELAY
(ABSOLUTE (ABSOLUTE
(IOPATH INP ZN (0.201:0.227:0.227) (0.250:0.271:0.271)) (IOPATH INP ZN (0.201:0.227:0.227) (0.250:0.271:0.271))
)
) )
)
) )
(CELL (CELL
(CELLTYPE "SDFFARX1") (CELLTYPE "SDFFARX1")
(INSTANCE reg3_reg_1_0) (INSTANCE reg3_reg_1_0)
(DELAY (DELAY
(ABSOLUTE (ABSOLUTE
(IOPATH (posedge CLK) Q (0.707:0.710:0.710) (0.737:0.740:0.740)) (IOPATH (posedge CLK) Q (0.707:0.710:0.710) (0.737:0.740:0.740))
(IOPATH (negedge RSTB) Q () (0.909:0.948:0.948)) (IOPATH (negedge RSTB) Q () (0.909:0.948:0.948))
(IOPATH (posedge CLK) QN (0.585:0.589:0.589) (0.545:0.550:0.550)) (IOPATH (posedge CLK) QN (0.585:0.589:0.589) (0.545:0.550:0.550))
(IOPATH (negedge RSTB) QN (1.546:1.593:1.593) ()) (IOPATH (negedge RSTB) QN (1.546:1.593:1.593) ())
)
) )
) (TIMINGCHECK
(TIMINGCHECK (WIDTH (posedge CLK) (0.284:0.284:0.284))
(WIDTH (posedge CLK) (0.284:0.284:0.284)) (WIDTH (negedge CLK) (0.642:0.642:0.642))
(WIDTH (negedge CLK) (0.642:0.642:0.642)) (SETUP (posedge D) (posedge CLK) (0.544:0.553:0.553))
(SETUP (posedge D) (posedge CLK) (0.544:0.553:0.553)) (SETUP (negedge D) (posedge CLK) (0.620:0.643:0.643))
(SETUP (negedge D) (posedge CLK) (0.620:0.643:0.643)) (HOLD (posedge D) (posedge CLK) (-0.321:-0.331:-0.331))
(HOLD (posedge D) (posedge CLK) (-0.321:-0.331:-0.331)) (HOLD (negedge D) (posedge CLK) (-0.196:-0.219:-0.219))
(HOLD (negedge D) (posedge CLK) (-0.196:-0.219:-0.219)) (RECOVERY (posedge RSTB) (posedge CLK) (-1.390:-1.455:-1.455))
(RECOVERY (posedge RSTB) (posedge CLK) (-1.390:-1.455:-1.455)) (HOLD (posedge RSTB) (posedge CLK) (1.448:1.509:1.509))
(HOLD (posedge RSTB) (posedge CLK) (1.448:1.509:1.509)) (SETUP (posedge SE) (posedge CLK) (0.662:0.670:0.670))
(SETUP (posedge SE) (posedge CLK) (0.662:0.670:0.670)) (SETUP (negedge SE) (posedge CLK) (0.698:0.702:0.702))
(SETUP (negedge SE) (posedge CLK) (0.698:0.702:0.702)) (HOLD (posedge SE) (posedge CLK) (-0.435:-0.444:-0.444))
(HOLD (posedge SE) (posedge CLK) (-0.435:-0.444:-0.444)) (HOLD (negedge SE) (posedge CLK) (-0.291:-0.295:-0.295))
(HOLD (negedge SE) (posedge CLK) (-0.291:-0.295:-0.295)) (SETUP (posedge SI) (posedge CLK) (0.544:0.544:0.544))
(SETUP (posedge SI) (posedge CLK) (0.544:0.544:0.544)) (SETUP (negedge SI) (posedge CLK) (0.634:0.688:0.688))
(SETUP (negedge SI) (posedge CLK) (0.634:0.688:0.688)) (HOLD (posedge SI) (posedge CLK) (-0.317:-0.318:-0.318))
(HOLD (posedge SI) (posedge CLK) (-0.317:-0.318:-0.318)) (HOLD (negedge SI) (posedge CLK) (-0.198:-0.247:-0.247))
(HOLD (negedge SI) (posedge CLK) (-0.198:-0.247:-0.247)) (WIDTH (negedge RSTB) (0.345:0.345:0.345))
(WIDTH (negedge RSTB) (0.345:0.345:0.345))
))) )))
''' '''
df = sdf.parse(test) df = sdf.parse(test)
assert df.name == 'test' assert df.name == 'test'
# print(f'DelayFile(name={df.name}, interconnects={len(df.interconnects)}, iopaths={len(df.iopaths)})')
def test_b14(mydir): def test_b15(mydir):
df = sdf.load(mydir / 'b14.sdf.gz') df = sdf.load(mydir / 'b15_2ig.sdf.gz')
assert df.name == 'b14' assert df.name == 'b15'
def test_gates(mydir): def test_gates(mydir):
c = verilog.load(mydir / 'gates.v') c = verilog.load(mydir / 'gates.v', tlib=SAED90)
df = sdf.load(mydir / 'gates.sdf') df = sdf.load(mydir / 'gates.sdf')
lt = df.annotation(c, dataset=1) lt = df.iopaths(c, tlib=SAED90)[1]
nand_a = c.cells['nandgate'].ins[0] nand_a = c.cells['nandgate'].ins[0]
nand_b = c.cells['nandgate'].ins[1] nand_b = c.cells['nandgate'].ins[1]
and_a = c.cells['andgate'].ins[0] and_a = c.cells['andgate'].ins[0]
@ -97,3 +99,133 @@ def test_gates(mydir):
assert lt[and_b, 0, 0] == 0.375 assert lt[and_b, 0, 0] == 0.375
assert lt[and_b, 0, 1] == 0.370 assert lt[and_b, 0, 1] == 0.370
def test_nand_xor():
c = bench.parse("""
input(A1,A2)
output(lt_1237_U91,lt_1237_U92)
lt_1237_U91 = NAND2X0_RVT(A1,A2)
lt_1237_U92 = XOR2X1_RVT(A1,A2)
""")
df = sdf.parse("""
(DELAYFILE
(CELL
(CELLTYPE "NAND2X0_RVT")
(INSTANCE lt_1237_U91)
(DELAY
(ABSOLUTE
(IOPATH A1 Y (0.018:0.022:0.021) (0.017:0.019:0.019))
(IOPATH A2 Y (0.021:0.024:0.024) (0.018:0.021:0.021))
)
)
)
(CELL
(CELLTYPE "XOR2X1_RVT")
(INSTANCE lt_1237_U92)
(DELAY
(ABSOLUTE
(IOPATH (posedge A1) Y (0.035:0.038:0.038) (0.037:0.062:0.062))
(IOPATH (negedge A1) Y (0.035:0.061:0.061) (0.036:0.040:0.040))
(IOPATH (posedge A2) Y (0.042:0.043:0.043) (0.051:0.064:0.064))
(IOPATH (negedge A2) Y (0.041:0.066:0.066) (0.051:0.053:0.053))
)
)
)
)
""")
d = df.iopaths(c, tlib=SAED32)[1]
c.resolve_tlib_cells(SAED32)
sim = WaveSim(c, delays=d, sims=16)
# input A1
sim.s[0,0] = [0,1,0,1] * 4 # initial values 0101010101010101
sim.s[1,0] = 0.0 # transition time
sim.s[2,0] = [0,0,1,1] * 4 # final values 0011001100110011
# input A2
sim.s[0,1] = ([0]*4 + [1]*4)*2 # initial values 0000111100001111
sim.s[1,1] = 0.0 # transition time
sim.s[2,1] = [0]*8 + [1]*8 # final values 0000000011111111
# A1: 0FR10FR10FR10FR1
# A2: 0000FFFFRRRR1111
# nand: 11111RNR1NFF1RF0
# xor: 0FR1FPPRRNPF1RF0
sim.s_to_c()
sim.c_prop()
sim.c_to_s()
eat = sim.s[4,2:]
lst = sim.s[5,2:]
# NAND-gate output
assert np.allclose(eat[0], [
TMAX, TMAX, TMAX, TMAX, TMAX,
0.022, # FF -> rising Y: min(0.022, 0.024)
TMAX, # RF: pulse filtered
0.024, # falling A2 -> rising Y
TMAX,
TMAX, # FR: pulse filtered
0.021, # RR -> falling Y: max(0.019, 0.021)
0.021, # rising A2 -> falling Y
TMAX,
0.022, # falling A1 -> rising Y
0.019, # rising A1 -> falling Y
TMAX
])
assert np.allclose(lst[0], [
TMIN, TMIN, TMIN, TMIN, TMIN,
0.022, # FF -> rising Y: min(0.022, 0.024)
TMIN, # RF: pulse filtered
0.024, # falling A2 -> rising Y
TMIN,
TMIN, # FR: pulse filtered
0.021, # RR -> falling Y: max(0.019, 0.021)
0.021, # rising A2 -> falling Y
TMIN,
0.022, # falling A1 -> rising Y
0.019, # rising A1 -> falling Y
TMIN
])
#XOR-gate output
assert np.allclose(eat[1], [
TMAX,
0.040, # A1:F -> Y:F
0.038, # A1:R -> Y:R
TMAX,
0.053, # A2:F -> Y:F
TMAX, # P filtered
TMAX, # P filtered
0.066, # A2:F -> Y:R
0.043, # A2:R -> Y:R
TMAX, # N filtered
TMAX, # P filtered
0.064, # A2:R -> Y:F
TMAX,
0.061, # A1:F -> Y:R
0.062, # A1:R -> Y:F
TMAX,
])
assert np.allclose(lst[1], [
TMIN,
0.040, # A1:F -> Y:F
0.038, # A1:R -> Y:R
TMIN,
0.053, # A2:F -> Y:F
TMIN, # P filtered
TMIN, # P filtered
0.066, # A2:F -> Y:R
0.043, # A2:R -> Y:R
TMIN, # N filtered
TMIN, # P filtered
0.064, # A2:R -> Y:F
TMIN,
0.061, # A1:F -> Y:R
0.062, # A1:R -> Y:F
TMIN,
])

20
tests/test_stil.py

@ -1,21 +1,21 @@
from kyupy import stil, verilog from kyupy import stil, verilog
from kyupy.techlib import SAED32
def test_b15(mydir):
b15 = verilog.load(mydir / 'b15_2ig.v.gz', tlib=SAED32)
def test_b14(mydir): s = stil.load(mydir / 'b15_2ig.sa_nf.stil.gz')
b14 = verilog.load(mydir / 'b14.v.gz')
s = stil.load(mydir / 'b14.stuck.stil.gz')
assert len(s.signal_groups) == 10 assert len(s.signal_groups) == 10
assert len(s.scan_chains) == 1 assert len(s.scan_chains) == 1
assert len(s.calls) == 2163 assert len(s.calls) == 1357
tests = s.tests(b14) tests = s.tests(b15)
resp = s.responses(b14) resp = s.responses(b15)
assert len(tests) > 0 assert len(tests) > 0
assert len(resp) > 0 assert len(resp) > 0
s2 = stil.load(mydir / 'b14.transition.stil.gz') s2 = stil.load(mydir / 'b15_2ig.tf_nf.stil.gz')
tests = s2.tests_loc(b14) tests = s2.tests_loc(b15)
resp = s2.responses(b14) resp = s2.responses(b15)
assert len(tests) > 0 assert len(tests) > 0
assert len(resp) > 0 assert len(resp) > 0

45
tests/test_verilog.py

@ -1,8 +1,45 @@
from kyupy import verilog from kyupy import verilog
from kyupy.techlib import SAED90, SAED32
def test_b01(mydir): def test_b01(mydir):
with open(mydir / 'b01.v', 'r') as f: with open(mydir / 'b01.v', 'r') as f:
modules = verilog.parse(f.read()) c = verilog.parse(f.read(), tlib=SAED90)
assert modules is not None assert c is not None
assert verilog.load(mydir / 'b01.v') is not None assert verilog.load(mydir / 'b01.v', tlib=SAED90) is not None
assert len(c.nodes) == 139
assert len(c.lines) == 203
stats = c.stats
assert stats['input'] == 6
assert stats['output'] == 3
assert stats['__seq__'] == 5
def test_b15(mydir):
c = verilog.load(mydir / 'b15_4ig.v.gz', tlib=SAED32)
assert len(c.nodes) == 12067
assert len(c.lines) == 20731
stats = c.stats
assert stats['input'] == 40
assert stats['output'] == 71
assert stats['__seq__'] == 417
def test_gates(mydir):
c = verilog.load(mydir / 'gates.v', tlib=SAED90)
assert len(c.nodes) == 10
assert len(c.lines) == 10
stats = c.stats
assert stats['input'] == 2
assert stats['output'] == 2
assert stats['__seq__'] == 0
def test_halton2(mydir):
c = verilog.load(mydir / 'rng_haltonBase2.synth_yosys.v', tlib=SAED90)
assert len(c.nodes) == 146
assert len(c.lines) == 210
stats = c.stats
assert stats['input'] == 2
assert stats['output'] == 12
assert stats['__seq__'] == 12

300
tests/test_wave_sim.py

@ -1,150 +1,168 @@
import numpy as np import numpy as np
from kyupy.wave_sim import WaveSim, WaveSimCuda, wave_eval, TMIN, TMAX from kyupy.wave_sim import WaveSim, WaveSimCuda, wave_eval_cpu, TMIN, TMAX
from kyupy.logic_sim import LogicSim from kyupy.logic_sim import LogicSim
from kyupy import verilog, sdf, logic from kyupy import logic, bench, sim
from kyupy.logic import MVArray, BPArray from kyupy.logic import mvarray
def test_nand_delays():
op = (sim.NAND4, 4, 0, 1, 2, 3, -1, 0, 0)
#op = (0b0111, 4, 0, 1)
c = np.full((5*16, 1), TMAX) # 5 waveforms of capacity 16
c_locs = np.zeros((5,), dtype='int')
c_caps = np.zeros((5,), dtype='int')
for i in range(5): c_locs[i], c_caps[i] = i*16, 16 # 1:1 mapping
def test_wave_eval():
# SDF specifies IOPATH delays with respect to output polarity # SDF specifies IOPATH delays with respect to output polarity
# SDF pulse rejection value is determined by IOPATH causing last transition and polarity of last transition # SDF pulse rejection value is determined by IOPATH causing last transition and polarity of last transition
line_times = np.zeros((3, 2, 2)) delays = np.zeros((1, 5, 2, 2))
line_times[0, 0, 0] = 0.1 # A -> Z rise delay delays[0, 0, 0, 0] = 0.1 # A -> Z rise delay
line_times[0, 0, 1] = 0.2 # A -> Z fall delay delays[0, 0, 0, 1] = 0.2 # A -> Z fall delay
line_times[0, 1, 0] = 0.1 # A -> Z negative pulse limit (terminate in rising Z) delays[0, 0, 1, 0] = 0.1 # A -> Z negative pulse limit (terminate in rising Z)
line_times[0, 1, 1] = 0.2 # A -> Z positive pulse limit delays[0, 0, 1, 1] = 0.2 # A -> Z positive pulse limit
line_times[1, 0, 0] = 0.3 # as above for B -> Z delays[0, 1, :, 0] = 0.3 # as above for B -> Z
line_times[1, 0, 1] = 0.4 delays[0, 1, :, 1] = 0.4
line_times[1, 1, 0] = 0.3 delays[0, 2, :, 0] = 0.5 # as above for C -> Z
line_times[1, 1, 1] = 0.4 delays[0, 2, :, 1] = 0.6
delays[0, 3, :, 0] = 0.7 # as above for D -> Z
state = np.zeros((3*16, 1)) + TMAX # 3 waveforms of capacity 16 delays[0, 3, :, 1] = 0.8
state[::16, 0] = 16 # first entry is capacity
a = state[0:16, 0] simctl_int = np.asarray([0], dtype=np.int32)
b = state[16:32, 0]
z = state[32:, 0] def wave_assert(inputs, output):
sat = np.zeros((3, 3), dtype='int') for i, a in zip(inputs, c.reshape(-1,16)): a[:len(i)] = i
sat[0] = 0, 16, 0 wave_eval_cpu(op, c, c_locs, c_caps, 0, delays, simctl_int)
sat[1] = 16, 16, 0 for i, v in enumerate(output): np.testing.assert_allclose(c.reshape(-1,16)[4,i], v)
sat[2] = 32, 16, 0
wave_assert([[TMAX,TMAX],[TMAX,TMAX],[TMIN,TMAX],[TMIN,TMAX]], [TMIN,TMAX]) # NAND(0,0,1,1) => 1
sdata = np.asarray([1, -1, 0, 0], dtype='float32') wave_assert([[TMIN,TMAX],[TMAX,TMAX],[TMIN,TMAX],[TMIN,TMAX]], [TMIN,TMAX]) # NAND(1,0,1,1) => 1
wave_assert([[TMIN,TMAX],[TMIN,TMAX],[TMIN,TMAX],[TMIN,TMAX]], [TMAX]) # NAND(1,1,1,1) => 0
wave_eval((0b0111, 2, 0, 1), state, sat, 0, line_times, sdata)
assert z[0] == TMIN # Keep inputs C=1 and D=1.
wave_assert([[1,TMAX],[2,TMAX]], [TMIN,2.4,TMAX]) # _/⎺⎺⎺ NAND __/⎺⎺ => ⎺⎺⎺\___ (B->Z fall delay)
a[0] = TMIN wave_assert([[TMIN,TMAX],[TMIN,2,TMAX]], [2.3,TMAX]) # ⎺⎺⎺⎺⎺ NAND ⎺⎺\__ => ___/⎺⎺⎺ (B->Z rise delay)
wave_eval((0b0111, 2, 0, 1), state, sat, 0, line_times, sdata) wave_assert([[TMIN,TMAX],[TMIN,2,2.35,TMAX]], [2.3,2.75,TMAX]) # ⎺⎺⎺⎺⎺ NAND ⎺\_/⎺ => __/⎺⎺\_ (pos pulse, .35@B -> .45@Z)
assert z[0] == TMIN wave_assert([[TMIN,TMAX],[TMIN,2,2.25,TMAX]], [TMAX]) # ⎺⎺⎺⎺⎺ NAND ⎺\_/⎺ => _______ (pos pulse, .25@B -> .35@Z, filtered)
wave_assert([[TMIN,TMAX],[2,2.45,TMAX]], [TMIN,2.4,2.75,TMAX]) # ⎺⎺⎺⎺⎺ NAND _/⎺\_ => ⎺⎺\_/⎺⎺ (neg pulse, .45@B -> .35@Z)
b[0] = TMIN wave_assert([[TMIN,TMAX],[2,2.35,TMAX]], [TMIN,TMAX]) # ⎺⎺⎺⎺⎺ NAND _/⎺\_ => ⎺⎺⎺⎺⎺⎺⎺ (neg pulse, .35@B -> .25@Z, filtered)
wave_eval((0b0111, 2, 0, 1), state, sat, 0, line_times, sdata)
assert z[0] == TMAX
def test_tiny_circuit():
a[0] = 1 # A _/^^^ c = bench.parse('input(x, y) output(a, o, n) a=and(x,y) o=or(x,y) n=not(x)')
b[0] = 2 # B __/^^ delays = np.full((1, len(c.lines), 2, 2), 1.0) # unit delay for all lines
wave_eval((0b0111, 2, 0, 1), state, sat, 0, line_times, sdata) wsim = WaveSim(c, delays)
assert z[0] == TMIN # ^^^\___ B -> Z fall delay assert wsim.s.shape[1] == 5
assert z[1] == 2.4
assert z[2] == TMAX # values for x
wsim.s[:3,0,0] = 0, 10, 0
a[0] = TMIN # A ^^^^^^ wsim.s[:3,0,1] = 0, 20, 1
b[0] = TMIN # B ^^^\__ wsim.s[:3,0,2] = 1, 30, 0
b[1] = 2 wsim.s[:3,0,3] = 1, 40, 1
wave_eval((0b0111, 2, 0, 1), state, sat, 0, line_times, sdata)
assert z[0] == 2.3 # ___/^^^ B -> Z rise delay # values for y
assert z[1] == TMAX wsim.s[:3,1,0] = 1, 50, 0
wsim.s[:3,1,1] = 1, 60, 0
# pos pulse of 0.35 at B -> 0.45 after delays wsim.s[:3,1,2] = 1, 70, 0
a[0] = TMIN # A ^^^^^^^^ wsim.s[:3,1,3] = 0, 80, 1
b[0] = TMIN
b[1] = 2 # B ^^\__/^^ wsim.s_to_c()
b[2] = 2.35
wave_eval((0b0111, 2, 0, 1), state, sat, 0, line_times, sdata) x_c_loc = wsim.c_locs[wsim.ppi_offset+0] # check x waveforms
assert z[0] == 2.3 # __/^^\__ np.testing.assert_allclose(wsim.c[x_c_loc:x_c_loc+3, 0], [TMAX, TMAX, TMAX])
assert z[1] == 2.75 np.testing.assert_allclose(wsim.c[x_c_loc:x_c_loc+3, 1], [20, TMAX, TMAX])
assert z[2] == TMAX np.testing.assert_allclose(wsim.c[x_c_loc:x_c_loc+3, 2], [TMIN, 30, TMAX])
np.testing.assert_allclose(wsim.c[x_c_loc:x_c_loc+3, 3], [TMIN, TMAX, TMAX])
# neg pulse of 0.45 at B -> 0.35 after delays
a[0] = TMIN # A ^^^^^^^^ y_c_loc = wsim.c_locs[wsim.ppi_offset+1] # check y waveforms
b[0] = 2 # B __/^^\__ np.testing.assert_allclose(wsim.c[y_c_loc:y_c_loc+3, 0], [TMIN, 50, TMAX])
b[1] = 2.45 np.testing.assert_allclose(wsim.c[y_c_loc:y_c_loc+3, 1], [TMIN, 60, TMAX])
b[2] = TMAX np.testing.assert_allclose(wsim.c[y_c_loc:y_c_loc+3, 2], [TMIN, 70, TMAX])
wave_eval((0b0111, 2, 0, 1), state, sat, 0, line_times, sdata) np.testing.assert_allclose(wsim.c[y_c_loc:y_c_loc+3, 3], [80, TMAX, TMAX])
assert z[0] == TMIN # ^^\__/^^
assert z[1] == 2.4 wsim.c_prop()
assert z[2] == 2.75
assert z[3] == TMAX a_c_loc = wsim.c_locs[wsim.ppo_offset+2] # check a waveforms
np.testing.assert_allclose(wsim.c[a_c_loc:a_c_loc+3, 0], [TMAX, TMAX, TMAX])
# neg pulse of 0.35 at B -> 0.25 after delays (filtered) np.testing.assert_allclose(wsim.c[a_c_loc:a_c_loc+3, 1], [21, 61, TMAX])
a[0] = TMIN # A ^^^^^^^^ np.testing.assert_allclose(wsim.c[a_c_loc:a_c_loc+3, 2], [TMIN, 31, TMAX])
b[0] = 2 # B __/^^\__ np.testing.assert_allclose(wsim.c[a_c_loc:a_c_loc+3, 3], [81, TMAX, TMAX])
b[1] = 2.35
b[2] = TMAX o_c_loc = wsim.c_locs[wsim.ppo_offset+3] # check o waveforms
wave_eval((0b0111, 2, 0, 1), state, sat, 0, line_times, sdata) np.testing.assert_allclose(wsim.c[o_c_loc:o_c_loc+3, 0], [TMIN, 51, TMAX])
assert z[0] == TMIN # ^^^^^^ np.testing.assert_allclose(wsim.c[o_c_loc:o_c_loc+3, 1], [TMIN, TMAX, TMAX])
assert z[1] == TMAX np.testing.assert_allclose(wsim.c[o_c_loc:o_c_loc+3, 2], [TMIN, 71, TMAX])
np.testing.assert_allclose(wsim.c[o_c_loc:o_c_loc+3, 3], [TMIN, TMAX, TMAX])
# pos pulse of 0.25 at B -> 0.35 after delays (filtered)
a[0] = TMIN # A ^^^^^^^^ n_c_loc = wsim.c_locs[wsim.ppo_offset+4] # check n waveforms
b[0] = TMIN np.testing.assert_allclose(wsim.c[n_c_loc:n_c_loc+3, 0], [TMIN, TMAX, TMAX])
b[1] = 2 # B ^^\__/^^ np.testing.assert_allclose(wsim.c[n_c_loc:n_c_loc+3, 1], [TMIN, 21, TMAX])
b[2] = 2.25 np.testing.assert_allclose(wsim.c[n_c_loc:n_c_loc+3, 2], [31, TMAX, TMAX])
wave_eval((0b0111, 2, 0, 1), state, sat, 0, line_times, sdata) np.testing.assert_allclose(wsim.c[n_c_loc:n_c_loc+3, 3], [TMAX, TMAX, TMAX])
assert z[0] == TMAX # ______
wsim.c_to_s()
def compare_to_logic_sim(wsim): # check a captures
tests = MVArray((len(wsim.interface), wsim.sims)) np.testing.assert_allclose(wsim.s[3:7, 2, 0], [0, TMAX, TMIN, 0])
np.testing.assert_allclose(wsim.s[3:7, 2, 1], [0, 21, 61, 0])
np.testing.assert_allclose(wsim.s[3:7, 2, 2], [1, 31, 31, 0])
np.testing.assert_allclose(wsim.s[3:7, 2, 3], [0, 81, 81, 1])
# check o captures
np.testing.assert_allclose(wsim.s[3:7, 3, 0], [1, 51, 51, 0])
np.testing.assert_allclose(wsim.s[3:7, 3, 1], [1, TMAX, TMIN, 1])
np.testing.assert_allclose(wsim.s[3:7, 3, 2], [1, 71, 71, 0])
np.testing.assert_allclose(wsim.s[3:7, 3, 3], [1, TMAX, TMIN, 1])
# check o captures
np.testing.assert_allclose(wsim.s[3:7, 4, 0], [1, TMAX, TMIN, 1])
np.testing.assert_allclose(wsim.s[3:7, 4, 1], [1, 21, 21, 0])
np.testing.assert_allclose(wsim.s[3:7, 4, 2], [0, 31, 31, 1])
np.testing.assert_allclose(wsim.s[3:7, 4, 3], [0, TMAX, TMIN, 0])
def compare_to_logic_sim(wsim: WaveSim):
choices = np.asarray([logic.ZERO, logic.ONE, logic.RISE, logic.FALL], dtype=np.uint8) choices = np.asarray([logic.ZERO, logic.ONE, logic.RISE, logic.FALL], dtype=np.uint8)
rng = np.random.default_rng(10) rng = np.random.default_rng(10)
tests.data[...] = rng.choice(choices, tests.data.shape) tests = rng.choice(choices, (wsim.s_len, wsim.sims))
tests_bp = BPArray(tests)
wsim.assign(tests_bp) wsim.s[0] = (tests & 2) >> 1
wsim.propagate() wsim.s[3] = (tests & 2) >> 1
cdata = wsim.capture() wsim.s[1] = 0.0
wsim.s[2] = tests & 1
resp = MVArray(tests) wsim.s[6] = tests & 1
for iidx, inode in enumerate(wsim.interface): wsim.s_to_c()
if len(inode.ins) > 0: wsim.c_prop()
for vidx in range(wsim.sims): wsim.c_to_s()
resp.data[iidx, vidx] = logic.ZERO if cdata[iidx, vidx, 0] < 0.5 else logic.ONE
# resp.set_value(vidx, iidx, 0 if cdata[iidx, vidx, 0] < 0.5 else 1) resp = np.array(wsim.s[6], dtype=np.uint8) | (np.array(wsim.s[3], dtype=np.uint8)<<1)
resp |= ((resp ^ (resp >> 1)) & 1) << 2 # transitions
lsim = LogicSim(wsim.circuit, len(tests_bp)) resp[wsim.pi_s_locs] = logic.UNASSIGNED
lsim.assign(tests_bp)
lsim.propagate() lsim = LogicSim(wsim.circuit, tests.shape[-1])
exp_bp = BPArray(tests_bp) lsim.s[0] = logic.mv_to_bp(tests)
lsim.capture(exp_bp) lsim.s_to_c()
exp = MVArray(exp_bp) lsim.c_prop()
lsim.c_to_s()
for i in range(8): exp = logic.bp_to_mv(lsim.s[1])
exp_str = exp[i].replace('R', '1').replace('F', '0').replace('P', '0').replace('N', '1')
res_str = resp[i].replace('R', '1').replace('F', '0').replace('P', '0').replace('N', '1') resp[resp == logic.PPULSE] = logic.ZERO
assert res_str == exp_str resp[resp == logic.NPULSE] = logic.ONE
exp[exp == logic.PPULSE] = logic.ZERO
def test_b14(mydir): exp[exp == logic.NPULSE] = logic.ONE
c = verilog.load(mydir / 'b14.v.gz', branchforks=True)
df = sdf.load(mydir / 'b14.sdf.gz') np.testing.assert_allclose(resp, exp)
lt = df.annotation(c)
wsim = WaveSim(c, lt, 8)
compare_to_logic_sim(wsim) def test_b15(b15_2ig_circuit, b15_2ig_delays):
compare_to_logic_sim(WaveSim(b15_2ig_circuit, b15_2ig_delays, 8))
def test_b14_strip_forks(mydir):
c = verilog.load(mydir / 'b14.v.gz', branchforks=True) def test_b15_strip_forks(b15_2ig_circuit, b15_2ig_delays):
df = sdf.load(mydir / 'b14.sdf.gz') compare_to_logic_sim(WaveSim(b15_2ig_circuit, b15_2ig_delays, 8, strip_forks=True))
lt = df.annotation(c)
wsim = WaveSim(c, lt, 8, strip_forks=True)
compare_to_logic_sim(wsim) def test_b15_cuda(b15_2ig_circuit, b15_2ig_delays):
compare_to_logic_sim(WaveSimCuda(b15_2ig_circuit, b15_2ig_delays, 8, strip_forks=True))
def test_b14_cuda(mydir):
c = verilog.load(mydir / 'b14.v.gz', branchforks=True)
df = sdf.load(mydir / 'b14.sdf.gz')
lt = df.annotation(c)
wsim = WaveSimCuda(c, lt, 8)
compare_to_logic_sim(wsim)

Loading…
Cancel
Save