Browse Source

doc improvements

devel
Stefan Holst 1 year 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 @@
"from kyupy import sdf\n", "from kyupy import sdf\n",
"\n", "\n",
"df = sdf.load('../tests/b15_2ig.sdf.gz')\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 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"78296" "79010"
] ]
}, },
"execution_count": 64, "execution_count": 64,
@ -2209,7 +2209,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"1.1518999338150024" "1.0424000024795532"
] ]
}, },
"execution_count": 71, "execution_count": 71,

4
src/kyupy/bench.py

@ -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))

166
src/kyupy/circuit.py

@ -65,9 +65,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).
@ -136,7 +136,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]
@ -145,7 +145,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())
@ -155,7 +155,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
@ -209,17 +209,21 @@ class Circuit:
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 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. """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 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. """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`. Port-nodes are contained in :py:attr:`nodes` as well as :py:attr:`io_nodes`.
@ -237,8 +241,74 @@ class Circuit:
@property @property
def s_nodes(self): 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()] 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): 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)
@ -289,46 +359,21 @@ class Circuit:
def __repr__(self): 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)}}}' 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): 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 = [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: 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
@ -343,17 +388,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
@ -393,42 +438,3 @@ class Circuit:
queue.extend(preds) queue.extend(preds)
region.append(n) region.append(n)
yield stem, region 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 @@
"""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. 8 logic values are defined as integer constants.
@ -21,10 +26,22 @@ Except when bit0 differs from bit1, but bit2 (activity) is 0:
4-valued logic only considers bit0 and bit1. 4-valued logic only considers bit0 and bit1.
8-valued logic considers all 3 bits. 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 from collections.abc import Iterable
import numpy as np import numpy as np
@ -80,41 +97,12 @@ def interpret(value):
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
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.
Similary to `np.packbits`, but returns an array of given dtype and the shape of `a` with the last axis removed. The given values are interpreted and the axes are arranged as per KyuPy's convention.
The last axis of `a` is truncated or padded according to the bit-width of the given dtype. Use this function to convert strings into multi-valued arrays.
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])
def mvarray(*a):
mva = np.array(interpret(a), dtype=np.uint8) mva = np.array(interpret(a), dtype=np.uint8)
if mva.ndim < 2: return mva if mva.ndim < 2: return mva
if mva.shape[-2] > 1: return mva.swapaxes(-1, -2) if mva.shape[-2] > 1: return mva.swapaxes(-1, -2)
@ -122,24 +110,13 @@ def mvarray(*a):
def mv_str(mva, delim='\n'): 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_)) sa = np.choose(mva, np.array([*'0X-1PRFN'], dtype=np.unicode_))
if mva.ndim == 1: return ''.join(sa) if mva.ndim == 1: return ''.join(sa)
return delim.join([''.join(c) for c in sa.swapaxes(-1,-2)]) 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): def _mv_not(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
np.putmask(out, (inp == UNKNOWN), UNKNOWN) # restore UNKNOWN np.putmask(out, (inp == UNKNOWN), UNKNOWN) # restore UNKNOWN
@ -148,13 +125,10 @@ def _mv_not(out, inp):
def mv_not(x1 : np.ndarray, 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)
#x1 = mv_cast(x1, m=m)[0]
out = out or np.empty(x1.shape, dtype=np.uint8) out = out or np.empty(x1.shape, dtype=np.uint8)
_mv_not(out, x1) _mv_not(out, x1)
return out return out
@ -176,14 +150,11 @@ def _mv_or(out, *ins):
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)
#x1, x2 = mv_cast(x1, x2, m=m)
out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8) out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8)
_mv_or(out, x1, x2) _mv_or(out, x1, x2)
return out return out
@ -206,14 +177,11 @@ def _mv_and(out, *ins):
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)
#x1, x2 = mv_cast(x1, x2, m=m)
out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8) out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8)
_mv_and(out, x1, x2) _mv_and(out, x1, x2)
return out return out
@ -233,24 +201,28 @@ def _mv_xor(out, *ins):
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)
#x1, x2 = mv_cast(x1, x2, m=m)
out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8) out = out or np.empty(np.broadcast(x1, x2).shape, dtype=np.uint8)
_mv_xor(out, x1, x2) _mv_xor(out, x1, x2)
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)
#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 = out or np.empty(np.broadcast(d, t, q_prev).shape, dtype=np.uint8)
out[...] = t & d & 0b011 out[...] = t & d & 0b011
out[...] |= ~t & 0b010 & (q_prev << 1) out[...] |= ~t & 0b010 & (q_prev << 1)
@ -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``. 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)
#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 = out or np.empty(np.broadcast(init, final).shape, dtype=np.uint8)
out[...] = (init & 0b010) | (final & 0b001) out[...] = (init & 0b010) | (final & 0b001)
out[...] |= ((out << 1) ^ (out << 2)) & 0b100 out[...] |= ((out << 1) ^ (out << 2)) & 0b100
@ -288,6 +255,28 @@ def mv_transition(init, final, out=None):
return out 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): def bp_buf(out, inp):
unknown = (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :] unknown = (inp[..., 0, :] ^ inp[..., 1, :]) & ~inp[..., 2, :]
out[..., 0, :] = inp[..., 0, :] | unknown out[..., 0, :] = inp[..., 0, :] | unknown
@ -356,3 +345,37 @@ def bp_latch(out, d, t, q_prev):
out[..., 1, :] &= ~any_unknown out[..., 1, :] &= ~any_unknown
out[..., 2, :] &= ~any_unknown out[..., 2, :] &= ~any_unknown
return out 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 @@
"""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). 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:
def iopaths(self, circuit:Circuit, tlib=TechLib()): def iopaths(self, circuit:Circuit, tlib=TechLib()):
"""Constructs an ndarray containing all IOPATH delays. """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 0: dataset (usually 3 datasets per SDF-file)
Axis 1: line index (e.g. `n.ins[0]`, `n.ins[1]`) * 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 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 3: polarity of the transition at the IOPATH-output (at ``n.outs[0]``), 0='rising/posedge', 1='falling/negedge'
""" """
def find_cell(name:str): def find_cell(name:str):
@ -72,6 +72,8 @@ class DelayFile:
def annotation(self, circuit:Circuit, tlib=TechLib(), dataset=1, interconnect=True, ffdelays=True): 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``. """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. 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. Currently, only ABSOLUTE IOPATH and INTERCONNECT delays are supported.
@ -266,6 +268,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))

4
src/kyupy/stil.py

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

2
src/kyupy/verilog.py

@ -1,7 +1,7 @@
"""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

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):
def test_gates(mydir): def test_gates(mydir):
c = verilog.load(mydir / 'gates.v') c = verilog.load(mydir / 'gates.v')
df = sdf.load(mydir / 'gates.sdf') 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_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]

Loading…
Cancel
Save