diff --git a/README.rst b/README.rst index 7daa173..1291483 100644 --- a/README.rst +++ b/README.rst @@ -16,8 +16,8 @@ Getting Started --------------- KyuPy is available in `PyPI `_. -It requires Python 3.6 or newer, `lark-parser `_, and `numpy `_. -Although optional, `numba `_ should be installed for best performance. +It requires Python 3.6 or newer, `lark-parser `_, and `numpy`_. +Although optional, `numba`_ should be installed for best performance. GPU/CUDA support in numba may `require some additional setup `_. If numba is not available, KyuPy will automatically fall back to slow, pure Python execution. diff --git a/docs/datastructures.rst b/docs/datastructures.rst index a66385c..81e2c0d 100644 --- a/docs/datastructures.rst +++ b/docs/datastructures.rst @@ -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: diff --git a/src/kyupy/circuit.py b/src/kyupy/circuit.py index 605192c..84cc96c 100644 --- a/src/kyupy/circuit.py +++ b/src/kyupy/circuit.py @@ -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): 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: """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: 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: 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: 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: return f'' 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: 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: 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 diff --git a/src/kyupy/logic.py b/src/kyupy/logic.py index 6927788..32f686c 100644 --- a/src/kyupy/logic.py +++ b/src/kyupy/logic.py @@ -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 diff --git a/tests/test_circuit.py b/tests/test_circuit.py index d61e8aa..b5d6055 100644 --- a/tests/test_circuit.py +++ b/tests/test_circuit.py @@ -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')