Browse Source

doc improvements

devel
Stefan Holst 2 years ago
parent
commit
0b15f9fa18
  1. 6
      examples/Introduction.ipynb
  2. 4
      src/kyupy/bench.py
  3. 166
      src/kyupy/circuit.py
  4. 197
      src/kyupy/logic.py
  5. 20
      src/kyupy/sdf.py
  6. 4
      src/kyupy/stil.py
  7. 2
      src/kyupy/verilog.py
  8. BIN
      tests/b15_2ig.v.gz
  9. BIN
      tests/b15_4ig.sdf.gz
  10. BIN
      tests/b15_4ig.v.gz
  11. 2
      tests/test_sdf.py

6
examples/Introduction.ipynb

@ -1944,7 +1944,7 @@ @@ -1944,7 +1944,7 @@
"from kyupy import sdf\n",
"\n",
"df = sdf.load('../tests/b15_2ig.sdf.gz')\n",
"delays = df.annotation(b15, dataset=0, interconnect=False)"
"delays = df.iopaths(b15)[0]"
]
},
{
@ -1989,7 +1989,7 @@ @@ -1989,7 +1989,7 @@
{
"data": {
"text/plain": [
"78296"
"79010"
]
},
"execution_count": 64,
@ -2209,7 +2209,7 @@ @@ -2209,7 +2209,7 @@
{
"data": {
"text/plain": [
"1.1518999338150024"
"1.0424000024795532"
]
},
"execution_count": 71,

4
src/kyupy/bench.py

@ -57,8 +57,8 @@ def parse(text, name=None): @@ -57,8 +57,8 @@ def parse(text, name=None):
def load(file, name=None):
"""Parses the contents of ``file`` as ISCAS89 bench code.
:param file: The file to be loaded.
:param name: The name of the circuit. If none given, the file name is used as circuit name.
: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, the file name is used as circuit name.
:return: A :class:`Circuit` object.
"""
return parse(readtext(file), name=name or str(file))

166
src/kyupy/circuit.py

@ -65,9 +65,9 @@ class Node: @@ -65,9 +65,9 @@ class Node:
self.index = len(circuit.nodes) - 1
"""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
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()
"""A list of input connections (:class:`Line` objects).
@ -136,7 +136,7 @@ class Line: @@ -136,7 +136,7 @@ class Line:
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
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())
self.driver = driver[0]
@ -145,7 +145,7 @@ class Line: @@ -145,7 +145,7 @@ class Line:
self.driver_pin = driver[1]
"""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`.
"""
if not isinstance(reader, tuple): reader = (reader, reader.ins.free_index())
@ -155,7 +155,7 @@ class Line: @@ -155,7 +155,7 @@ class Line:
self.reader_pin = reader[1]
"""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`.
"""
self.driver.outs[self.driver_pin] = self
@ -209,17 +209,21 @@ class Circuit: @@ -209,17 +209,21 @@ class Circuit:
self.name = name
"""The name of the circuit.
"""
self.nodes = IndexList()
self.nodes : list[Node] = IndexList()
"""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`.
This list should 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.
The position of a line in this list equals its index :code:`self.lines[42].index == 42`.
This list should not be changed directly.
Use the :class:`Line` constructor and :py:attr:`Line.remove()` to add and remove lines.
"""
self.io_nodes = GrowingList()
self.io_nodes : list[Node] = GrowingList()
"""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:`io_nodes`.
@ -237,8 +241,74 @@ class Circuit: @@ -237,8 +241,74 @@ class Circuit:
@property
def s_nodes(self):
"""A list of all io_nodes as well as all flip-flops and latches in the circuit (in that order).
"""
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` array.
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 number 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):
return self.forks[name] if name in self.forks else Node(self, name)
@ -289,46 +359,21 @@ class Circuit: @@ -289,46 +359,21 @@ class Circuit:
def __repr__(self):
return f'{{name: "{self.name}", cells: {len(self.cells)}, forks: {len(self.forks)}, lines: {len(self.lines)}, io_nodes: {len(self.io_nodes)}}}'
@property
def stats(self):
"""Generates a dictionary with the number 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 `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 topological_order(self):
"""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
yielded first.
Nodes without input lines and nodes whose :py:attr:`Node.kind` contains the
substrings 'dff' or 'latch' are yielded first.
"""
visit_count = [0] * len(self.nodes)
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:
n = queue.popleft()
for line in n.outs:
if line is None: continue
succ = line.reader
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)
yield n
@ -343,17 +388,17 @@ class Circuit: @@ -343,17 +388,17 @@ class Circuit:
def reversed_topological_order(self):
"""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
yielded first.
Nodes without output lines and nodes whose :py:attr:`Node.kind` contains the
substrings 'dff' or 'latch' are yielded first.
"""
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:
n = queue.popleft()
for line in n.ins:
pred = line.driver
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)
yield n
@ -393,42 +438,3 @@ class Circuit: @@ -393,42 +438,3 @@ class Circuit:
queue.extend(preds)
region.append(n)
yield stem, region
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. `data[0]`) to MSB (e.g. `data[31]`).
If a prefix matches multiple different signals or busses, alphanumerically sorted
lists of lists are returned. Therefore, higher-dimensional busses
(e.g. `data0[0], data0[1], ...`, `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` array.
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

197
src/kyupy/logic.py

@ -1,4 +1,9 @@ @@ -1,4 +1,9 @@
"""This module contains definitions and utilities for 2-, 4-, and 8-valued logic operations.
"""This module contains definitions and tools for 2-, 4-, and 8-valued logic operations.
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.
@ -21,10 +26,22 @@ Except when bit0 differs from bit1, but bit2 (activity) is 0: @@ -21,10 +26,22 @@ Except when bit0 differs from bit1, but bit2 (activity) is 0:
4-valued logic only considers bit0 and bit1.
8-valued logic considers all 3 bits.
Logic values are stored in numpy arrays of data type ``np.uint8``.
The axis convention is as follows:
* The **last** axis goes along patterns/vectors. I.e. ``values[...,0]`` is pattern 0, ``values[...,1]`` is pattern 1, etc.
* 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 ``c.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.
"""
import math
from collections.abc import Iterable
import numpy as np
@ -80,41 +97,12 @@ def interpret(value): @@ -80,41 +97,12 @@ def interpret(value):
return UNKNOWN
_bit_in_lut = np.array([2 ** x for x in range(7, -1, -1)], dtype='uint8')
@numba.njit
def bit_in(a, pos):
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.
"""
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.
def mvarray(*a):
"""Converts (lists of) Boolean values or strings into a multi-valued array.
Similary 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`.
The given values are interpreted and the axes are arranged as per KyuPy's convention.
Use this function to convert strings into multi-valued arrays.
"""
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])
def mvarray(*a):
mva = np.array(interpret(a), dtype=np.uint8)
if mva.ndim < 2: return mva
if mva.shape[-2] > 1: return mva.swapaxes(-1, -2)
@ -122,24 +110,13 @@ def mvarray(*a): @@ -122,24 +110,13 @@ def mvarray(*a):
def mv_str(mva, delim='\n'):
"""Renders a given multi-valued array into a string.
"""
sa = np.choose(mva, np.array([*'0X-1PRFN'], dtype=np.unicode_))
if mva.ndim == 1: return ''.join(sa)
return delim.join([''.join(c) for c in sa.swapaxes(-1,-2)])
def mv_to_bp(mva):
if mva.ndim == 1: mva = mva[..., np.newaxis]
return np.packbits(unpackbits(mva)[...,:3], axis=-2, bitorder='little').swapaxes(-1,-2)
def bparray(*a):
return mv_to_bp(mvarray(*a))
def bp_to_mv(bpa):
return packbits(np.unpackbits(bpa, axis=-1, bitorder='little').swapaxes(-1,-2))
def _mv_not(out, inp):
np.bitwise_xor(inp, 0b11, out=out) # this also exchanges UNASSIGNED <-> UNKNOWN
np.putmask(out, (inp == UNKNOWN), UNKNOWN) # restore UNKNOWN
@ -148,13 +125,10 @@ def _mv_not(out, inp): @@ -148,13 +125,10 @@ def _mv_not(out, inp):
def mv_not(x1 : np.ndarray, out=None):
"""A multi-valued NOT operator.
:param x1: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts.
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray`
is returned.
:return: An :py:class:`MVArray` with the result.
:param x1: A multi-valued array.
:param out: An optional storage destination. If None, a new multi-valued array is returned.
:return: A multi-valued array with the result.
"""
#m = mv_getm(x1)
#x1 = mv_cast(x1, m=m)[0]
out = out or np.empty(x1.shape, dtype=np.uint8)
_mv_not(out, x1)
return out
@ -176,14 +150,11 @@ def _mv_or(out, *ins): @@ -176,14 +150,11 @@ def _mv_or(out, *ins):
def mv_or(x1, x2, out=None):
"""A multi-valued OR operator.
:param x1: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts.
:param x2: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts.
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray`
is returned.
:return: An :py:class:`MVArray` with the result.
:param x1: A multi-valued array.
:param x2: A multi-valued array.
:param out: An optional storage destination. If None, a new multi-valued array is returned.
:return: A multi-valued array with the result.
"""
#m = mv_getm(x1, x2)
#x1, x2 = mv_cast(x1, x2, m=m)
out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8)
_mv_or(out, x1, x2)
return out
@ -206,14 +177,11 @@ def _mv_and(out, *ins): @@ -206,14 +177,11 @@ def _mv_and(out, *ins):
def mv_and(x1, x2, out=None):
"""A multi-valued AND operator.
:param x1: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts.
:param x2: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts.
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray`
is returned.
:return: An :py:class:`MVArray` with the result.
:param x1: A multi-valued array.
:param x2: A multi-valued array.
:param out: An optional storage destination. If None, a new multi-valued array is returned.
:return: A multi-valued array with the result.
"""
#m = mv_getm(x1, x2)
#x1, x2 = mv_cast(x1, x2, m=m)
out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8)
_mv_and(out, x1, x2)
return out
@ -233,24 +201,28 @@ def _mv_xor(out, *ins): @@ -233,24 +201,28 @@ def _mv_xor(out, *ins):
def mv_xor(x1, x2, out=None):
"""A multi-valued XOR operator.
:param x1: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts.
:param x2: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts.
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray`
is returned.
:return: An :py:class:`MVArray` with the result.
:param x1: A multi-valued array.
:param x2: A multi-valued array.
:param out: An optional storage destination. If None, a new multi-valued array is returned.
:return: A multi-valued array with the result.
"""
#m = mv_getm(x1, x2)
#x1, x2 = mv_cast(x1, x2, m=m)
out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8)
_mv_xor(out, x1, x2)
return out
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)
#d, t, q_prev = mv_cast(d, t, q_prev, m=m)
out = out or np.empty(np.broadcast(d, t, q_prev).shape, dtype=np.uint8)
out[...] = t & d & 0b011
out[...] |= ~t & 0b010 & (q_prev << 1)
@ -268,16 +240,11 @@ def mv_transition(init, final, out=None): @@ -268,16 +240,11 @@ 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``.
If both inputs are ``UNASSIGNED``, the result is ``UNASSIGNED``.
:param init: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts.
:param final: An :py:class:`MVArray` or data the :py:class:`MVArray` constructor accepts.
:param out: Optionally an :py:class:`MVArray` as storage destination. If None, a new :py:class:`MVArray`
is returned.
:return: An :py:class:`MVArray` with the result.
:param init: A multi-valued array.
:param final: A multi-valued array.
:param out: An optional storage destination. If None, a new multi-valued array is returned.
:return: A multi-valued array with the result.
"""
#m = mv_getm(init, final)
#init, final = mv_cast(init, final, m=m)
#init = init.data
#final = final.data
out = out or np.empty(np.broadcast(init, final).shape, dtype=np.uint8)
out[...] = (init & 0b010) | (final & 0b001)
out[...] |= ((out << 1) ^ (out << 2)) & 0b100
@ -288,6 +255,28 @@ def mv_transition(init, final, out=None): @@ -288,6 +255,28 @@ def mv_transition(init, final, out=None):
return out
def mv_to_bp(mva):
"""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))
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 bp_buf(out, inp):
unknown = (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :]
out[..., 0, :] = inp[..., 0, :] | unknown
@ -356,3 +345,37 @@ def bp_latch(out, d, t, q_prev): @@ -356,3 +345,37 @@ def bp_latch(out, d, t, q_prev):
out[..., 1, :] &= ~any_unknown
out[..., 2, :] &= ~any_unknown
return out
_bit_in_lut = np.array([2 ** x for x in range(7, -1, -1)], dtype='uint8')
@numba.njit
def bit_in(a, pos):
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])

20
src/kyupy/sdf.py

@ -1,10 +1,10 @@ @@ -1,10 +1,10 @@
"""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.
Sophisticated timing specifications (timing checks, conditional delays, etc.) are currently not supported.
This parser extracts pin-to-pin delay and interconnect delay information from SDF files.
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.
Call :py:func:`DelayFile.iopaths` to match the intermediate representation to a given circuit.
"""
@ -38,12 +38,12 @@ class DelayFile: @@ -38,12 +38,12 @@ class DelayFile:
def iopaths(self, circuit:Circuit, tlib=TechLib()):
"""Constructs an ndarray containing all IOPATH delays.
All IOPATH delays for a node `n` are 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.
Axis 0: dataset (usually 3 datasets per SDF-file)
Axis 1: line index (e.g. `n.ins[0]`, `n.ins[1]`)
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'
Axis 3: polarity of the transition at the IOPATH-output (at `n.outs[0]`), 0='rising/posedge', 1='falling/negedge'
* Axis 0: dataset (usually 3 datasets per SDF-file)
* Axis 1: line index (e.g. ``n.ins[0]``, ``n.ins[1]``)
* 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'
* Axis 3: polarity of the transition at the IOPATH-output (at ``n.outs[0]``), 0='rising/posedge', 1='falling/negedge'
"""
def find_cell(name:str):
@ -72,6 +72,8 @@ class DelayFile: @@ -72,6 +72,8 @@ class DelayFile:
def annotation(self, circuit:Circuit, tlib=TechLib(), dataset=1, interconnect=True, ffdelays=True):
"""Constructs an 3-dimensional ndarray with timing data for each line in ``circuit``.
DEPRECATED
An IOPATH delay for a node is annotated to the line connected to the input pin specified in the IOPATH.
Currently, only ABSOLUTE IOPATH and INTERCONNECT delays are supported.
@ -266,6 +268,6 @@ def parse(text): @@ -266,6 +268,6 @@ def parse(text):
def load(file):
"""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))

4
src/kyupy/stil.py

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
"""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.
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).
Call :py:func:`StilFile.tests`, :py:func:`StilFile.tests_loc`, or :py:func:`StilFile.responses` to
@ -248,6 +248,6 @@ def parse(text): @@ -248,6 +248,6 @@ def parse(text):
def load(file):
"""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))

2
src/kyupy/verilog.py

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
"""A simple and incomplete parser for Verilog files.
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

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.

2
tests/test_sdf.py

@ -80,7 +80,7 @@ def test_b14(mydir): @@ -80,7 +80,7 @@ def test_b14(mydir):
def test_gates(mydir):
c = verilog.load(mydir / 'gates.v')
df = sdf.load(mydir / 'gates.sdf')
lt = df.annotation(c, dataset=1)
lt = df.iopaths(c)[1]
nand_a = c.cells['nandgate'].ins[0]
nand_b = c.cells['nandgate'].ins[1]
and_a = c.cells['andgate'].ins[0]

Loading…
Cancel
Save