diff --git a/Demo.ipynb b/Demo.ipynb index 288f1bd..0da44a0 100644 --- a/Demo.ipynb +++ b/Demo.ipynb @@ -489,11 +489,11 @@ "\n", "for cell in b14.topological_order():\n", " if 'DFF' in cell.kind or 'input' == cell.kind:\n", - " levels[cell.index] = 0\n", + " levels[cell] = 0\n", " elif '__fork__' == cell.kind:\n", - " levels[cell.index] = levels[cell.ins[0].driver.index] # forks only have exactly one driver\n", + " levels[cell] = levels[cell.ins[0].driver] # forks only have exactly one driver\n", " else:\n", - " levels[cell.index] = max([levels[line.driver.index] for line in cell.ins]) + 1\n", + " levels[cell] = max([levels[line.driver] for line in cell.ins]) + 1\n", " \n", "print(f'Maximum logic depth: {np.max(levels)}')" ] @@ -1118,6 +1118,7 @@ "metadata": {}, "source": [ "The capture data contains for each PI, PO, and scan flip-flop (axis 0), and each test (axis 1) seven values:\n", + "\n", "0. Probability of capturing a 1 at the given capture time (same as next value, if no standard deviation given).\n", "1. A capture value decided by random sampling according to above probability.\n", "2. The final value (assume a very late capture time).\n", diff --git a/docs/simulators.rst b/docs/simulators.rst index 1cb48bb..bcc0ea4 100644 --- a/docs/simulators.rst +++ b/docs/simulators.rst @@ -14,6 +14,7 @@ Timing Simulation - :mod:`kyupy.wave_sim` ----------------------------------------- .. automodule:: kyupy.wave_sim + :members: TMAX, TMAX_OVL, TMIN .. autoclass:: kyupy.wave_sim.WaveSim :members: diff --git a/src/kyupy/circuit.py b/src/kyupy/circuit.py index 63b132a..68d65ec 100644 --- a/src/kyupy/circuit.py +++ b/src/kyupy/circuit.py @@ -245,8 +245,9 @@ class Circuit: return header + '\n'.join([str(n) for n in self.nodes]) def __repr__(self): - name = f" '{self.name}'" if self.name else '' - return f'' + name = f' {self.name}' if self.name else '' + return f'' def topological_order(self): """Generator function to iterate over all nodes in topological order. diff --git a/src/kyupy/logic.py b/src/kyupy/logic.py index 559c50c..9cf1067 100644 --- a/src/kyupy/logic.py +++ b/src/kyupy/logic.py @@ -58,6 +58,12 @@ on a signal. ``'N'``, ``'n'``, and ``'v'`` are interpreted as ``NPULSE``. def interpret(value): + """Converts characters, strings, and lists of them to lists of logic constants defined above. + + :param value: A character (string of length 1), Boolean, Integer, None, or Iterable. + Iterables (such as strings) are traversed and their individual characters are interpreted. + :return: A logic constant or a (possibly multi-dimensional) list of logic constants. + """ if isinstance(value, Iterable) and not (isinstance(value, str) and len(value) == 1): return list(map(interpret, value)) if value in [0, '0', False, 'L', 'l']: @@ -85,6 +91,79 @@ def bit_in(a, pos): return a[pos >> 3] & _bit_in_lut[pos & 7] +class MVArray: + """An n-dimensional array of m-valued logic values. + + This class wraps a numpy.ndarray of type uint8 and adds support for encoding and + interpreting 2-valued, 4-valued, and 8-valued logic values. + Each logic value is stored as an uint8, manipulations of individual values are cheaper than in + :py:class:`BPArray`. + + :param a: If a tuple is given, it is interpreted as desired shape. To make an array of ``n`` vectors + compatible with a simulator ``sim``, use ``(len(sim.interface), n)``. If a :py:class:`BPArray` or + :py:class:`MVArray` is given, a deep copy is made. If a string, a list of strings, a list of characters, + or a list of lists of characters are given, the data is interpreted best-effort and the array is + initialized accordingly. + :param m: The arity of the logic. Can be set to 2, 4, or 8. If None is given, the arity of a given + :py:class:`BPArray` or :py:class:`MVArray` is used, or, if the array is initialized differently, 8 is used. + """ + + def __init__(self, a, m=None): + self.m = m or 8 + assert self.m in [2, 4, 8] + + # Try our best to interpret given a. + if isinstance(a, MVArray): + self.data = a.data.copy() + """The wrapped 2-dimensional ndarray of logic values. + + * Axis 0 is PI/PO/FF position, the length of this axis is called "width". + * Axis 1 is vector/pattern, the length of this axis is called "length". + """ + self.m = m or a.m + elif hasattr(a, 'data'): # assume it is a BPArray. Can't use isinstance() because BPArray isn't declared yet. + self.data = np.zeros((a.width, a.length), dtype=np.uint8) + self.m = m or a.m + for i in range(a.data.shape[-2]): + self.data[...] <<= 1 + self.data[...] |= np.unpackbits(a.data[..., -i-1, :], axis=1)[:, :a.length] + if a.data.shape[-2] == 1: + self.data *= 3 + elif isinstance(a, int): + self.data = np.full((a, 1), UNASSIGNED, dtype=np.uint8) + elif isinstance(a, tuple): + self.data = np.full(a, UNASSIGNED, dtype=np.uint8) + else: + if isinstance(a, str): a = [a] + self.data = np.asarray(interpret(a), dtype=np.uint8) + self.data = self.data[:, np.newaxis] if self.data.ndim == 1 else np.moveaxis(self.data, -2, -1) + + # Cast data to m-valued logic. + if self.m == 2: + self.data[...] = ((self.data & 0b001) & ((self.data >> 1) & 0b001) | (self.data == RISE)) * ONE + elif self.m == 4: + self.data[...] = (self.data & 0b011) & ((self.data != FALL) * ONE) | ((self.data == RISE) * ONE) + elif self.m == 8: + self.data[...] = self.data & 0b111 + + self.length = self.data.shape[-1] + self.width = self.data.shape[-2] + + def __repr__(self): + return f'' + + def __str__(self): + return str([self[idx] for idx in range(self.length)]) + + def __getitem__(self, vector_idx): + """Returns a string representing the desired vector.""" + chars = ["0", "X", "-", "1", "P", "R", "F", "N"] + return ''.join(chars[v] for v in self.data[:, vector_idx]) + + def __len__(self): + return self.length + + def mv_cast(*args, m=8): return [a if isinstance(a, MVArray) else MVArray(a, m=m) for a in args] @@ -100,6 +179,13 @@ def _mv_not(m, out, inp): def mv_not(x1, 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. + """ m = mv_getm(x1) x1 = mv_cast(x1, m=m)[0] out = out or MVArray(x1.data.shape, m=m) @@ -125,6 +211,14 @@ def _mv_or(m, 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. + """ m = mv_getm(x1, x2) x1, x2 = mv_cast(x1, x2, m=m) out = out or MVArray(np.broadcast(x1.data, x2.data).shape, m=m) @@ -151,6 +245,14 @@ def _mv_and(m, 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. + """ m = mv_getm(x1, x2) x1, x2 = mv_cast(x1, x2, m=m) out = out or MVArray(np.broadcast(x1.data, x2.data).shape, m=m) @@ -174,6 +276,14 @@ def _mv_xor(m, 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. + """ m = mv_getm(x1, x2) x1, x2 = mv_cast(x1, x2, m=m) out = out or MVArray(np.broadcast(x1.data, x2.data).shape, m=m) @@ -182,6 +292,16 @@ def mv_xor(x1, x2, out=None): def mv_transition(init, final, out=None): + """Computes the logic transitions from the initial values of ``init`` to the final values of ``final``. + 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. + """ m = mv_getm(init, final) init, final = mv_cast(init, final, m=m) init = init.data @@ -196,65 +316,46 @@ def mv_transition(init, final, out=None): return out -class MVArray: - """An n-dimensional array of m-valued logic values. - - This class wraps a numpy.ndarray of type uint8 and adds support for encoding and - interpreting 2-valued, 4-valued, and 8-valued logic values. - Each logic value is stored as an uint8, value manipulations are cheaper than in BPArray. - - An MVArray always has 2 axes: +class BPArray: + """An n-dimensional array of m-valued logic values that uses bit-parallel storage. - * Axis 0 is PI/PO/FF position, the length of this axis is called "width". - * Axis 1 is vector/pattern, the length of this axis is called "length". + The primary use of this format is in aiding efficient bit-parallel logic simulation. + The secondary benefit over :py:class:`MVArray` is its memory efficiency. + Accessing individual values is more expensive than with :py:class:`MVArray`. + Therefore it may be more efficient to unpack the data into an :py:class:`MVArray` and pack it again into a + :py:class:`BPArray` for simulation. + See :py:class:`MVArray` for constructor parameters. """ def __init__(self, a, m=None): - self.m = m or 8 - assert self.m in [2, 4, 8] - - # Try our best to interpret given a. + if not isinstance(a, MVArray) and not isinstance(a, BPArray): + a = MVArray(a, m) + self.m = a.m if isinstance(a, MVArray): - self.data = a.data.copy() - self.m = m or a.m - elif hasattr(a, 'data'): # assume it is a BPArray. Can't use isinstance() because BPArray isn't declared yet. - self.data = np.zeros((a.width, a.length), dtype=np.uint8) - self.m = m or a.m - for i in range(a.data.shape[-2]): - self.data[...] <<= 1 - self.data[...] |= np.unpackbits(a.data[..., -i-1, :], axis=1)[:, :a.length] - if a.data.shape[-2] == 1: - self.data *= 3 - elif isinstance(a, int): - self.data = np.full((a, 1), UNASSIGNED, dtype=np.uint8) - elif isinstance(a, tuple): - self.data = np.full(a, UNASSIGNED, dtype=np.uint8) - else: - if isinstance(a, str): a = [a] - self.data = np.asarray(interpret(a), dtype=np.uint8) - self.data = self.data[:, np.newaxis] if self.data.ndim == 1 else np.moveaxis(self.data, -2, -1) - - # Cast data to m-valued logic. - if self.m == 2: - self.data[...] = ((self.data & 0b001) & ((self.data >> 1) & 0b001) | (self.data == RISE)) * ONE - elif self.m == 4: - self.data[...] = (self.data & 0b011) & ((self.data != FALL) * ONE) | ((self.data == RISE) * ONE) - elif self.m == 8: - self.data[...] = self.data & 0b111 + if m is not None and m != a.m: + a = MVArray(a, m) # cast data + self.m = a.m + assert self.m in [2, 4, 8] + nwords = math.ceil(math.log2(self.m)) + nbytes = (a.data.shape[-1] - 1) // 8 + 1 + self.data = np.zeros(a.data.shape[:-1] + (nwords, nbytes), dtype=np.uint8) + """The wrapped 3-dimensional ndarray. - self.length = self.data.shape[-1] - self.width = self.data.shape[-2] + * Axis 0 is PI/PO/FF position, the length of this axis is called "width". + * Axis 1 has length ``ceil(log2(m))`` for storing all bits. + * Axis 2 are the vectors/patterns packed into uint8 words. + """ + for i in range(self.data.shape[-2]): + self.data[..., i, :] = np.packbits((a.data >> i) & 1, axis=-1) + else: # we have a BPArray + self.data = a.data.copy() # TODO: support conversion to different m + self.m = a.m + self.length = a.length + self.width = a.width def __repr__(self): - return f'' - - def __str__(self): - return str([self[idx] for idx in range(self.length)]) - - def __getitem__(self, vector_idx): - chars = ["0", "X", "-", "1", "P", "R", "F", "N"] - return ''.join(chars[v] for v in self.data[:, vector_idx]) + return f'' def __len__(self): return self.length @@ -359,44 +460,3 @@ def bp_xor(out, *ins): out[..., 0, :] |= any_unknown out[..., 1, :] &= ~any_unknown out[..., 2, :] &= ~any_unknown - - -class BPArray: - """An n-dimensional array of m-valued logic values that uses bit-parallel storage. - - The primary use of this format is in aiding efficient bit-parallel logic simulation. - The secondary benefit over MVArray is its memory efficiency. - Accessing individual values is more expensive than with :py:class:`MVArray`. - It is advised to first construct a MVArray, pack it into a :py:class:`BPArray` for simulation and unpack the results - back into a :py:class:`MVArray` for value access. - - The values along the last axis (vectors/patterns) are packed into uint8 words. - The second-last axis has length ceil(log2(m)) for storing all bits. - All other axes stay the same as in MVArray. - """ - - def __init__(self, a, m=None): - if not isinstance(a, MVArray) and not isinstance(a, BPArray): - a = MVArray(a, m) - self.m = a.m - if isinstance(a, MVArray): - if m is not None and m != a.m: - a = MVArray(a, m) # cast data - self.m = a.m - assert self.m in [2, 4, 8] - nwords = math.ceil(math.log2(self.m)) - nbytes = (a.data.shape[-1] - 1) // 8 + 1 - self.data = np.zeros(a.data.shape[:-1] + (nwords, nbytes), dtype=np.uint8) - for i in range(self.data.shape[-2]): - self.data[..., i, :] = np.packbits((a.data >> i) & 1, axis=-1) - else: # we have a BPArray - self.data = a.data.copy() # TODO: support conversion to different m - self.m = a.m - self.length = a.length - self.width = a.width - - def __repr__(self): - return f'' - - def __len__(self): - return self.length diff --git a/src/kyupy/logic_sim.py b/src/kyupy/logic_sim.py index c52c34e..993938a 100644 --- a/src/kyupy/logic_sim.py +++ b/src/kyupy/logic_sim.py @@ -126,10 +126,10 @@ class LogicSim: def cycle(self, state, inject_cb=None): """Assigns the given state, propagates it and captures the new state. - :param responses: A bit-parallel array in a compatible shape holding the current circuit state. + :param state: A bit-parallel array in a compatible shape holding the current circuit state. The contained data is assigned to the PI and PPI and overwritten by data at the PO and PPO after propagation. - :type responses: :py:class:`~kyupy.logic.BPArray` + :type state: :py:class:`~kyupy.logic.BPArray` :param inject_cb: A callback function for manipulating intermediate signal values. See :py:func:`propagate`. :returns: The given state object. """ diff --git a/src/kyupy/wave_sim.py b/src/kyupy/wave_sim.py index 4902f1a..cea2202 100644 --- a/src/kyupy/wave_sim.py +++ b/src/kyupy/wave_sim.py @@ -332,8 +332,8 @@ class WaveSim: self.lst_eat_valid = False def wave(self, line, vector): - """Returns the desired waveform from the simulation state. Only valid, if simulator was - instanciated with ``keep_waveforms=True``.""" + # """Returns the desired waveform from the simulation state. Only valid, if simulator was + # instantiated with ``keep_waveforms=True``.""" if line < 0: return [TMAX] mem, wcap, _ = self.sat[line]