Compare commits
21
Commits
f863b70457
...
devel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea2ef1c3ec | ||
|
|
aabef17eea | ||
|
|
f0c10633bf | ||
|
|
ed9b5c6cd6 | ||
|
|
e19463d249 | ||
|
|
7a69ce9016 | ||
|
|
f5af7ec3d9 | ||
|
|
53309f9e59 | ||
|
|
c90bc89522 | ||
|
|
bda89dc1b0 | ||
|
|
449bf9a031 | ||
|
|
df463826ae | ||
|
|
aebdf9540d | ||
|
|
9dc58f8c40 | ||
|
|
e8a9025c56 | ||
|
|
8aa0f09254 | ||
|
|
4f7e605819 | ||
|
|
c1ea05555e | ||
|
|
e7b5567d87 | ||
|
|
3a2cfb7788 | ||
|
|
ca25942b27 |
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: "ubuntu-20.04"
|
||||
os: "ubuntu-24.04"
|
||||
tools:
|
||||
python: "3.12"
|
||||
jobs:
|
||||
|
||||
+3
-1
@@ -33,8 +33,10 @@ build-backend = "hatchling.build"
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"myst-parser>=4.0.1",
|
||||
"pytest>=9.0.1",
|
||||
"sphinx>=8.1.3",
|
||||
"sphinx-rtd-theme>=3.0.2",
|
||||
"pytest>=9.0.1",
|
||||
"ipython>=9.14.1",
|
||||
"jupyter>=1.1.1",
|
||||
]
|
||||
|
||||
+30
-18
@@ -12,6 +12,8 @@ import gzip
|
||||
|
||||
import numpy as np
|
||||
|
||||
type NestedNumericList = list['int|NestedNumericList']
|
||||
type NestedStrIntDict = dict[str,'int|NestedStrIntDict']
|
||||
|
||||
_pop_count_lut = np.asarray([bin(x).count('1') for x in range(256)])
|
||||
|
||||
@@ -111,9 +113,9 @@ class Timer:
|
||||
|
||||
|
||||
class Timers:
|
||||
def __init__(self, t={}): self.timers = defaultdict(Timer) | t
|
||||
def __init__(self, t={}): self.timers: dict[str, Timer] = defaultdict(Timer) | t
|
||||
def __getitem__(self, name): return self.timers[name]
|
||||
def __repr__(self): return '{' + ', '.join([f'{k}: {v}' for k, v in self.timers.items()]) + '}'
|
||||
def __repr__(self): return '{' + ', '.join([f'{k}: {v:.2f}' for k, v in self.dict().items()]) + '}'
|
||||
def __add__(self, t):
|
||||
tmr = Timers(self.timers)
|
||||
for k, v in t.timers.items(): tmr.timers[k] += v
|
||||
@@ -121,7 +123,7 @@ class Timers:
|
||||
def sum(self):
|
||||
return sum([v.s for v in self.timers.values()])
|
||||
def dict(self):
|
||||
return dict([(k, v.s) for k, v in self.timers.items()])
|
||||
return dict(sorted([(k, v.s) for k, v in self.timers.items()], key=lambda item: -item[1]))
|
||||
|
||||
|
||||
class Log:
|
||||
@@ -198,25 +200,35 @@ class Log:
|
||||
"""Log an error message."""
|
||||
self.log('E', message)
|
||||
|
||||
def progress(self):
|
||||
class Progress:
|
||||
def __init__(self, l): self.l = l
|
||||
def __enter__(self):
|
||||
self.start_time = time.perf_counter()
|
||||
self.lastlog_time = self.start_time
|
||||
self.log_interval = 5
|
||||
return self
|
||||
def update(self, completion: float, msg: str = ''):
|
||||
assert completion > 0 and completion <= 1
|
||||
current_time = time.perf_counter()
|
||||
if current_time > self.lastlog_time + self.log_interval:
|
||||
elapsed_time = current_time - self.start_time
|
||||
total_time = elapsed_time / completion
|
||||
rem_time = total_time - elapsed_time
|
||||
self.l.log(':', f'{msg} - {completion*100:.0f}% done {hr_time(elapsed_time)} elapsed {hr_time(rem_time)} remaining')
|
||||
self.log_interval = min(600, int(self.log_interval*1.5))
|
||||
self.lastlog_time = current_time
|
||||
def __exit__(self, *args): pass
|
||||
return Progress(self)
|
||||
|
||||
def range(self, *args):
|
||||
"""A generator that operates just like the ``range()`` built-in, and also occasionally logs the progress
|
||||
and compute time estimates."""
|
||||
elems = len(range(*args))
|
||||
start_time = time.perf_counter()
|
||||
lastlog_time = start_time
|
||||
log_interval = 5
|
||||
for elem, i in enumerate(range(*args)):
|
||||
yield i
|
||||
current_time = time.perf_counter()
|
||||
if current_time > lastlog_time + log_interval:
|
||||
done = (elem + 1) / elems
|
||||
elapsed_time = current_time - start_time
|
||||
total_time = elapsed_time / done
|
||||
rem_time = total_time - elapsed_time
|
||||
self.log(
|
||||
':', f'{done*100:.0f}% done {hr_time(elapsed_time)} elapsed {hr_time(rem_time)} remaining')
|
||||
log_interval = min(600, int(log_interval*1.5))
|
||||
lastlog_time = current_time
|
||||
with self.progress() as p:
|
||||
for elem, i in enumerate(range(*args)):
|
||||
yield i
|
||||
p.update((elem + 1) / elems)
|
||||
|
||||
|
||||
log = Log()
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""A parser and dumper for Atalanta's plain-text test pattern format (*.test).
|
||||
|
||||
A *.test file contains one test pattern per line in the form ``<index>: <bits>``,
|
||||
where ``<bits>`` is a string of ``'0'`` and ``'1'`` characters giving the values of
|
||||
the primary inputs (in circuit order). Lines starting with ``'*'`` are comments.
|
||||
|
||||
This parser also accepts files without ``<index>:`` and patterns elements in 8-valued
|
||||
logic such as ``'R'``, ``'F'``, ``'X'``.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
|
||||
from . import readtext, logic
|
||||
from .circuit import Circuit
|
||||
|
||||
_pattern_re = re.compile(r'^\s*(?:\d+:\s*)?([01HLRFX-]+)\s*$')
|
||||
|
||||
|
||||
class TestFile:
|
||||
"""Intermediate representation of a *.test pattern file.
|
||||
|
||||
:param text: The contents of a *.test file.
|
||||
"""
|
||||
|
||||
def __init__(self, text: str):
|
||||
patterns = []
|
||||
for line in text.splitlines():
|
||||
if (m := _pattern_re.match(line)) is not None:
|
||||
patterns.append(logic.mvarray(m.group(1)))
|
||||
|
||||
self.patterns = np.stack(patterns, axis=-1) if patterns \
|
||||
else np.zeros((0, 0), dtype=np.uint8)
|
||||
"""The parsed patterns as a mvarray (see :py:mod:`~kyupy.logic`).
|
||||
|
||||
The second-to-last axis goes along primary inputs, the last axis goes along patterns.
|
||||
"""
|
||||
|
||||
def tests(self, circuit: Circuit):
|
||||
"""Assembles and returns the test pattern set for the given circuit.
|
||||
|
||||
:param circuit: The circuit to assemble the patterns for. The patterns will follow the
|
||||
:py:attr:`~kyupy.circuit.Circuit.io_nodes` ordering of the circuit.
|
||||
:return: A logic array (see :py:mod:`~kyupy.logic`). The values for primary inputs are
|
||||
filled, all other values are left unassigned.
|
||||
"""
|
||||
pi_locs = [i for i, n in enumerate(circuit.io_nodes) if len(n.ins) == 0]
|
||||
tests = np.full((len(circuit.io_nodes), self.patterns.shape[-1]), logic.UNASSIGNED, dtype=np.uint8)
|
||||
tests[pi_locs] = self.patterns
|
||||
return tests
|
||||
|
||||
|
||||
def parse(text) -> TestFile:
|
||||
"""Parses the given ``text`` and returns a :class:`TestFile` object."""
|
||||
return TestFile(text)
|
||||
|
||||
|
||||
def load(file) -> TestFile:
|
||||
"""Parses the contents of ``file`` and returns a :class:`TestFile` object.
|
||||
|
||||
Files with `.gz`-suffix are decompressed on-the-fly.
|
||||
"""
|
||||
return parse(readtext(file))
|
||||
+24
-4
@@ -14,7 +14,7 @@ Since simulators only support at most four-input-gates, gates with more than fou
|
||||
from lark import Lark, Transformer
|
||||
|
||||
from .circuit import Circuit, Node, Line
|
||||
from . import readtext, batchrange
|
||||
from . import readtext, batchrange, log
|
||||
|
||||
def treeify(l, max_degree=4):
|
||||
if len(l) <= max_degree: return l
|
||||
@@ -26,12 +26,32 @@ class BenchTransformer(Transformer):
|
||||
def __init__(self, name):
|
||||
super().__init__()
|
||||
self.c = Circuit(name)
|
||||
self.inputs = set()
|
||||
self.outputs = set()
|
||||
|
||||
def start(self, _): return self.c
|
||||
|
||||
def parameters(self, args): return [self.c.get_or_add_fork(str(name)) for name in args if name is not None]
|
||||
|
||||
def interface(self, args): self.c.io_nodes.extend(args[0])
|
||||
def input(self, args):
|
||||
for input in args[0]:
|
||||
if input in self.outputs:
|
||||
new_input = self.c.get_or_add_fork(input.name + '~i')
|
||||
Line(self.c, new_input, input)
|
||||
log.warn(f'input-output passthrough, renaming input: {input.name} -> {new_input.name}')
|
||||
input = new_input
|
||||
self.inputs.add(input)
|
||||
self.c.io_nodes.append(input)
|
||||
|
||||
def output(self, args):
|
||||
for output in args[0]:
|
||||
if output in self.inputs:
|
||||
new_output = self.c.get_or_add_fork(output.name + '~o')
|
||||
Line(self.c, output, new_output)
|
||||
log.warn(f'input-output passthrough, renaming output: {output.name} -> {new_output.name}')
|
||||
output = new_output
|
||||
self.outputs.add(output)
|
||||
self.c.io_nodes.append(output)
|
||||
|
||||
def _cell_tree_inner(self, name, kind, inner_kind, drivers):
|
||||
cell = Node(self.c, name, f'{kind}{len(drivers)}')
|
||||
@@ -69,8 +89,8 @@ class BenchTransformer(Transformer):
|
||||
GRAMMAR = r"""
|
||||
start: (statement)*
|
||||
statement: input | output | assignment
|
||||
input: ("INPUT" | "input") parameters -> interface
|
||||
output: ("OUTPUT" | "output") parameters -> interface
|
||||
input: ("INPUT" | "input") parameters
|
||||
output: ("OUTPUT" | "output") parameters
|
||||
assignment: NAME "=" NAME parameters
|
||||
parameters: "(" [ NAME ( "," NAME )* ] ")"
|
||||
NAME: /[-_a-z0-9]+/i
|
||||
|
||||
+30
-33
@@ -14,10 +14,10 @@ from __future__ import annotations
|
||||
|
||||
from collections import deque, defaultdict
|
||||
import re
|
||||
from typing import Union, Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from . import NestedNumericList, NestedStrIntDict
|
||||
|
||||
class GrowingList[T](list[T]):
|
||||
def __setitem__(self, index, value):
|
||||
@@ -280,20 +280,19 @@ class Circuit:
|
||||
Use the :class:`Node` constructor and :py:attr:`Node.remove()` to add and remove nodes.
|
||||
"""
|
||||
|
||||
@property
|
||||
def s_nodes(self):
|
||||
def s_nodes(self, tlib: 'TechLib') -> list: # type: ignore
|
||||
"""A list of all primary I/Os as well as all flip-flops and latches in the circuit (in that order).
|
||||
|
||||
The s_nodes list defines the order of all ports and all sequential elements in the circuit.
|
||||
This list is constructed on-the-fly. If used in some inner toop, consider caching the list for better performance.
|
||||
"""
|
||||
return list(self.io_nodes) + [n for n in self.nodes if 'dff' in n.kind.lower()] + [n for n in self.nodes if 'latch' in n.kind.lower()]
|
||||
return list(self.io_nodes) + [n for n in self.nodes if tlib.is_dff(n.kind)] + [n for n in self.nodes if tlib.is_latch(n.kind)]
|
||||
|
||||
def io_locs(self, prefix):
|
||||
"""Returns the indices of primary I/Os that start with given name prefix.
|
||||
"""Returns a list of indices of primary I/Os that start with given name prefix.
|
||||
|
||||
The returned values are used to index into the :py:attr:`io_nodes` array.
|
||||
If only one I/O cell matches the given prefix, a single integer is returned.
|
||||
If only one I/O cell matches the given prefix, a list with a single integer is returned.
|
||||
If a bus matches the given prefix, a sorted list of indices is returned.
|
||||
Busses are identified by integers in the cell names following the given prefix.
|
||||
Lists for bus indices are sorted from LSB (e.g. :code:`data[0]`) to MSB (e.g. :code:`data[31]`).
|
||||
@@ -303,16 +302,16 @@ class Circuit:
|
||||
"""
|
||||
return self._locs(prefix, list(self.io_nodes))
|
||||
|
||||
def s_locs(self, prefix):
|
||||
def s_locs(self, prefix, tlib: 'TechLib'): # type: ignore
|
||||
"""Returns the indices of I/Os and sequential elements that start with given name prefix.
|
||||
|
||||
The returned values are used to index into the :py:attr:`s_nodes` list.
|
||||
It works the same as :py:attr:`io_locs`. See there for more details.
|
||||
"""
|
||||
return self._locs(prefix, self.s_nodes)
|
||||
return self._locs(prefix, self.s_nodes(tlib))
|
||||
|
||||
def _locs(self, prefix, nodes:list[Node]) -> Node|list[Any]: # can return list[list[...]]
|
||||
d_top = dict()
|
||||
def _locs(self, prefix, nodes:list[Node]) -> NestedNumericList: # can return list[list[...]]
|
||||
d_top: NestedStrIntDict = dict()
|
||||
for i, n in enumerate(nodes):
|
||||
if m := re.match(fr'({re.escape(prefix)}.*?)((?:[\d_\[\]])*$)', n.name):
|
||||
path = [m[1]] + [int(v) for v in re.split(r'[_\[\]]+', m[2]) if len(v) > 0]
|
||||
@@ -320,16 +319,16 @@ class Circuit:
|
||||
for j in path[:-1]:
|
||||
d[j] = d.get(j, dict())
|
||||
d = d[j]
|
||||
assert isinstance(d, dict)
|
||||
d[path[-1]] = i
|
||||
|
||||
# sort recursively for multi-dimensional lists.
|
||||
def sorted_values(d): return [sorted_values(v) for k, v in sorted(d.items())] if isinstance(d, dict) else d
|
||||
def sorted_values(d) -> NestedNumericList: return [sorted_values(v) for k, v in sorted(d.items())] if isinstance(d, dict) else d
|
||||
l = sorted_values(d_top)
|
||||
while isinstance(l, list) and len(l) == 1: l = l[0]
|
||||
return l #None if isinstance(l, list) and len(l) == 0 else l
|
||||
while isinstance(l, list) and len(l) == 1 and isinstance(l[0], list): l = l[0]
|
||||
return l
|
||||
|
||||
@property
|
||||
def stats(self):
|
||||
def stats(self, tlib: 'TechLib'): # type: ignore
|
||||
"""A dictionary with the counts of all different elements in the circuit.
|
||||
|
||||
The dictionary contains the number of all different kinds of nodes, the number
|
||||
@@ -347,9 +346,9 @@ class Circuit:
|
||||
stats['__line__'] = len(self.lines)
|
||||
for n in self.cells.values():
|
||||
stats[n.kind] += 1
|
||||
if 'dff' in n.kind.lower(): stats['__dff__'] += 1
|
||||
elif 'latch' in n.kind.lower(): stats['__latch__'] += 1
|
||||
elif 'put' not in n.kind.lower(): stats['__comb__'] += 1 # no input or output
|
||||
if tlib.is_dff(n.kind): stats['__dff__'] += 1
|
||||
elif tlib.is_latch(n.kind): stats['__latch__'] += 1
|
||||
elif 'put' not in n.kind.lower(): stats['__comb__'] += 1 # don't count input or output towards comb
|
||||
stats['__seq__'] = stats['__dff__'] + stats['__latch__']
|
||||
return dict(stats)
|
||||
|
||||
@@ -581,24 +580,23 @@ class Circuit:
|
||||
if line is not None:
|
||||
yield line
|
||||
|
||||
def reversed_topological_order(self):
|
||||
def reversed_topological_order(self, tlib: 'TechLib'): # type: ignore
|
||||
"""Generator function to iterate over all nodes in reversed topological order.
|
||||
|
||||
Nodes without output lines and nodes whose :py:attr:`Node.kind` contains the
|
||||
substrings 'dff' or 'latch' are yielded first.
|
||||
Nodes without output lines and sequential nodes (flip-flops, latches) 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.lower() or 'latch' in n.kind.lower())
|
||||
queue = deque(n for n in self.nodes if len(n.outs) == 0 or tlib.is_dff(n.kind) or tlib.is_latch(n.kind))
|
||||
while len(queue) > 0:
|
||||
n = queue.popleft()
|
||||
for line in n.ins:
|
||||
pred = line.driver
|
||||
visit_count[pred] += 1
|
||||
if visit_count[pred] == len(pred.outs) and 'dff' not in pred.kind.lower() and 'latch' not in pred.kind.lower():
|
||||
if visit_count[pred] == len(pred.outs) and not tlib.is_dff(pred.kind) and not tlib.is_latch(pred.kind):
|
||||
queue.append(pred)
|
||||
yield n
|
||||
|
||||
def fanin(self, origin_nodes):
|
||||
def fanin(self, origin_nodes, tlib: 'TechLib'): # type: ignore
|
||||
"""Generator function to iterate over the fan-in cone of a given list of origin nodes.
|
||||
|
||||
Nodes are yielded in reversed topological order.
|
||||
@@ -606,11 +604,10 @@ class Circuit:
|
||||
marks = [False] * len(self.nodes)
|
||||
for n in origin_nodes:
|
||||
marks[n] = True
|
||||
for n in self.reversed_topological_order():
|
||||
for n in self.reversed_topological_order(tlib):
|
||||
if not marks[n]:
|
||||
for line in n.outs:
|
||||
if line is not None:
|
||||
marks[n] |= marks[line.reader]
|
||||
for line in n.outs.without_nones():
|
||||
marks[n] |= marks[line.reader]
|
||||
if marks[n]:
|
||||
yield n
|
||||
|
||||
@@ -633,8 +630,8 @@ class Circuit:
|
||||
queue.append(succ)
|
||||
yield n
|
||||
|
||||
def fanout_free_regions(self):
|
||||
for stem in self.reversed_topological_order():
|
||||
def fanout_free_regions(self, tlib: 'TechLib'): # type: ignore
|
||||
for stem in self.reversed_topological_order(tlib):
|
||||
if len(stem.outs) == 1 and 'dff' not in stem.kind.lower(): continue
|
||||
region = []
|
||||
if 'dff' in stem.kind.lower():
|
||||
@@ -654,11 +651,11 @@ class Circuit:
|
||||
region.append(n)
|
||||
yield stem, region
|
||||
|
||||
def dot(self, format='svg', graph_attr={}, line_labels={}):
|
||||
def dot(self, tlib: 'TechLib', format='svg', graph_attr={}, line_labels={}):
|
||||
from graphviz import Digraph
|
||||
dot = Digraph(format=format, graph_attr={'rankdir': 'LR', 'splines': 'true', 'size': '10', 'ranksep': '0.1'} | graph_attr)
|
||||
|
||||
s_dict = dict((n, i) for i, n in enumerate(self.s_nodes))
|
||||
s_dict = dict((n, i) for i, n in enumerate(self.s_nodes(tlib)))
|
||||
node_level = np.zeros(len(self.nodes), dtype=np.uint32)
|
||||
level_nodes = defaultdict(list)
|
||||
for n, lv in self.topological_order_with_level():
|
||||
@@ -681,7 +678,7 @@ class Circuit:
|
||||
reader = f'{l.reader.index}:i{l.reader_pin}' if len(l.reader.ins)>1 else f'{l.reader.index}'
|
||||
label = str(line_labels.get(l, l.index))
|
||||
if node_level[l.driver] == node_level[l.reader]:
|
||||
s.node(f'_{l.index}_')
|
||||
dot.node(f'_{l.index}_')
|
||||
dot.edge(driver, f'_{l.index}_', style='dotted', label=label)
|
||||
dot.edge(f'_{l.index}_', reader, style='dotted', label=label)
|
||||
elif node_level[l.driver] > node_level[l.reader]:
|
||||
|
||||
+19
-10
@@ -6,7 +6,7 @@ as an intermediate representation (:class:`DefFile` object).
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from lark import Lark, Transformer, Tree
|
||||
from lark import Lark, Transformer
|
||||
|
||||
from kyupy import readtext
|
||||
|
||||
@@ -85,6 +85,8 @@ class DefFile:
|
||||
self.pins = {}
|
||||
self.specialnets = {}
|
||||
self.nets = {}
|
||||
self.design = ""
|
||||
self.diearea = 0
|
||||
|
||||
|
||||
class DefTransformer(Transformer):
|
||||
@@ -140,9 +142,11 @@ class DefTransformer(Transformer):
|
||||
def comp_stmt(self, args):
|
||||
name = args[0].value
|
||||
kind = args[1].value
|
||||
point = args[2]
|
||||
orientation = args[3].value
|
||||
self.def_file.components[name] = (kind, point, orientation)
|
||||
for aidx in range(2, len(args)):
|
||||
if args[aidx] in ('PLACED', 'FIXED'):
|
||||
point = args[aidx+1]
|
||||
orientation = args[aidx+2].value
|
||||
self.def_file.components[name] = (kind, point, orientation)
|
||||
|
||||
def pins_stmt(self, args):
|
||||
pin = DefPin(args[0].value)
|
||||
@@ -207,6 +211,7 @@ GRAMMAR = r"""
|
||||
| /DIEAREA/ point+ ";"
|
||||
| /ROW/ ID ID NUMBER NUMBER ID do_step ";"
|
||||
| /TRACKS/ /[XY]/ NUMBER "DO" NUMBER "STEP" NUMBER "LAYER" ID ";"
|
||||
| /GCELLGRID/ /[XY]/ NUMBER "DO" NUMBER "STEP" NUMBER ";"
|
||||
| propdef | vias | nondef | comp | pins | pinprop | spnets | nets
|
||||
|
||||
propdef: "PROPERTYDEFINITIONS" propdef_stmt* "END" "PROPERTYDEFINITIONS"
|
||||
@@ -228,7 +233,9 @@ GRAMMAR = r"""
|
||||
| "+" /VIA/ ID )* ";"
|
||||
|
||||
comp: "COMPONENTS" NUMBER ";" comp_stmt* "END" "COMPONENTS"
|
||||
comp_stmt: "-" ID ID "+" "PLACED" point ID ";"
|
||||
comp_stmt: "-" ID ID ( "+" /PLACED/ point ID
|
||||
| "+" /FIXED/ point ID
|
||||
| "+" /SOURCE/ ID )* ";"
|
||||
|
||||
pins: "PINS" NUMBER ";" pins_stmt* "END" "PINS"
|
||||
pins_stmt: "-" ID pins_opt* ";"
|
||||
@@ -239,6 +246,8 @@ GRAMMAR = r"""
|
||||
| "+" /PORT/
|
||||
| "+" /LAYER/ ID point point
|
||||
| "+" /PLACED/ point ID
|
||||
| "+" /FIXED/ point ID
|
||||
|
||||
|
||||
pinprop: "PINPROPERTIES" NUMBER ";" pinprop_stmt* "END" "PINPROPERTIES"
|
||||
pinprop_stmt: "-" "PIN" ID "+" "PROPERTY" ID STRING ";"
|
||||
@@ -266,10 +275,10 @@ GRAMMAR = r"""
|
||||
wire: ID wire_opt points
|
||||
wire_opt: ( "TAPER" | "TAPERRULE" ID )? ("STYLE" ID)?
|
||||
|
||||
points: point ( point | points_via )+
|
||||
points_via: ID ORIENTATION?
|
||||
points: point ( point | points_via_or_rect )+
|
||||
points_via_or_rect: ID ( ORIENTATION? | "(" (NUMBER|SIGNED_NUMBER|/\*/) (NUMBER|SIGNED_NUMBER|/\*/) (NUMBER|SIGNED_NUMBER|/\*/) (NUMBER|SIGNED_NUMBER|/\*/) ")" )
|
||||
|
||||
point: "(" (NUMBER|/\*/) (NUMBER|/\*/) NUMBER? ")"
|
||||
point: "(" (NUMBER|SIGNED_NUMBER|/\*/) (NUMBER|SIGNED_NUMBER|/\*/) NUMBER? ")"
|
||||
|
||||
do_step: "DO" NUMBER "BY" NUMBER "STEP" (NUMBER|SIGNED_NUMBER) (NUMBER|SIGNED_NUMBER)
|
||||
|
||||
@@ -284,9 +293,9 @@ GRAMMAR = r"""
|
||||
"""
|
||||
|
||||
|
||||
def parse(text):
|
||||
def parse(text) -> DefFile:
|
||||
"""Parses the given ``text`` and returns a :class:`DefFile` object."""
|
||||
return Lark(GRAMMAR, parser="lalr", transformer=DefTransformer()).parse(text)
|
||||
return Lark(GRAMMAR, parser="lalr", transformer=DefTransformer()).parse(text) # type: ignore
|
||||
|
||||
|
||||
def load(file):
|
||||
|
||||
+6
-5
@@ -49,7 +49,7 @@ from collections.abc import Iterable
|
||||
import numpy as np
|
||||
from numpy.typing import DTypeLike
|
||||
|
||||
from . import numba, hr_bytes
|
||||
from . import numba, hr_bytes, NestedNumericList
|
||||
|
||||
|
||||
ZERO = 0b000
|
||||
@@ -104,7 +104,7 @@ _mv_merge = np.array([[0,0,0,1,1,1,1,1], # 0
|
||||
], dtype=np.uint8)
|
||||
|
||||
|
||||
def interpret(value):
|
||||
def interpret(value) -> int|NestedNumericList:
|
||||
"""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.
|
||||
@@ -123,11 +123,12 @@ def interpret(value):
|
||||
return UNKNOWN
|
||||
|
||||
|
||||
def mvarray(*a):
|
||||
def mvarray(*a) -> np.ndarray[tuple[int]|tuple[int,int],np.dtype[np.uint8]]:
|
||||
"""Converts (lists of) Boolean values or strings into a multi-valued array.
|
||||
|
||||
The given values are interpreted and the axes are arranged as per KyuPy's convention.
|
||||
Use this function to convert strings into multi-valued arrays.
|
||||
Iterable over characters, Booleans, Integers (0,1), None are interpreted as a single pattern.
|
||||
Multi-character strings are Iterables over characters and are interpreted as a single pattern.
|
||||
Lists of said patterns form a pattern set as ndarray with the axes are arranged as per KyuPy's convention.
|
||||
"""
|
||||
mva = np.array(interpret(a), dtype=np.uint8)
|
||||
if mva.ndim < 2: return mva
|
||||
|
||||
+48
-12
@@ -1,12 +1,24 @@
|
||||
"""A high-throughput combinational logic simulator.
|
||||
"""High-throughput combinational logic simulators.
|
||||
|
||||
The class :py:class:`~kyupy.logic_sim.LogicSim` performs parallel simulations of the combinational part of a circuit.
|
||||
The logic operations are performed bit-parallel on packed numpy arrays (see bit-parallel (bp) array description in :py:mod:`~kyupy.logic`).
|
||||
Simple sequential circuits can be simulated by repeated assignments and propagations.
|
||||
However, this simulator ignores the clock network and simply assumes that all state-elements are clocked all the time.
|
||||
These simulators take batches of input assignments to a logic circuit and compute batches of output values.
|
||||
Each batch is simulated in data-parallel fashion using packed numpy arrays (see bit-parallel (bp) array description in :py:mod:`~kyupy.logic`) for each signal in the circuit.
|
||||
All simulations within a batch are mutually independent.
|
||||
Inner simulation loops (that iterate over the topologically sorted circuit model) are just-in-time (JIT) compiled using numba.
|
||||
JIT compiling triggered by the first simulation may take several seconds.
|
||||
If numba is not available, inner loop is executed in the python interpreter and is significantly slower.
|
||||
|
||||
Circuits passed to these simulators must contain only cells defined in the :py:class:`~kyupy.techlib.KYUPY` cell library.
|
||||
Other circuits must be mapped to that library first using :py:method:`~kyupy.circuit.resolve_tlib_cells()`.
|
||||
|
||||
Sequential circuits are supported with some caveats.
|
||||
The simulation model is built by stripping out all flip-flops, clock, and asynchronous set/reset logic.
|
||||
Values stored in the flip-flops become part of the input assignments and output values (pseudo-primary I/O).
|
||||
Only the combinational portion is actually simulated.
|
||||
Simulation results remain correct under the assumption that all flip-clops are clocked all the time and set/reset logic is passive.
|
||||
"""
|
||||
|
||||
import math
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -17,6 +29,10 @@ from .circuit import Circuit, Line
|
||||
class LogicSim(sim.SimOps):
|
||||
"""A bit-parallel naïve combinational simulator for 2-, 4-, or 8-valued logic.
|
||||
|
||||
.. deprecated::
|
||||
Use the specialized :py:class:`LogicSim2V`, :py:class:`LogicSim4V`, or :py:class:`LogicSim6V`
|
||||
simulators instead.
|
||||
|
||||
:param circuit: The circuit to simulate.
|
||||
:param sims: The number of parallel logic simulations to perform.
|
||||
:param m: The arity of the logic, must be 2, 4, or 8.
|
||||
@@ -24,6 +40,9 @@ class LogicSim(sim.SimOps):
|
||||
:param strip_forks: If True, forks are not included in the simulation model to save memory and simulation time.
|
||||
"""
|
||||
def __init__(self, circuit: Circuit, sims: int = 8, m: int = 8, c_reuse: bool = False, strip_forks: bool = False):
|
||||
warnings.warn(
|
||||
'LogicSim is deprecated; use LogicSim2V, LogicSim4V, or LogicSim6V instead.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
assert m in [2, 4, 8]
|
||||
super().__init__(circuit, c_reuse=c_reuse, strip_forks=strip_forks)
|
||||
self.m = m
|
||||
@@ -32,6 +51,7 @@ class LogicSim(sim.SimOps):
|
||||
nbytes = cdiv(sims, 8)
|
||||
|
||||
self.c = np.zeros((self.c_len, self.mdim, nbytes), dtype=np.uint8)
|
||||
self.c_dirty = np.full(self.c_len, 1, dtype=np.uint8)
|
||||
self.s = np.zeros((2, self.s_len, 3, nbytes), dtype=np.uint8)
|
||||
"""Logic values of the sequential elements (flip-flops) and ports.
|
||||
|
||||
@@ -78,7 +98,7 @@ class LogicSim(sim.SimOps):
|
||||
t1 = self.c_locs[self.tmp2_idx]
|
||||
if self.m == 2:
|
||||
if inject_cb is None:
|
||||
c_prop_2v_cpu(self.ops, self.c_locs, self.c, int(fault_line), fault_mask, int(fault_model))
|
||||
c_prop_2v_cpu(self.ops, self.c_locs, self.c, self.c_dirty, int(fault_line), fault_mask, int(fault_model))
|
||||
else:
|
||||
for op, o0l, i0l, i1l, i2l, i3l in self.ops[:,:6]:
|
||||
o0, i0, i1, i2, i3 = [self.c_locs[x] for x in (o0l, i0l, i1l, i2l, i3l)]
|
||||
@@ -337,6 +357,19 @@ class LogicSim2V(sim.SimOps):
|
||||
Storage locations are indirectly addressed.
|
||||
Data for line `l` is in `self.c[self.c_locs[l]]`.
|
||||
"""
|
||||
self.c_dirty = np.full(self.c_len, 1, dtype=np.uint8)
|
||||
"""Marker for logic values that have changed recently for use in incremental simulation.
|
||||
|
||||
A node n will be evaluated by `c_prop()` if and only if at least one of its inputs is marked dirty (`c_dirty[c_locs[n.ins[i]]] == 1`).
|
||||
If a node is evaluated, its output is marked dirty.
|
||||
By default, this array is all-1, therefore every node is evaluated every time by `c_prop()`.
|
||||
Enable incremental simulation by setting this array to all-0, change signal values in `c` and set `c_dirty` to 1 for all changed signals.
|
||||
Next call to `c_prop()` will only evaluate nodes that read from the changed signals.
|
||||
To restore the original simulation state, revert the signal value changes in `c`, and call `c_prop()` again.
|
||||
Caveats:
|
||||
- Injected changes that are located downstream of other changes will have no effect.
|
||||
- Only works with `c_reuse=False` (default).
|
||||
"""
|
||||
self.s_assign = np.zeros((self.s_len, self.sims), dtype=np.uint8)
|
||||
"""Logic values assigned to the ports and flip-flops of the circuit.
|
||||
|
||||
@@ -371,7 +404,7 @@ class LogicSim2V(sim.SimOps):
|
||||
fault_mask2 = np.full(self.c.shape[-1], 0, dtype=np.uint8)
|
||||
fault_mask2[:len(fault_mask)] = fault_mask
|
||||
fault_mask = fault_mask2
|
||||
c_prop_2v_cpu(self.ops, self.c_locs, self.c, int(fault_line), fault_mask, int(fault_model))
|
||||
c_prop_2v_cpu(self.ops, self.c_locs, self.c, self.c_dirty, int(fault_line), fault_mask, int(fault_model))
|
||||
|
||||
def c_to_s(self):
|
||||
"""Captures the results of the combinational portion into ``self.s_result``.
|
||||
@@ -405,9 +438,11 @@ class LogicSim2V(sim.SimOps):
|
||||
|
||||
|
||||
@numba.njit
|
||||
def c_prop_2v_cpu(ops, c_locs, c, fault_line, fault_mask, fault_model):
|
||||
def c_prop_2v_cpu(ops, c_locs, c, c_dirty, fault_line, fault_mask, fault_model):
|
||||
for op, o0l, i0l, i1l, i2l, i3l in ops[:,:6]:
|
||||
o0, i0, i1, i2, i3 = [c_locs[x] for x in (o0l, i0l, i1l, i2l, i3l)]
|
||||
if fault_line < 0 or o0l != fault_line: # fault injection forces node evaluation
|
||||
if not (c_dirty[i0] | c_dirty[i1] | c_dirty[i2] | c_dirty[i3]): continue
|
||||
if op == sim.BUF1: c[o0]=c[i0]
|
||||
elif op == sim.INV1: c[o0] = ~c[i0]
|
||||
elif op == sim.AND2: c[o0] = c[i0] & c[i1]
|
||||
@@ -447,8 +482,9 @@ def c_prop_2v_cpu(ops, c_locs, c, fault_line, fault_mask, fault_model):
|
||||
c[o0] = c[o0] & ~fault_mask
|
||||
elif fault_model == 1:
|
||||
c[o0] = c[o0] | fault_mask
|
||||
else:
|
||||
elif fault_model == 2:
|
||||
c[o0] = c[o0] ^ fault_mask
|
||||
c_dirty[o0] = 1
|
||||
|
||||
|
||||
class LogicSim4V(sim.SimOps):
|
||||
@@ -473,7 +509,7 @@ class LogicSim4V(sim.SimOps):
|
||||
Storage locations are indirectly addressed.
|
||||
Data for line `l` is in `self.c[self.c_locs[l]]`.
|
||||
"""
|
||||
self.s_assign = np.zeros((self.s_len, self.sims), dtype=np.uint8)
|
||||
self.s_assign = np.full((self.s_len, self.sims), logic.UNASSIGNED, dtype=np.uint8)
|
||||
"""Logic values assigned to the ports and flip-flops of the circuit.
|
||||
The simulator reads (P)PI values from here.
|
||||
Values assigned to PO positions are ignored.
|
||||
@@ -481,7 +517,7 @@ class LogicSim4V(sim.SimOps):
|
||||
First index is the port position (defined by `self.circuit.s_nodes`), second
|
||||
index is the pattern index (0 ... `self.sims-1`).
|
||||
"""
|
||||
self.s_result = np.zeros((self.s_len, self.sims), dtype=np.uint8)
|
||||
self.s_result = np.full((self.s_len, self.sims), logic.UNASSIGNED, dtype=np.uint8)
|
||||
"""Logic values at the ports and flip-flops of the circuit after simulation.
|
||||
The simulator writes (P)PO values here.
|
||||
Values assigned to PI positions are ignored.
|
||||
@@ -739,7 +775,7 @@ class LogicSim6V(sim.SimOps):
|
||||
nbytes = cdiv(sims, 8)
|
||||
|
||||
self.c = np.zeros((self.c_len, 3, nbytes), dtype=np.uint8)
|
||||
self.s = np.zeros((2, self.s_len, self.sims), dtype=np.uint8)
|
||||
self.s = np.full((2, self.s_len, self.sims), logic.UNASSIGNED, dtype=np.uint8)
|
||||
"""Logic values of the sequential elements (flip-flops) and ports.
|
||||
|
||||
It is a pair of arrays in mv storage format:
|
||||
|
||||
+31
-14
@@ -6,6 +6,7 @@ import numpy as np
|
||||
|
||||
from . import log
|
||||
from .circuit import Circuit
|
||||
from .techlib import KYUPY
|
||||
|
||||
BUF1 = np.uint16(0b1010_1010_1010_1010)
|
||||
INV1 = ~BUF1
|
||||
@@ -159,7 +160,7 @@ class SimOps:
|
||||
"""A static scheduler that translates a Circuit into a topologically sorted list of basic logic operations (self.ops) and
|
||||
a memory mapping (self.c_locs, self.c_caps) for use in simulators.
|
||||
|
||||
:param circuit: The circuit to create a schedule for.
|
||||
:param circuit: The circuit to create a schedule for. Must contain cells from KYUPY techlib only.
|
||||
:param strip_forks: If enabled, the scheduler will not include fork nodes to safe simulation time.
|
||||
Stripping forks will cause interconnect delay annotations of lines read by fork nodes to be ignored.
|
||||
:param c_reuse: If enabled, memory of intermediate signal waveforms will be re-used. This greatly reduces
|
||||
@@ -167,7 +168,7 @@ class SimOps:
|
||||
"""
|
||||
def __init__(self, circuit: Circuit, c_caps=1, c_caps_min=1, a_ctrl=None, c_reuse=False, strip_forks=False):
|
||||
self.circuit = circuit
|
||||
self.s_len = len(circuit.s_nodes)
|
||||
self.s_len = len(circuit.s_nodes(KYUPY))
|
||||
|
||||
if isinstance(c_caps, int):
|
||||
c_caps = [c_caps] * (len(circuit.lines)+3)
|
||||
@@ -187,11 +188,11 @@ class SimOps:
|
||||
# ALAP-toposort the circuit into self.ops
|
||||
levels = []
|
||||
|
||||
ppio2idx = dict((n, i) for i, n in enumerate(circuit.s_nodes))
|
||||
root_nodes = set([n for n in circuit.s_nodes if len(n.ins) > 0] + [n for n in circuit.nodes if len(n.outs) == 0]) # start from POs, PPOs, and any dangling nodes
|
||||
ppio2idx = dict((n, i) for i, n in enumerate(circuit.s_nodes(KYUPY)))
|
||||
root_nodes = set([n for n in circuit.nodes if len(n.outs) == 0 or KYUPY.is_dff(n.kind)]) # start from POs (that are unused by circuit itself), PPOs, and any dangling nodes
|
||||
readers = np.array([1 if l.reader in root_nodes else len(l.reader.outs) for l in circuit.lines], dtype=np.int32) # for ref-counting forks
|
||||
|
||||
level_lines = [n.ins[0] for n in root_nodes if len(n.ins) > 0 ]
|
||||
level_lines = [n.ins[0] for n in sorted(root_nodes, key=lambda n: n.index) if len(n.ins) > 0 ]
|
||||
# FIXME: Should probably instanciate buffers for PPOs and attach DFF clocks
|
||||
|
||||
while len(level_lines) > 0: # traverse the circuit level-wise back towards (P)PIs
|
||||
@@ -200,11 +201,15 @@ class SimOps:
|
||||
|
||||
for l in level_lines:
|
||||
n = l.driver
|
||||
if len(n.ins) > 4:
|
||||
log.warn(f'too many input pins: {n}')
|
||||
assert len(n.ins) <= 4, f'Too many input pins for node {n}. Map circuit to KYUPY techlib first using resolve_tlib_cells().'
|
||||
in_idxs = [n.ins[x].index if len(n.ins) > x and n.ins[x] is not None else self.zero_idx for x in [0,1,2,3]]
|
||||
if n in ppio2idx:
|
||||
in_idxs[0] = self.ppi_offset + ppio2idx[n]
|
||||
in_idxs[1] = self.zero_idx
|
||||
if 'dff' in n.kind.lower() or in_idxs[0] == self.zero_idx:
|
||||
in_idxs[0] = self.ppi_offset + ppio2idx[n]
|
||||
else: # this is a PO that is also used within the circuit. Treat as normal fork with ref counting. TODO: check strip_forks=True case for this.
|
||||
readers[n.ins[0]] -= 1
|
||||
if readers[n.ins[0]] == 0: prev_level_lines.append(n.ins[0])
|
||||
if l.driver_pin == 1 and 'dff' in n.kind.lower(): # second output of DFF is inverted
|
||||
level_ops.append((INV1, l.index, *in_idxs, *a_ctrl[l]))
|
||||
else:
|
||||
@@ -225,10 +230,8 @@ class SimOps:
|
||||
if in_idxs[2] == self.zero_idx:
|
||||
sp = prims[2]
|
||||
break
|
||||
if sp is None:
|
||||
log.warn(f'ignored cell of unknown type: {n}')
|
||||
else:
|
||||
level_ops.append((sp, l.index, *in_idxs, *a_ctrl[l]))
|
||||
assert sp is not None, f'Unsupported node type {n}. Map circuit to KYUPY techlib first using resolce_tlib_cells().'
|
||||
level_ops.append((sp, l.index, *in_idxs, *a_ctrl[l]))
|
||||
|
||||
if len(level_ops) > 0: levels.append(level_ops)
|
||||
level_lines = prev_level_lines
|
||||
@@ -271,7 +274,7 @@ class SimOps:
|
||||
ref_count[self.tmp2_idx] += 1
|
||||
|
||||
# allocate and keep memory for PI/PPI, keep memory for PO/PPO (allocated later)
|
||||
for i, n in enumerate(circuit.s_nodes):
|
||||
for i, n in enumerate(circuit.s_nodes(KYUPY)):
|
||||
if 'dff' in n.kind.lower() or len(n.ins) == 0: # PPI or PI
|
||||
self.c_locs[self.ppi_offset + i], self.c_caps[self.ppi_offset + i] = h.alloc(c_caps_min), c_caps_min
|
||||
ref_count[self.ppi_offset + i] += 1
|
||||
@@ -310,10 +313,15 @@ class SimOps:
|
||||
self.c_locs[lidx], self.c_caps[lidx] = self.c_locs[stem], self.c_caps[stem]
|
||||
|
||||
# copy memory location to PO/PPO area
|
||||
for i, n in enumerate(circuit.s_nodes):
|
||||
for i, n in enumerate(circuit.s_nodes(KYUPY)):
|
||||
if len(n.ins) > 0:
|
||||
self.c_locs[self.ppo_offset + i], self.c_caps[self.ppo_offset + i] = self.c_locs[n.ins[0]], self.c_caps[n.ins[0]]
|
||||
|
||||
# sanity check
|
||||
for oidx, io in zip(*np.nonzero(self.c_locs[self.ops[:, 1:6]] < 0)):
|
||||
log.error(f'unallocated {["o0","i0","i1","i2","i3"][io]} in op {self.ops[oidx]}')
|
||||
assert np.min(self.c_locs[self.ops[:, 1:6]]) >= 0, "Unallocated signal lines."
|
||||
|
||||
# line use information
|
||||
self.line_use_start = np.full(self.c_locs_len, -1, dtype=np.int32)
|
||||
self.line_use_stop = np.full(self.c_locs_len, len(self.levels), dtype=np.int32)
|
||||
@@ -348,3 +356,12 @@ class SimOps:
|
||||
|
||||
self.pippi_c_locs = np.concatenate([self.pi_c_locs, self.ppi_c_locs])
|
||||
self.poppo_c_locs = np.concatenate([self.po_c_locs, self.ppo_c_locs])
|
||||
|
||||
def cone_ops(self, origin_lines: set[int]):
|
||||
active_lines = {l for l in origin_lines}
|
||||
ops = []
|
||||
for kind, out0, in0, in1, in2, in3, *params in self.ops:
|
||||
if in0 in active_lines or in1 in active_lines or in2 in active_lines or in3 in active_lines or out0 in origin_lines:
|
||||
ops.append((kind, out0, in0, in1, in2, in3, *params))
|
||||
active_lines.add(out0)
|
||||
return np.asarray(ops, dtype=np.int32)
|
||||
+7
-5
@@ -106,7 +106,7 @@ class StilFile:
|
||||
tests[pi_map, i] = logic.mvarray(p.capture['_pi'][0])
|
||||
return tests
|
||||
|
||||
def tests_loc(self, circuit, init_filter=None, launch_filter=None):
|
||||
def tests_loc(self, circuit, tlib, init_filter=None, launch_filter=None):
|
||||
"""Assembles and returns a LoC scan test pattern set for given circuit.
|
||||
|
||||
This function assumes a launch-on-capture (LoC) delay test.
|
||||
@@ -139,7 +139,9 @@ class StilFile:
|
||||
init[scan_maps[si_port], i] = pattern
|
||||
init[pi_map, i] = logic.mvarray(p.launch['_pi'][0] if '_pi' in p.launch else p.capture['_pi'][0])
|
||||
if init_filter: init = init_filter(init)
|
||||
sim8v = LogicSim(circuit, init.shape[-1], m=8)
|
||||
circuit_resolved = circuit.copy()
|
||||
circuit_resolved.resolve_tlib_cells(tlib)
|
||||
sim8v = LogicSim(circuit_resolved, init.shape[-1], m=8)
|
||||
sim8v.s[0] = logic.mv_to_bp(init)
|
||||
sim8v.s_to_c()
|
||||
sim8v.c_prop()
|
||||
@@ -263,12 +265,12 @@ GRAMMAR = r"""
|
||||
"""
|
||||
|
||||
|
||||
def parse(text):
|
||||
def parse(text) -> StilFile:
|
||||
"""Parses the given ``text`` and returns a :class:`StilFile` object."""
|
||||
return Lark(GRAMMAR, parser="lalr", transformer=StilTransformer()).parse(text)
|
||||
return Lark(GRAMMAR, parser="lalr", transformer=StilTransformer()).parse(text) # type: ignore
|
||||
|
||||
|
||||
def load(file):
|
||||
def load(file) -> StilFile:
|
||||
"""Parses the contents of ``file`` and returns a :class:`StilFile` object.
|
||||
|
||||
Files with `.gz`-suffix are decompressed on-the-fly.
|
||||
|
||||
+26
-3
@@ -38,9 +38,11 @@ class TechLib:
|
||||
else:
|
||||
pin_dict[n.name] = (o_idx, True)
|
||||
o_idx += 1
|
||||
has_dff = 'DFF' in set(n.kind for n in c.cells.values())
|
||||
has_latch = 'LATCH' in set(n.kind for n in c.cells.values())
|
||||
parts = [s[1:-1].split(',') if s[0] == '{' else [s] for s in re.split(r'({[^}]+})', c.name) if len(s) > 0]
|
||||
for name in [''.join(item) for item in product(*parts)]:
|
||||
self.cells[name] = (c, pin_dict)
|
||||
self.cells[name] = (c, pin_dict, has_dff, has_latch)
|
||||
|
||||
def pin_index(self, kind, pin):
|
||||
"""Returns a pin list position for a given node kind and pin name."""
|
||||
@@ -48,19 +50,39 @@ class TechLib:
|
||||
assert pin in self.cells[kind][1], f'Unknown pin: {pin} for cell {kind}'
|
||||
return self.cells[kind][1][pin][0]
|
||||
|
||||
def pin_name(self, kind, pos, output=False):
|
||||
def pin_name(self, kind: str, pos: int, output=False) -> str:
|
||||
"""Returns the pin name for a given node kind, list position, and direction."""
|
||||
assert kind in self.cells, f'Unknown cell: {kind}'
|
||||
for name, (ppos, isout) in self.cells[kind][1].items():
|
||||
if isout == output and ppos == pos:
|
||||
return name
|
||||
return None
|
||||
raise ValueError(f'Invalid {"output" if output else "input"} pin position {pos} for cell: {kind}')
|
||||
|
||||
def pin_is_output(self, kind, pin):
|
||||
"""Returns True, if given pin name of a node kind is an output."""
|
||||
assert kind in self.cells, f'Unknown cell: {kind}'
|
||||
assert pin in self.cells[kind][1], f'Unknown pin: {pin} for cell {kind}'
|
||||
return self.cells[kind][1][pin][1]
|
||||
|
||||
def is_dff(self, kind):
|
||||
"""Returns True, if given node kind is a d-flip-flop."""
|
||||
if kind == '__fork__': return False
|
||||
if kind == '__const0__': return False
|
||||
if kind == '__const1__': return False
|
||||
if kind == 'input': return False
|
||||
if kind == 'output': return False
|
||||
assert kind in self.cells, f'Unknown cell: {kind}'
|
||||
return self.cells[kind][2]
|
||||
|
||||
def is_latch(self, kind):
|
||||
"""Returns True, if given node kind is a latch."""
|
||||
if kind == '__fork__': return False
|
||||
if kind == '__const0__': return False
|
||||
if kind == '__const1__': return False
|
||||
if kind == 'input': return False
|
||||
if kind == 'output': return False
|
||||
assert kind in self.cells, f'Unknown cell: {kind}'
|
||||
return self.cells[kind][3]
|
||||
|
||||
|
||||
KYUPY = TechLib(r"""
|
||||
@@ -98,6 +120,7 @@ AOI211 input(i0,i1,i2,i3) output(o) o=AOI211(i0,i1,i2,i3) ;
|
||||
OAI211 input(i0,i1,i2,i3) output(o) o=OAI211(i0,i1,i2,i3) ;
|
||||
MUX21 input(i0,i1,i2) output(o) o=MUX21(i0,i1,i2) ;
|
||||
DFF input(D,CLK) output(Q) Q=DFF(D,CLK) ;
|
||||
LATCH input(D,CLK) output(Q) Q=LATCH(D,CLK) ;
|
||||
""")
|
||||
"""A synthetic library of all KyuPy simulation primitives.
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import numpy as np
|
||||
|
||||
from kyupy import atalanta, logic
|
||||
|
||||
|
||||
def test_parse():
|
||||
text = '\n'.join([
|
||||
'* Test pattern file',
|
||||
'1: 0011',
|
||||
'2: 1100',
|
||||
'1010',
|
||||
])
|
||||
tf = atalanta.parse(text)
|
||||
|
||||
# second-to-last axis = inputs, last axis = patterns
|
||||
assert tf.patterns.shape == (4, 3)
|
||||
assert tf.patterns.dtype == np.uint8
|
||||
assert set(np.unique(tf.patterns)) <= {logic.ZERO, logic.ONE}
|
||||
|
||||
# patterns are stored along the last axis
|
||||
assert list(tf.patterns[:, 0]) == [logic.ZERO, logic.ZERO, logic.ONE, logic.ONE]
|
||||
assert list(tf.patterns[:, 1]) == [logic.ONE, logic.ONE, logic.ZERO, logic.ZERO]
|
||||
assert list(tf.patterns[:, 2]) == [logic.ONE, logic.ZERO, logic.ONE, logic.ZERO]
|
||||
|
||||
|
||||
def test_empty():
|
||||
tf = atalanta.parse('* only comments\n* nothing else\n')
|
||||
assert tf.patterns.shape == (0, 0)
|
||||
assert tf.patterns.dtype == np.uint8
|
||||
@@ -2,7 +2,7 @@ import pickle
|
||||
|
||||
from kyupy.circuit import GrowingList, Circuit, Node, Line
|
||||
from kyupy import verilog, bench
|
||||
from kyupy.techlib import SAED32
|
||||
from kyupy.techlib import KYUPY, SAED32
|
||||
|
||||
def test_growing_list():
|
||||
gl = GrowingList()
|
||||
@@ -155,7 +155,7 @@ def test_substitute():
|
||||
|
||||
def test_resolve(mydir):
|
||||
c = verilog.load(mydir / 'b15_4ig.v.gz', tlib=SAED32)
|
||||
s_names = [n.name for n in c.s_nodes]
|
||||
s_names = [n.name for n in c.s_nodes(SAED32)]
|
||||
c.resolve_tlib_cells(SAED32)
|
||||
s_names_prim = [n.name for n in c.s_nodes]
|
||||
s_names_prim = [n.name for n in c.s_nodes(KYUPY)]
|
||||
assert s_names == s_names_prim, 'resolve_tlib_cells does not preserve names or order of s_nodes'
|
||||
+49
-54
@@ -2,8 +2,8 @@ import numpy as np
|
||||
|
||||
from kyupy.logic_sim import LogicSim, LogicSim2V, LogicSim4V, LogicSim6V
|
||||
from kyupy import bench, logic, sim, verilog
|
||||
from kyupy.logic import mvarray, bparray, bp_to_mv, mv_to_bp
|
||||
from kyupy.techlib import SAED90
|
||||
from kyupy.logic import mv_str, mvarray, bparray, bp_to_mv, mv_to_bp
|
||||
from kyupy.techlib import SAED90, KYUPY
|
||||
|
||||
def test_dangling():
|
||||
c = verilog.parse('''
|
||||
@@ -20,6 +20,18 @@ def test_dangling():
|
||||
sim.simulate(p)
|
||||
assert p[1,0] == logic.ONE
|
||||
|
||||
def test_used_output():
|
||||
c = bench.parse('input(i0) output(o0) output(o1) o0=BUF1(i0) o1=BUF(o0)')
|
||||
sim = LogicSim2V(c)
|
||||
expect = mvarray('000', '111')
|
||||
actual = sim.simulate(expect.copy())
|
||||
np.testing.assert_array_equal(
|
||||
expect[c.io_locs('o0')],
|
||||
actual[c.io_locs('o0')])
|
||||
np.testing.assert_array_equal(
|
||||
expect[c.io_locs('o1')],
|
||||
actual[c.io_locs('o1')])
|
||||
|
||||
def assert_equal_shape_and_contents(actual, desired):
|
||||
desired = np.array(desired, dtype=np.uint8)
|
||||
assert actual.shape == desired.shape
|
||||
@@ -105,7 +117,7 @@ def test_LogicSim6V_simprims(mydir):
|
||||
sim1.simulate(tests1)
|
||||
sim2.simulate(tests2)
|
||||
for loc1, loc2 in zip(c1.io_locs('o'), c2.io_locs('o')):
|
||||
n = c1.s_nodes[loc1]
|
||||
n = c1.s_nodes(KYUPY)[loc1]
|
||||
if (tests1[loc1] != tests2[loc2]).any():
|
||||
print(f'Mismatch at output {n}')
|
||||
np.testing.assert_array_equal(
|
||||
@@ -150,14 +162,12 @@ def test_2v():
|
||||
o31=OAI211(i0,i1,i2,i3)
|
||||
o32=MUX21(i0,i1,i2)
|
||||
''')
|
||||
s = LogicSim(c, 16, m=2)
|
||||
bpa = logic.bparray([f'{i:04b}'+('-'*(s.s_len-4)) for i in range(16)])
|
||||
s.s[0] = bpa
|
||||
s = LogicSim2V(c, 16)
|
||||
s.s_assign[...] = logic.mvarray([f'{i:04b}'+('-'*(s.s_len-4)) for i in range(16)])
|
||||
s.s_to_c()
|
||||
s.c_prop()
|
||||
s.c_to_s()
|
||||
mva = logic.bp_to_mv(s.s[1])
|
||||
for res, exp in zip(logic.packbits(mva[4:], dtype=np.uint32), [
|
||||
for res, exp in zip(logic.packbits(s.s_result[4:], dtype=np.uint32), [
|
||||
sim.BUF1, sim.INV1,
|
||||
sim.AND2, sim.AND3, sim.AND4,
|
||||
sim.NAND2, sim.NAND3, sim.NAND4,
|
||||
@@ -178,19 +188,17 @@ def test_2v():
|
||||
|
||||
def test_4v():
|
||||
c = bench.parse('input(x, y) output(a, o, n) a=and(x,y) o=or(x,y) n=not(x)')
|
||||
s = LogicSim(c, 16, m=4)
|
||||
s = LogicSim4V(c, 16)
|
||||
assert s.s_len == 5
|
||||
bpa = bparray(
|
||||
s.s_assign[...] = mvarray(
|
||||
'00---', '01---', '0----', '0X---',
|
||||
'10---', '11---', '1----', '1X---',
|
||||
'-0---', '-1---', '-----', '-X---',
|
||||
'X0---', 'X1---', 'X----', 'XX---')
|
||||
s.s[0] = bpa
|
||||
s.s_to_c()
|
||||
s.c_prop()
|
||||
s.c_to_s()
|
||||
mva = bp_to_mv(s.s[1])
|
||||
assert_equal_shape_and_contents(mva, mvarray(
|
||||
assert_equal_shape_and_contents(s.s_result, mvarray(
|
||||
'--001', '--011', '--0X1', '--0X1',
|
||||
'--010', '--110', '--X10', '--X10',
|
||||
'--0XX', '--X1X', '--XXX', '--XXX',
|
||||
@@ -198,38 +206,34 @@ def test_4v():
|
||||
|
||||
def test_4v_fault():
|
||||
c = bench.parse('input(x, y) output(a) a=and(x,y)')
|
||||
s = LogicSim(c, 16, m=4)
|
||||
s = LogicSim4V(c, 16)
|
||||
assert s.s_len == 3
|
||||
bpa = bparray(
|
||||
s.s_assign[...] = mvarray(
|
||||
'00-', '01-', '0--', '0X-',
|
||||
'10-', '11-', '1--', '1X-',
|
||||
'-0-', '-1-', '---', '-X-',
|
||||
'X0-', 'X1-', 'X--', 'XX-')
|
||||
s.s[0] = bpa
|
||||
s.s_to_c()
|
||||
s.c_prop()
|
||||
s.c_to_s()
|
||||
mva = bp_to_mv(s.s[1])
|
||||
assert_equal_shape_and_contents(mva, mvarray(
|
||||
assert_equal_shape_and_contents(s.s_result, mvarray(
|
||||
'--0', '--0', '--0', '--0',
|
||||
'--0', '--1', '--X', '--X',
|
||||
'--0', '--X', '--X', '--X',
|
||||
'--0', '--X', '--X', '--X'))
|
||||
fault_line = s.circuit.cells['a'].ins[0]
|
||||
s.s_to_c()
|
||||
s.c_prop(fault_line=fault_line, fault_model=1)
|
||||
s.c_prop(fault_line=fault_line.index, fault_model=1)
|
||||
s.c_to_s()
|
||||
mva = bp_to_mv(s.s[1])
|
||||
assert_equal_shape_and_contents(mva, mvarray(
|
||||
assert_equal_shape_and_contents(s.s_result, mvarray(
|
||||
'--0', '--1', '--X', '--X',
|
||||
'--0', '--1', '--X', '--X',
|
||||
'--0', '--1', '--X', '--X',
|
||||
'--0', '--1', '--X', '--X'))
|
||||
s.s_to_c()
|
||||
s.c_prop(fault_line=fault_line, fault_model=0)
|
||||
s.c_prop(fault_line=fault_line.index, fault_model=0)
|
||||
s.c_to_s()
|
||||
mva = bp_to_mv(s.s[1])
|
||||
assert_equal_shape_and_contents(mva, mvarray(
|
||||
assert_equal_shape_and_contents(s.s_result, mvarray(
|
||||
'--0', '--0', '--0', '--0',
|
||||
'--0', '--0', '--0', '--0',
|
||||
'--0', '--0', '--0', '--0',
|
||||
@@ -255,11 +259,11 @@ def test_6v():
|
||||
resp = s.s[1].copy()
|
||||
|
||||
exp_resp = np.copy(mva)
|
||||
exp_resp[:2] = logic.ZERO
|
||||
exp_resp[:2] = logic.UNASSIGNED
|
||||
np.testing.assert_allclose(resp, exp_resp)
|
||||
|
||||
|
||||
def test_8v():
|
||||
def xtest_8v():
|
||||
c = bench.parse('input(x, y) output(a, o, n, xo) a=and(x,y) o=or(x,y) n=not(x) xo=xor(x,y)')
|
||||
s = LogicSim(c, 64, m=8)
|
||||
assert s.s_len == 6
|
||||
@@ -286,8 +290,8 @@ def test_8v():
|
||||
np.testing.assert_allclose(resp, exp_resp)
|
||||
|
||||
|
||||
def test_loop():
|
||||
c = bench.parse('q=dff(d) d=not(q)')
|
||||
def xtest_loop():
|
||||
c = bench.parse('q=DFF(d) d=NOT(q)')
|
||||
s = LogicSim(c, 4, m=8)
|
||||
assert s.s_len == 1
|
||||
mva = mvarray([['0'], ['1'], ['R'], ['F']])
|
||||
@@ -313,8 +317,8 @@ def test_loop():
|
||||
# assert resp[3] == 'F'
|
||||
|
||||
|
||||
def test_latch():
|
||||
c = bench.parse('input(d, t) output(q) q=latch(d, t)')
|
||||
def xtest_latch():
|
||||
c = bench.parse('input(d, t) output(q) q=LATCH(d, t)')
|
||||
s = LogicSim(c, 8, m=8)
|
||||
assert s.s_len == 4
|
||||
mva = mvarray('00-0', '00-1', '01-0', '01-1', '10-0', '10-1', '11-0', '11-1')
|
||||
@@ -327,19 +331,6 @@ def test_latch():
|
||||
# assert resp[i] == exp[i]
|
||||
|
||||
|
||||
def test_b01(mydir):
|
||||
c = bench.load(mydir / 'b01.bench')
|
||||
|
||||
# 8-valued
|
||||
s = LogicSim(c, 8, m=8)
|
||||
mva = np.zeros((s.s_len, 8), dtype=np.uint8)
|
||||
s.s[0] = mv_to_bp(mva)
|
||||
s.s_to_c()
|
||||
s.c_prop()
|
||||
s.c_to_s()
|
||||
bp_to_mv(s.s[1])
|
||||
|
||||
|
||||
def sim_and_compare(c, test_resp, m=8):
|
||||
tests, resp = test_resp
|
||||
lsim = LogicSim(c, m=m, sims=tests.shape[1])
|
||||
@@ -356,14 +347,14 @@ def sim_and_compare(c, test_resp, m=8):
|
||||
print(f'mismatch pattern:{pat} ppio:{idx} exp:{logic.mv_str(resp[idx,pat])} act:{logic.mv_str(resp_sim[idx,pat])}')
|
||||
assert len(idxs) == 0
|
||||
|
||||
def sim_and_compare_6v(c, test_resp):
|
||||
def sim_and_compare_cls(c, test_resp, SimCls):
|
||||
tests, resp = test_resp
|
||||
lsim = LogicSim6V(c, sims=tests.shape[1])
|
||||
lsim.s[0] = tests
|
||||
lsim = SimCls(c, sims=tests.shape[1])
|
||||
lsim.s_assign[...] = tests
|
||||
lsim.s_to_c()
|
||||
lsim.c_prop()
|
||||
lsim.c_to_s()
|
||||
resp_sim = lsim.s[1]
|
||||
resp_sim = lsim.s_result
|
||||
idxs, pats = np.nonzero(((resp == logic.ONE) & (resp_sim != logic.ONE)) | ((resp == logic.ZERO) & (resp_sim != logic.ZERO)))
|
||||
for i, (idx, pat) in enumerate(zip(idxs, pats)):
|
||||
if i >= 10:
|
||||
@@ -374,28 +365,32 @@ def sim_and_compare_6v(c, test_resp):
|
||||
|
||||
|
||||
def test_b15_2ig_sa_2v(b15_2ig_circuit_resolved, b15_2ig_sa_nf_test_resp):
|
||||
sim_and_compare(b15_2ig_circuit_resolved, b15_2ig_sa_nf_test_resp, m=2)
|
||||
sim_and_compare_cls(b15_2ig_circuit_resolved, b15_2ig_sa_nf_test_resp, LogicSim2V)
|
||||
|
||||
|
||||
def test_b15_2ig_sa_4v(b15_2ig_circuit_resolved, b15_2ig_sa_nf_test_resp):
|
||||
sim_and_compare(b15_2ig_circuit_resolved, b15_2ig_sa_nf_test_resp, m=4)
|
||||
sim_and_compare_cls(b15_2ig_circuit_resolved, b15_2ig_sa_nf_test_resp, LogicSim4V)
|
||||
|
||||
|
||||
def test_b15_2ig_sa_6v(b15_2ig_circuit_resolved, b15_2ig_sa_nf_test_resp):
|
||||
sim_and_compare_6v(b15_2ig_circuit_resolved, b15_2ig_sa_nf_test_resp)
|
||||
sim_and_compare_cls(b15_2ig_circuit_resolved, b15_2ig_sa_nf_test_resp, LogicSim6V)
|
||||
|
||||
|
||||
def test_b15_2ig_sa_8v(b15_2ig_circuit_resolved, b15_2ig_sa_nf_test_resp):
|
||||
def xtest_b15_2ig_sa_8v(b15_2ig_circuit_resolved, b15_2ig_sa_nf_test_resp):
|
||||
sim_and_compare(b15_2ig_circuit_resolved, b15_2ig_sa_nf_test_resp, m=8)
|
||||
|
||||
|
||||
def test_b15_4ig_sa_2v(b15_4ig_circuit_resolved, b15_4ig_sa_rf_test_resp):
|
||||
sim_and_compare(b15_4ig_circuit_resolved, b15_4ig_sa_rf_test_resp, m=2)
|
||||
sim_and_compare_cls(b15_4ig_circuit_resolved, b15_4ig_sa_rf_test_resp, LogicSim2V)
|
||||
|
||||
|
||||
def test_b15_4ig_sa_4v(b15_4ig_circuit_resolved, b15_4ig_sa_rf_test_resp):
|
||||
sim_and_compare(b15_4ig_circuit_resolved, b15_4ig_sa_rf_test_resp, m=4)
|
||||
sim_and_compare_cls(b15_4ig_circuit_resolved, b15_4ig_sa_rf_test_resp, LogicSim4V)
|
||||
|
||||
|
||||
def test_b15_4ig_sa_8v(b15_4ig_circuit_resolved, b15_4ig_sa_rf_test_resp):
|
||||
def test_b15_4ig_sa_6v(b15_4ig_circuit_resolved, b15_4ig_sa_rf_test_resp):
|
||||
sim_and_compare_cls(b15_4ig_circuit_resolved, b15_4ig_sa_rf_test_resp, LogicSim6V)
|
||||
|
||||
|
||||
def xtest_b15_4ig_sa_8v(b15_4ig_circuit_resolved, b15_4ig_sa_rf_test_resp):
|
||||
sim_and_compare(b15_4ig_circuit_resolved, b15_4ig_sa_rf_test_resp, m=8)
|
||||
+1
-1
@@ -14,7 +14,7 @@ def test_b15(mydir):
|
||||
assert len(resp) > 0
|
||||
|
||||
s2 = stil.load(mydir / 'b15_2ig.tf_nf.stil.gz')
|
||||
tests = s2.tests_loc(b15)
|
||||
tests = s2.tests_loc(b15, SAED32)
|
||||
resp = s2.responses(b15)
|
||||
assert len(tests) > 0
|
||||
assert len(resp) > 0
|
||||
|
||||
@@ -9,7 +9,7 @@ def test_b01(mydir):
|
||||
|
||||
assert len(c.nodes) == 139
|
||||
assert len(c.lines) == 203
|
||||
stats = c.stats
|
||||
stats = c.stats(SAED90)
|
||||
assert stats['input'] == 6
|
||||
assert stats['output'] == 3
|
||||
assert stats['__seq__'] == 5
|
||||
@@ -19,7 +19,7 @@ def test_b15(mydir):
|
||||
c = verilog.load(mydir / 'b15_4ig.v.gz', tlib=SAED32)
|
||||
assert len(c.nodes) == 12067
|
||||
assert len(c.lines) == 20731
|
||||
stats = c.stats
|
||||
stats = c.stats(SAED32)
|
||||
assert stats['input'] == 40
|
||||
assert stats['output'] == 71
|
||||
assert stats['__seq__'] == 417
|
||||
@@ -29,7 +29,7 @@ def test_gates(mydir):
|
||||
c = verilog.load(mydir / 'gates.v', tlib=NANGATE45)
|
||||
assert len(c.nodes) == 18
|
||||
assert len(c.lines) == 21
|
||||
stats = c.stats
|
||||
stats = c.stats(NANGATE45)
|
||||
assert stats['input'] == 3
|
||||
assert stats['output'] == 4
|
||||
assert stats['__seq__'] == 0
|
||||
@@ -39,7 +39,7 @@ def test_halton2(mydir):
|
||||
c = verilog.load(mydir / 'rng_haltonBase2.synth_yosys.v', tlib=SAED90)
|
||||
assert len(c.nodes) == 146
|
||||
assert len(c.lines) == 210
|
||||
stats = c.stats
|
||||
stats = c.stats(SAED90)
|
||||
assert stats['input'] == 2
|
||||
assert stats['output'] == 12
|
||||
assert stats['__seq__'] == 12
|
||||
@@ -1,7 +1,7 @@
|
||||
import numpy as np
|
||||
|
||||
from kyupy.wave_sim import WaveSim, WaveSimCuda, wave_eval_cpu, TMIN, TMAX
|
||||
from kyupy.logic_sim import LogicSim
|
||||
from kyupy.logic_sim import LogicSim6V
|
||||
from kyupy import logic, bench, sim
|
||||
from kyupy.logic import mvarray
|
||||
|
||||
@@ -175,12 +175,12 @@ def compare_to_logic_sim(wsim: WaveSim):
|
||||
resp |= ((resp ^ (resp >> 1)) & 1) << 2 # transitions
|
||||
resp[wsim.pi_s_locs] = logic.UNASSIGNED
|
||||
|
||||
lsim = LogicSim(wsim.circuit, tests.shape[-1])
|
||||
lsim.s[0] = logic.mv_to_bp(tests)
|
||||
lsim = LogicSim6V(wsim.circuit, tests.shape[-1])
|
||||
lsim.s_assign[...] = tests
|
||||
lsim.s_to_c()
|
||||
lsim.c_prop()
|
||||
lsim.c_to_s()
|
||||
exp = logic.bp_to_mv(lsim.s[1])[:,:tests.shape[-1]]
|
||||
exp = lsim.s_result[:,:tests.shape[-1]]
|
||||
|
||||
resp[resp == logic.PPULSE] = logic.ZERO
|
||||
resp[resp == logic.NPULSE] = logic.ONE
|
||||
|
||||
Reference in new issue
Block a user