Browse Source

Documenting circuit module

devel
Stefan Holst 4 years ago
parent
commit
5830608527
  1. 4
      README.rst
  2. 18
      docs/datastructures.rst
  3. 161
      src/kyupy/circuit.py
  4. 2
      src/kyupy/logic.py
  5. 45
      tests/test_circuit.py

4
README.rst

@ -16,8 +16,8 @@ Getting Started @@ -16,8 +16,8 @@ Getting Started
---------------
KyuPy is available in `PyPI <https://pypi.org/project/kyupy>`_.
It requires Python 3.6 or newer, `lark-parser <https://pypi.org/project/lark-parser>`_, and `numpy <https://pypi.org/project/numpy>`_.
Although optional, `numba <https://pypi.org/project/numba>`_ should be installed for best performance.
It requires Python 3.6 or newer, `lark-parser <https://pypi.org/project/lark-parser>`_, and `numpy`_.
Although optional, `numba`_ should be installed for best performance.
GPU/CUDA support in numba may `require some additional setup <https://numba.pydata.org/numba-doc/latest/cuda/index.html>`_.
If numba is not available, KyuPy will automatically fall back to slow, pure Python execution.

18
docs/datastructures.rst

@ -1,26 +1,30 @@ @@ -1,26 +1,30 @@
Core Data Structures
====================
Data Structures
===============
KyuPy provides two types of core data structures, one for gate-level circuits, and a few others for representing and storing logic data and signal values.
The data structures are designed to work together nicely with numpy arrays.
For example, all the nodes and connections in the circuit graph have numerical indices that can be used to access ndarrays with associated data.
For example, all the nodes and connections in the circuit graph have consecutive integer indices that can be used to access ndarrays with associated data.
Circuit graphs also define an ordering of inputs, outputs and other nodes to easily process test vector data and alike.
Module :mod:`kyupy.circuit`
---------------------------
Circuit Graph - :mod:`kyupy.circuit`
------------------------------------
.. automodule:: kyupy.circuit
.. autoclass:: kyupy.circuit.Node
:members:
.. autoclass:: kyupy.circuit.Line
:members:
.. autoclass:: kyupy.circuit.Circuit
:members:
Logic Values and Arrays
-----------------------
M-Valued Logic - :mod:`kyupy.logic`
-----------------------------------
.. automodule:: kyupy.logic
:members:
.. autoclass:: kyupy.logic.MVArray
:members:

161
src/kyupy/circuit.py

@ -14,6 +14,9 @@ class GrowingList(list): @@ -14,6 +14,9 @@ class GrowingList(list):
self.extend([None] * (index + 1 - len(self)))
super().__setitem__(index, value)
def free_index(self):
return next((i for i, x in enumerate(self) if x is None), len(self))
class IndexList(list):
def __delitem__(self, index):
@ -26,7 +29,7 @@ class IndexList(list): @@ -26,7 +29,7 @@ class IndexList(list):
class Node:
"""A named entity in a circuit (e.g. a gate, a standard cell,
"""A node is a named entity in a circuit (e.g. a gate, a standard cell,
a named signal, or a fan-out point) that is connected to other nodes via lines.
The constructor automatically adds the new node to the given circuit.
@ -46,17 +49,17 @@ class Node: @@ -46,17 +49,17 @@ class Node:
"""The name of the node.
Names must be unique among all forks and all cells in the circuit.
However, a fork (:code:`kind=='__fork__'`) and a cell with the same name may coexist.
However, a fork (:py:attr:`kind` is set to '__fork__') and a cell with the same name may coexist.
"""
self.kind = kind
"""A string describing the type of the node.
Common types are the names from a standard cell library or general gate names like 'AND' or 'NOR'.
If :code:`kind` is set to '__fork__', it receives special treatment.
If :py:attr:`kind` is set to '__fork__', it receives special treatment.
A `fork` describes a named signal or a fan-out point in the circuit and not a physical `cell` like a gate.
In the circuit, the namespaces of forks and cells are kept separate.
While :code:`name` must be unique among all forks and all cells, a fork can have the same name as a cell.
The :code:`index`, however, is unique among all nodes; a fork cannot have the same index as a cell.
While :py:attr:`name` must be unique among all forks and all cells, a fork can have the same name as a cell.
The :py:attr:`index`, however, is unique among all nodes; a fork cannot have the same index as a cell.
"""
self.index = len(circuit.nodes) - 1
"""A unique and consecutive integer index of the node within the circuit.
@ -95,57 +98,67 @@ class Node: @@ -95,57 +98,67 @@ class Node:
class Line:
"""A directional 1:1 connection between two nodes.
It always connects an output of a node (called `driver`) to an input of a node
(called `reader`) and has a circuit-unique index (`self.index`).
"""A line is a directional 1:1 connection between two nodes.
Furthermore, `self.driver_pin` and `self.reader_pin` are the
integer indices of the connected pins of the nodes. They always correspond
to the positions of the line in the connection lists of the nodes:
It always connects an output of one `driver` node to an input of one `reader` node.
If a signal fans out to multiple readers, a '__fork__' node needs to be added.
* `self.driver.outs[self.driver_pin] == self`
* `self.reader.ins[self.reader_pin] == self`
The constructor automatically adds the new line to the given circuit and inserts references into the connection
lists of connected nodes.
A Line always connects a single driver to a single reader. If a signal fans out to
multiple readers, a '__fork__' Node needs to be added.
When adding a line, input and output pins can either be specified explicitly
:code:`Line(circuit, (driver, 2), (reader, 0))`, or implicitly :code:`Line(circuit, driver, reader)`.
In the implicit case, the line will be connected to the first free pin of the node.
Use the explicit case only if connections to specific pins are required.
It may overwrite any previous line references in the connection list of the nodes.
"""
def __init__(self, circuit, driver, reader):
self.index = len(circuit.lines)
circuit.lines.append(self)
if type(driver) is Node:
self.driver = driver
self.driver_pin = len(driver.outs)
for pin, line in enumerate(driver.outs):
if line is None:
self.driver_pin = pin
break
else:
self.driver, self.driver_pin = driver
if type(reader) is Node:
self.reader = reader
self.reader_pin = len(reader.ins)
for pin, line in enumerate(reader.ins):
if line is None:
self.reader_pin = pin
break
else:
self.reader, self.reader_pin = reader
self.circuit = circuit
"""The :class:`Circuit` object the line is part of.
"""
self.circuit.lines.append(self)
self.index = len(self.circuit.lines) - 1
"""A unique and consecutive integer index of the line within the circuit.
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]`.
"""
if not isinstance(driver, tuple): driver = (driver, driver.outs.free_index())
self.driver = driver[0]
"""The :class:`Node` object that drives this 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:
:code:`self.driver.outs[self.driver_pin] == self`.
"""
if not isinstance(reader, tuple): reader = (reader, reader.ins.free_index())
self.reader = reader[0]
"""The :class:`Node` object that reads this 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:
:code:`self.reader.ins[self.reader_pin] == self`.
"""
self.driver.outs[self.driver_pin] = self
self.reader.ins[self.reader_pin] = self
def remove(self):
circuit = None
if self.driver is not None:
self.driver.outs[self.driver_pin] = None
circuit = self.driver.circuit
if self.reader is not None:
self.reader.ins[self.reader_pin] = None
circuit = self.reader.circuit
if circuit is not None:
del circuit.lines[self.index]
"""Removes the line from its circuit and its referencing nodes.
To keep the indices consecutive, the line with the highest index within the circuit
will be assigned the index of the removed line.
"""
if self.driver is not None: self.driver.outs[self.driver_pin] = None
if self.reader is not None: self.reader.ins[self.reader_pin] = None
if self.circuit is not None: del self.circuit.lines[self.index]
self.driver = None
self.reader = None
self.circuit = None
def __repr__(self):
return f'{self.index}'
@ -157,27 +170,53 @@ class Line: @@ -157,27 +170,53 @@ class Line:
class Circuit:
"""A Circuit is a container for interconnected nodes and lines.
All contained lines have unique indices, so have all contained nodes.
These indices can be used to store additional data about nodes or lines
by allocating an array `my_data` of length `len(self.nodes)` and then
accessing it by `my_data[n.index]`. The indices may change iff lines or
nodes are removed from the circuit.
It provides access to lines by index and to nodes by index and by name.
Nodes come in two flavors: `cells` and `forks` (see :py:attr:`Node.kind`).
The name spaces of cells and forks are kept separate.
Nodes come in two flavors (cells and forks, see `Node`). The names of
these nodes are kept unique within these two flavors.
The indices of nodes and lines are kept consecutive and unique.
Whenever lines or nodes are removed from the circuit, the indices of some other lines or nodes may change
to enforce consecutiveness.
A subset of nodes can be designated as primary input- or output-ports of the circuit.
This is done by adding them to the :py:attr:`interface` list.
"""
def __init__(self, name=None):
self.name = name
"""The name of the circuit.
"""
self.nodes = 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`.
"""
self.lines = 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`.
"""
self.interface = 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:`interface`.
The position of a node in the interface list corresponds to positions of logic values in test vectors.
The port direction is not stored explicitly.
Usually, nodes in the interface list without any lines in their :py:attr:`Node.ins` list are primary inputs,
and nodes without any lines in their :py:attr:`Node.outs` list are regarded as primary outputs.
"""
self.cells = {}
"""A dictionary to access cells by name.
"""
self.forks = {}
"""A dictionary to access forks by name.
"""
def get_or_add_fork(self, name):
return self.forks[name] if name in self.forks else Node(self, name)
def copy(self):
"""Returns a deep copy of the circuit.
"""
c = Circuit(self.name)
for node in self.nodes:
Node(c, node.name, node.kind)
@ -194,6 +233,8 @@ class Circuit: @@ -194,6 +233,8 @@ class Circuit:
return c
def dump(self):
"""Returns a string representation of the circuit and all its nodes.
"""
header = f'{self.name}({",".join([str(n.index) for n in self.interface])})\n'
return header + '\n'.join([str(n) for n in self.nodes])
@ -202,6 +243,11 @@ class Circuit: @@ -202,6 +243,11 @@ class Circuit:
return f'<Circuit{name} with {len(self.nodes)} nodes, {len(self.lines)} lines, {len(self.interface)} ports>'
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.
"""
visit_count = [0] * len(self.nodes)
queue = deque(n for n in self.nodes if len(n.ins) == 0 or 'DFF' in n.kind)
while len(queue) > 0:
@ -215,12 +261,19 @@ class Circuit: @@ -215,12 +261,19 @@ class Circuit:
yield n
def topological_line_order(self):
"""Generator function to iterate over all lines in topological order.
"""
for n in self.topological_order():
for line in n.outs:
if line is not None:
yield line
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.
"""
visit_count = [0] * len(self.nodes)
queue = deque(n for n in self.nodes if len(n.outs) == 0 or 'DFF' in n.kind)
while len(queue) > 0:
@ -233,6 +286,10 @@ class Circuit: @@ -233,6 +286,10 @@ class Circuit:
yield n
def fanin(self, origin_nodes):
"""Generator function to iterate over the fan-in cone of a given list of origin nodes.
Nodes are yielded in reversed topological order.
"""
marks = [False] * len(self.nodes)
for n in origin_nodes:
marks[n.index] = True

2
src/kyupy/logic.py

@ -33,6 +33,8 @@ from collections.abc import Iterable @@ -33,6 +33,8 @@ from collections.abc import Iterable
import numpy as np
ZERO = 0b000
"""Integer constant ``0b000`` for logic-0.
"""
UNASSIGNED = 0b001
UNKNOWN = 0b010
ONE = 0b011

45
tests/test_circuit.py

@ -1,6 +1,51 @@ @@ -1,6 +1,51 @@
from kyupy.circuit import Circuit, Node, Line
def test_lines():
c = Circuit()
n1 = Node(c, 'n1')
n2 = Node(c, 'n2')
line = Line(c, n1, n2)
assert line.driver == n1
assert line.reader == n2
assert line.driver_pin == 0
assert line.reader_pin == 0
assert n1.outs[0] == line
assert n2.ins[0] == line
line2 = Line(c, n1, (n2, 2))
assert line2.driver == n1
assert line2.reader == n2
assert line2.driver_pin == 1
assert line2.reader_pin == 2
assert n1.outs[0] == line
assert n1.outs[1] == line2
assert n2.ins[1] is None
assert n2.ins[2] == line2
line3 = Line(c, n1, n2)
assert line3.driver_pin == 2
assert line3.reader_pin == 1
assert n1.outs[2] == line3
assert n2.ins[1] == line3
assert n2.ins[2] == line2
assert len(c.lines) == 3
line3.remove()
assert len(c.lines) == 2
assert c.lines[0].index == 0
assert c.lines[1].index == 1
assert n1.outs[2] is None
assert n2.ins[1] is None
assert n2.ins[2] == line2
def test_circuit():
c = Circuit()
in1 = Node(c, 'in1', 'buf')

Loading…
Cancel
Save