diff --git a/src/kyupy/atalanta.py b/src/kyupy/atalanta.py new file mode 100644 index 0000000..f101299 --- /dev/null +++ b/src/kyupy/atalanta.py @@ -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 ``: ``, +where ```` 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 ``:`` 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)) diff --git a/tests/test_atalanta.py b/tests/test_atalanta.py new file mode 100644 index 0000000..8fd2c58 --- /dev/null +++ b/tests/test_atalanta.py @@ -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