Documenting circuit module
This commit is contained in:
1 parent
cff18e0915
commit
5830608527
5 files changed
+169
-61
No files matched your search
+2
-2
@@ -16,8 +16,8 @@ Getting Started
|
|||||||
---------------
|
---------------
|
||||||
|
|
||||||
KyuPy is available in `PyPI <https://pypi.org/project/kyupy>`_.
|
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>`_.
|
It requires Python 3.6 or newer, `lark-parser <https://pypi.org/project/lark-parser>`_, and `numpy`_.
|
||||||
Although optional, `numba <https://pypi.org/project/numba>`_ should be installed for best performance.
|
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>`_.
|
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.
|
If numba is not available, KyuPy will automatically fall back to slow, pure Python execution.
|
||||||
|
|
||||||
|
|||||||
+11
-7
@@ -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.
|
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.
|
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.
|
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
|
.. automodule:: kyupy.circuit
|
||||||
|
|
||||||
.. autoclass:: kyupy.circuit.Node
|
.. autoclass:: kyupy.circuit.Node
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: kyupy.circuit.Line
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoclass:: kyupy.circuit.Circuit
|
.. autoclass:: kyupy.circuit.Circuit
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Logic Values and Arrays
|
M-Valued Logic - :mod:`kyupy.logic`
|
||||||
-----------------------
|
-----------------------------------
|
||||||
|
|
||||||
.. automodule:: kyupy.logic
|
.. automodule:: kyupy.logic
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoclass:: kyupy.logic.MVArray
|
.. autoclass:: kyupy.logic.MVArray
|
||||||
:members:
|
:members:
|
||||||
|
|||||||
+109
-52
@@ -14,6 +14,9 @@ class GrowingList(list):
|
|||||||
self.extend([None] * (index + 1 - len(self)))
|
self.extend([None] * (index + 1 - len(self)))
|
||||||
super().__setitem__(index, value)
|
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):
|
class IndexList(list):
|
||||||
def __delitem__(self, index):
|
def __delitem__(self, index):
|
||||||
@@ -26,7 +29,7 @@ class IndexList(list):
|
|||||||
|
|
||||||
|
|
||||||
class Node:
|
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.
|
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.
|
The constructor automatically adds the new node to the given circuit.
|
||||||
@@ -46,17 +49,17 @@ class Node:
|
|||||||
"""The name of the node.
|
"""The name of the node.
|
||||||
|
|
||||||
Names must be unique among all forks and all cells in the circuit.
|
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
|
self.kind = kind
|
||||||
"""A string describing the type of the node.
|
"""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'.
|
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.
|
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.
|
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.
|
While :py:attr:`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.
|
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
|
self.index = len(circuit.nodes) - 1
|
||||||
"""A unique and consecutive integer index of the node within the circuit.
|
"""A unique and consecutive integer index of the node within the circuit.
|
||||||
@@ -95,57 +98,67 @@ class Node:
|
|||||||
|
|
||||||
|
|
||||||
class Line:
|
class Line:
|
||||||
"""A directional 1:1 connection between two nodes.
|
"""A line is a directional 1:1 connection between two nodes.
|
||||||
|
|
||||||
It always connects an output of a node (called `driver`) to an input of a node
|
It always connects an output of one `driver` node to an input of one `reader` node.
|
||||||
(called `reader`) and has a circuit-unique index (`self.index`).
|
If a signal fans out to multiple readers, a '__fork__' node needs to be added.
|
||||||
|
|
||||||
Furthermore, `self.driver_pin` and `self.reader_pin` are the
|
The constructor automatically adds the new line to the given circuit and inserts references into the connection
|
||||||
integer indices of the connected pins of the nodes. They always correspond
|
lists of connected nodes.
|
||||||
to the positions of the line in the connection lists of the nodes:
|
|
||||||
|
|
||||||
* `self.driver.outs[self.driver_pin] == self`
|
When adding a line, input and output pins can either be specified explicitly
|
||||||
* `self.reader.ins[self.reader_pin] == self`
|
: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.
|
||||||
A Line always connects a single driver to a single reader. If a signal fans out to
|
Use the explicit case only if connections to specific pins are required.
|
||||||
multiple readers, a '__fork__' Node needs to be added.
|
It may overwrite any previous line references in the connection list of the nodes.
|
||||||
"""
|
"""
|
||||||
def __init__(self, circuit, driver, reader):
|
def __init__(self, circuit, driver, reader):
|
||||||
self.index = len(circuit.lines)
|
self.circuit = circuit
|
||||||
circuit.lines.append(self)
|
"""The :class:`Circuit` object the line is part of.
|
||||||
if type(driver) is Node:
|
"""
|
||||||
self.driver = driver
|
self.circuit.lines.append(self)
|
||||||
self.driver_pin = len(driver.outs)
|
self.index = len(self.circuit.lines) - 1
|
||||||
for pin, line in enumerate(driver.outs):
|
"""A unique and consecutive integer index of the line within the circuit.
|
||||||
if line is None:
|
|
||||||
self.driver_pin = pin
|
It can be used to store additional data about the line :code:`l`
|
||||||
break
|
by allocating an array or list :code:`my_data` of length :code:`len(l.circuit.lines)` and
|
||||||
else:
|
accessing it by :code:`my_data[l.index]`.
|
||||||
self.driver, self.driver_pin = driver
|
"""
|
||||||
if type(reader) is Node:
|
if not isinstance(driver, tuple): driver = (driver, driver.outs.free_index())
|
||||||
self.reader = reader
|
self.driver = driver[0]
|
||||||
self.reader_pin = len(reader.ins)
|
"""The :class:`Node` object that drives this line.
|
||||||
for pin, line in enumerate(reader.ins):
|
"""
|
||||||
if line is None:
|
self.driver_pin = driver[1]
|
||||||
self.reader_pin = pin
|
"""The output pin position of the driver node this line is connected to.
|
||||||
break
|
|
||||||
else:
|
This is the position in the outs-list of the driving node this line referenced from:
|
||||||
self.reader, self.reader_pin = reader
|
: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.driver.outs[self.driver_pin] = self
|
||||||
self.reader.ins[self.reader_pin] = self
|
self.reader.ins[self.reader_pin] = self
|
||||||
|
|
||||||
def remove(self):
|
def remove(self):
|
||||||
circuit = None
|
"""Removes the line from its circuit and its referencing nodes.
|
||||||
if self.driver is not None:
|
|
||||||
self.driver.outs[self.driver_pin] = None
|
To keep the indices consecutive, the line with the highest index within the circuit
|
||||||
circuit = self.driver.circuit
|
will be assigned the index of the removed line.
|
||||||
if self.reader is not None:
|
"""
|
||||||
self.reader.ins[self.reader_pin] = None
|
if self.driver is not None: self.driver.outs[self.driver_pin] = None
|
||||||
circuit = self.reader.circuit
|
if self.reader is not None: self.reader.ins[self.reader_pin] = None
|
||||||
if circuit is not None:
|
if self.circuit is not None: del self.circuit.lines[self.index]
|
||||||
del circuit.lines[self.index]
|
|
||||||
self.driver = None
|
self.driver = None
|
||||||
self.reader = None
|
self.reader = None
|
||||||
|
self.circuit = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'{self.index}'
|
return f'{self.index}'
|
||||||
@@ -157,27 +170,53 @@ class Line:
|
|||||||
class Circuit:
|
class Circuit:
|
||||||
"""A Circuit is a container for interconnected nodes and lines.
|
"""A Circuit is a container for interconnected nodes and lines.
|
||||||
|
|
||||||
All contained lines have unique indices, so have all contained nodes.
|
It provides access to lines by index and to nodes by index and by name.
|
||||||
These indices can be used to store additional data about nodes or lines
|
Nodes come in two flavors: `cells` and `forks` (see :py:attr:`Node.kind`).
|
||||||
by allocating an array `my_data` of length `len(self.nodes)` and then
|
The name spaces of cells and forks are kept separate.
|
||||||
accessing it by `my_data[n.index]`. The indices may change iff lines or
|
|
||||||
nodes are removed from the circuit.
|
|
||||||
|
|
||||||
Nodes come in two flavors (cells and forks, see `Node`). The names of
|
The indices of nodes and lines are kept consecutive and unique.
|
||||||
these nodes are kept unique within these two flavors.
|
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):
|
def __init__(self, name=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
"""The name of the circuit.
|
||||||
|
"""
|
||||||
self.nodes = IndexList()
|
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()
|
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()
|
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 = {}
|
self.cells = {}
|
||||||
|
"""A dictionary to access cells by name.
|
||||||
|
"""
|
||||||
self.forks = {}
|
self.forks = {}
|
||||||
|
"""A dictionary to access forks by name.
|
||||||
|
"""
|
||||||
|
|
||||||
def get_or_add_fork(self, name):
|
def get_or_add_fork(self, name):
|
||||||
return self.forks[name] if name in self.forks else Node(self, name)
|
return self.forks[name] if name in self.forks else Node(self, name)
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
|
"""Returns a deep copy of the circuit.
|
||||||
|
"""
|
||||||
c = Circuit(self.name)
|
c = Circuit(self.name)
|
||||||
for node in self.nodes:
|
for node in self.nodes:
|
||||||
Node(c, node.name, node.kind)
|
Node(c, node.name, node.kind)
|
||||||
@@ -194,6 +233,8 @@ class Circuit:
|
|||||||
return c
|
return c
|
||||||
|
|
||||||
def dump(self):
|
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'
|
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])
|
return header + '\n'.join([str(n) for n in self.nodes])
|
||||||
|
|
||||||
@@ -202,6 +243,11 @@ class Circuit:
|
|||||||
return f'<Circuit{name} with {len(self.nodes)} nodes, {len(self.lines)} lines, {len(self.interface)} ports>'
|
return f'<Circuit{name} with {len(self.nodes)} nodes, {len(self.lines)} lines, {len(self.interface)} ports>'
|
||||||
|
|
||||||
def topological_order(self):
|
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)
|
visit_count = [0] * len(self.nodes)
|
||||||
queue = deque(n for n in self.nodes if len(n.ins) == 0 or 'DFF' in n.kind)
|
queue = deque(n for n in self.nodes if len(n.ins) == 0 or 'DFF' in n.kind)
|
||||||
while len(queue) > 0:
|
while len(queue) > 0:
|
||||||
@@ -215,12 +261,19 @@ class Circuit:
|
|||||||
yield n
|
yield n
|
||||||
|
|
||||||
def topological_line_order(self):
|
def topological_line_order(self):
|
||||||
|
"""Generator function to iterate over all lines in topological order.
|
||||||
|
"""
|
||||||
for n in self.topological_order():
|
for n in self.topological_order():
|
||||||
for line in n.outs:
|
for line in n.outs:
|
||||||
if line is not None:
|
if line is not None:
|
||||||
yield line
|
yield line
|
||||||
|
|
||||||
def reversed_topological_order(self):
|
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)
|
visit_count = [0] * len(self.nodes)
|
||||||
queue = deque(n for n in self.nodes if len(n.outs) == 0 or 'DFF' in n.kind)
|
queue = deque(n for n in self.nodes if len(n.outs) == 0 or 'DFF' in n.kind)
|
||||||
while len(queue) > 0:
|
while len(queue) > 0:
|
||||||
@@ -233,6 +286,10 @@ class Circuit:
|
|||||||
yield n
|
yield n
|
||||||
|
|
||||||
def fanin(self, origin_nodes):
|
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)
|
marks = [False] * len(self.nodes)
|
||||||
for n in origin_nodes:
|
for n in origin_nodes:
|
||||||
marks[n.index] = True
|
marks[n.index] = True
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ from collections.abc import Iterable
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
ZERO = 0b000
|
ZERO = 0b000
|
||||||
|
"""Integer constant ``0b000`` for logic-0.
|
||||||
|
"""
|
||||||
UNASSIGNED = 0b001
|
UNASSIGNED = 0b001
|
||||||
UNKNOWN = 0b010
|
UNKNOWN = 0b010
|
||||||
ONE = 0b011
|
ONE = 0b011
|
||||||
|
|||||||
@@ -1,6 +1,51 @@
|
|||||||
from kyupy.circuit import Circuit, Node, Line
|
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():
|
def test_circuit():
|
||||||
c = Circuit()
|
c = Circuit()
|
||||||
in1 = Node(c, 'in1', 'buf')
|
in1 = Node(c, 'in1', 'buf')
|
||||||
|
|||||||
Reference in new issue
Block a user