diff --git a/examples/Introduction.ipynb b/examples/Introduction.ipynb index fa72d0d..7c70c46 100644 --- a/examples/Introduction.ipynb +++ b/examples/Introduction.ipynb @@ -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 @@ { "data": { "text/plain": [ - "78296" + "79010" ] }, "execution_count": 64, @@ -2209,7 +2209,7 @@ { "data": { "text/plain": [ - "1.1518999338150024" + "1.0424000024795532" ] }, "execution_count": 71, diff --git a/src/kyupy/bench.py b/src/kyupy/bench.py index df8bea9..aeec5a2 100644 --- a/src/kyupy/bench.py +++ b/src/kyupy/bench.py @@ -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)) diff --git a/src/kyupy/circuit.py b/src/kyupy/circuit.py index 3699d15..bbea2f4 100644 --- a/src/kyupy/circuit.py +++ b/src/kyupy/circuit.py @@ -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: 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: 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: 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: 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: @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: 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: 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: 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 diff --git a/src/kyupy/logic.py b/src/kyupy/logic.py index 85152fb..146bf7c 100644 --- a/src/kyupy/logic.py +++ b/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. @@ -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): 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): 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): 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): 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): 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): 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): 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): 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): 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]) diff --git a/src/kyupy/sdf.py b/src/kyupy/sdf.py index e2e9e0b..a726771 100644 --- a/src/kyupy/sdf.py +++ b/src/kyupy/sdf.py @@ -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: 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: 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): 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)) diff --git a/src/kyupy/stil.py b/src/kyupy/stil.py index 64c4920..9bcc376 100644 --- a/src/kyupy/stil.py +++ b/src/kyupy/stil.py @@ -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): 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)) diff --git a/src/kyupy/verilog.py b/src/kyupy/verilog.py index 10e5baf..43f5118 100644 --- a/src/kyupy/verilog.py +++ b/src/kyupy/verilog.py @@ -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 diff --git a/tests/b15_2ig.v.gz b/tests/b15_2ig.v.gz new file mode 100644 index 0000000..78d1a29 Binary files /dev/null and b/tests/b15_2ig.v.gz differ diff --git a/tests/b15_4ig.sdf.gz b/tests/b15_4ig.sdf.gz new file mode 100644 index 0000000..32db9bf Binary files /dev/null and b/tests/b15_4ig.sdf.gz differ diff --git a/tests/b15_4ig.v.gz b/tests/b15_4ig.v.gz new file mode 100644 index 0000000..bc98cc1 Binary files /dev/null and b/tests/b15_4ig.v.gz differ diff --git a/tests/test_sdf.py b/tests/test_sdf.py index b09469e..79e586d 100644 --- a/tests/test_sdf.py +++ b/tests/test_sdf.py @@ -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]