Browse Source

documentation improvements

devel
Stefan Holst 4 years ago
parent
commit
8b5a71f498
  1. 7
      Demo.ipynb
  2. 1
      docs/simulators.rst
  3. 5
      src/kyupy/circuit.py
  4. 242
      src/kyupy/logic.py
  5. 4
      src/kyupy/logic_sim.py
  6. 4
      src/kyupy/wave_sim.py

7
Demo.ipynb

@ -489,11 +489,11 @@
"\n", "\n",
"for cell in b14.topological_order():\n", "for cell in b14.topological_order():\n",
" if 'DFF' in cell.kind or 'input' == cell.kind:\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", " 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", " 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", " \n",
"print(f'Maximum logic depth: {np.max(levels)}')" "print(f'Maximum logic depth: {np.max(levels)}')"
] ]
@ -1118,6 +1118,7 @@
"metadata": {}, "metadata": {},
"source": [ "source": [
"The capture data contains for each PI, PO, and scan flip-flop (axis 0), and each test (axis 1) seven values:\n", "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", "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", "1. A capture value decided by random sampling according to above probability.\n",
"2. The final value (assume a very late capture time).\n", "2. The final value (assume a very late capture time).\n",

1
docs/simulators.rst

@ -14,6 +14,7 @@ Timing Simulation - :mod:`kyupy.wave_sim`
----------------------------------------- -----------------------------------------
.. automodule:: kyupy.wave_sim .. automodule:: kyupy.wave_sim
:members: TMAX, TMAX_OVL, TMIN
.. autoclass:: kyupy.wave_sim.WaveSim .. autoclass:: kyupy.wave_sim.WaveSim
:members: :members:

5
src/kyupy/circuit.py

@ -245,8 +245,9 @@ class Circuit:
return header + '\n'.join([str(n) for n in self.nodes]) return header + '\n'.join([str(n) for n in self.nodes])
def __repr__(self): def __repr__(self):
name = f" '{self.name}'" if self.name else '' name = f' {self.name}' if self.name else ''
return f'<Circuit{name} with {len(self.nodes)} nodes, {len(self.lines)} lines, {len(self.interface)} ports>' return f'<Circuit{name} cells={len(self.cells)} forks={len(self.forks)} ' + \
f'lines={len(self.lines)} ports={len(self.interface)}>'
def topological_order(self): def topological_order(self):
"""Generator function to iterate over all nodes in topological order. """Generator function to iterate over all nodes in topological order.

242
src/kyupy/logic.py

@ -58,6 +58,12 @@ on a signal. ``'N'``, ``'n'``, and ``'v'`` are interpreted as ``NPULSE``.
def interpret(value): 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): if isinstance(value, Iterable) and not (isinstance(value, str) and len(value) == 1):
return list(map(interpret, value)) return list(map(interpret, value))
if value in [0, '0', False, 'L', 'l']: if value in [0, '0', False, 'L', 'l']:
@ -85,6 +91,79 @@ def bit_in(a, pos):
return a[pos >> 3] & _bit_in_lut[pos & 7] 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'<MVArray length={self.length} width={self.width} m={self.m} mem={hr_bytes(self.data.nbytes)}>'
def __str__(self):
return str([self[idx] for idx in range(self.length)])
def __getitem__(self, vector_idx):
"""Returns a string representing the desired vector."""
chars = ["0", "X", "-", "1", "P", "R", "F", "N"]
return ''.join(chars[v] for v in self.data[:, vector_idx])
def __len__(self):
return self.length
def mv_cast(*args, m=8): def mv_cast(*args, m=8):
return [a if isinstance(a, MVArray) else MVArray(a, m=m) for a in args] 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): 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) m = mv_getm(x1)
x1 = mv_cast(x1, m=m)[0] x1 = mv_cast(x1, m=m)[0]
out = out or MVArray(x1.data.shape, m=m) 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): 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) m = mv_getm(x1, x2)
x1, x2 = mv_cast(x1, x2, m=m) x1, x2 = mv_cast(x1, x2, m=m)
out = out or MVArray(np.broadcast(x1.data, x2.data).shape, 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): 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) m = mv_getm(x1, x2)
x1, x2 = mv_cast(x1, x2, m=m) x1, x2 = mv_cast(x1, x2, m=m)
out = out or MVArray(np.broadcast(x1.data, x2.data).shape, 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): 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) m = mv_getm(x1, x2)
x1, x2 = mv_cast(x1, x2, m=m) x1, x2 = mv_cast(x1, x2, m=m)
out = out or MVArray(np.broadcast(x1.data, x2.data).shape, 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): 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) m = mv_getm(init, final)
init, final = mv_cast(init, final, m=m) init, final = mv_cast(init, final, m=m)
init = init.data init = init.data
@ -196,65 +316,46 @@ def mv_transition(init, final, out=None):
return out return out
class MVArray: class BPArray:
"""An n-dimensional array of m-valued logic values. """An n-dimensional array of m-valued logic values that uses bit-parallel storage.
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:
* Axis 0 is PI/PO/FF position, the length of this axis is called "width". The primary use of this format is in aiding efficient bit-parallel logic simulation.
* Axis 1 is vector/pattern, the length of this axis is called "length". 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): def __init__(self, a, m=None):
self.m = m or 8 if not isinstance(a, MVArray) and not isinstance(a, BPArray):
assert self.m in [2, 4, 8] a = MVArray(a, m)
self.m = a.m
# Try our best to interpret given a.
if isinstance(a, MVArray): if isinstance(a, MVArray):
self.data = a.data.copy() if m is not None and m != a.m:
self.m = m or a.m a = MVArray(a, m) # cast data
elif hasattr(a, 'data'): # assume it is a BPArray. Can't use isinstance() because BPArray isn't declared yet. self.m = a.m
self.data = np.zeros((a.width, a.length), dtype=np.uint8) assert self.m in [2, 4, 8]
self.m = m or a.m nwords = math.ceil(math.log2(self.m))
for i in range(a.data.shape[-2]): nbytes = (a.data.shape[-1] - 1) // 8 + 1
self.data[...] <<= 1 self.data = np.zeros(a.data.shape[:-1] + (nwords, nbytes), dtype=np.uint8)
self.data[...] |= np.unpackbits(a.data[..., -i-1, :], axis=1)[:, :a.length] """The wrapped 3-dimensional ndarray.
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] * Axis 0 is PI/PO/FF position, the length of this axis is called "width".
self.width = self.data.shape[-2] * 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): def __repr__(self):
return f'<MVArray length={self.length} width={self.width} m={self.m} mem={hr_bytes(self.data.nbytes)}>' return f'<BPArray length={self.length} width={self.width} m={self.m} mem={hr_bytes(self.data.nbytes)}>'
def __str__(self):
return str([self[idx] for idx in range(self.length)])
def __getitem__(self, vector_idx):
chars = ["0", "X", "-", "1", "P", "R", "F", "N"]
return ''.join(chars[v] for v in self.data[:, vector_idx])
def __len__(self): def __len__(self):
return self.length return self.length
@ -359,44 +460,3 @@ def bp_xor(out, *ins):
out[..., 0, :] |= any_unknown out[..., 0, :] |= any_unknown
out[..., 1, :] &= ~any_unknown out[..., 1, :] &= ~any_unknown
out[..., 2, :] &= ~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'<BPArray length={self.length} width={self.width} m={self.m} mem={hr_bytes(self.data.nbytes)}>'
def __len__(self):
return self.length

4
src/kyupy/logic_sim.py

@ -126,10 +126,10 @@ class LogicSim:
def cycle(self, state, inject_cb=None): def cycle(self, state, inject_cb=None):
"""Assigns the given state, propagates it and captures the new state. """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 The contained data is assigned to the PI and PPI and overwritten by data at the PO and PPO after
propagation. 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`. :param inject_cb: A callback function for manipulating intermediate signal values. See :py:func:`propagate`.
:returns: The given state object. :returns: The given state object.
""" """

4
src/kyupy/wave_sim.py

@ -332,8 +332,8 @@ class WaveSim:
self.lst_eat_valid = False self.lst_eat_valid = False
def wave(self, line, vector): def wave(self, line, vector):
"""Returns the desired waveform from the simulation state. Only valid, if simulator was # """Returns the desired waveform from the simulation state. Only valid, if simulator was
instanciated with ``keep_waveforms=True``.""" # instantiated with ``keep_waveforms=True``."""
if line < 0: if line < 0:
return [TMAX] return [TMAX]
mem, wcap, _ = self.sat[line] mem, wcap, _ = self.sat[line]

Loading…
Cancel
Save