diff --git a/.vscode/settings.json b/.vscode/settings.json index 882c8c5..037adde 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,10 @@ { "python.linting.enabled": true, + "python.linting.pycodestyleEnabled": true, "python.testing.pytestArgs": [ "tests" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, + } \ No newline at end of file diff --git a/setup.py b/setup.py index 1bae99c..61f39e7 100644 --- a/setup.py +++ b/setup.py @@ -6,12 +6,15 @@ setup( setup_requires=['setuptools_scm'], author='Maxim Slipenko', packages=['turinglab'], - install_requires = [ - 'python-docx' + install_requires=[ + 'python-docx', + 'pydot', + 'requests', + 'sortedcontainers' ], - entry_points = { + entry_points={ 'console_scripts': [ 'turinglab = turinglab.__main__:main' ] } -) \ No newline at end of file +) diff --git a/turinglab/__main__.py b/turinglab/__main__.py index a916508..414ac15 100644 --- a/turinglab/__main__.py +++ b/turinglab/__main__.py @@ -1,35 +1,39 @@ import os import sys from argparse import ArgumentParser -from turinglab.image import get_image +# from turinglab.image import get_image from turinglab.input import from_file from turinglab.emulator import Emulator -from turinglab.output import to_docx +from turinglab.output import output def get_parser() -> ArgumentParser: parser = ArgumentParser() - parser.add_argument("input_file", type=str, help="Path to file with program",) + parser.add_argument("input_file", type=str, + help="Path to file with program",) parser.add_argument("output_dir", type=str, help="Output dir") - parser.add_argument('-t','--tests', nargs='+', type=str, help='List of input strings for tests') - parser.add_argument('-e','--empty-character', type=str, help='Empty character') - parser.add_argument('-f','--force', default=False, action='store_true', help='Force rewrite output dir') + parser.add_argument("-t", '--tests', nargs='+', type=str, + help='List of input strings for tests') + parser.add_argument('-e', '--empty-character', + type=str, help='Empty character') + parser.add_argument('-f', '--force', default=False, + action='store_true', help='Force rewrite output dir') return parser + def main(): parser = get_parser() - args = parser.parse_args() program = from_file(args.input_file) - - if args.force == False and os.path.exists(args.output_dir): + + if args.force is False and os.path.exists(args.output_dir): print('Directory already exists!') return -1 os.makedirs(args.output_dir, exist_ok=True) - test_data = [] + tests = [] for test in args.tests: test = test.replace(args.empty_character, 'λ') @@ -42,11 +46,13 @@ def main(): tm.step() data.append(tm.info()) - test_data.append(data) + tests.append(data) + + output(args.output_dir, program, tests) + + # to_docx(os.path.join(args.output_dir, 'report.docx'), program, test_data) + # get_image(os.path.join(args.output_dir, 'graph'), program) - to_docx(os.path.join(args.output_dir, 'report.docx'), program, test_data) - get_image(os.path.join(args.output_dir, 'graph'), program) - if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/turinglab/emulator.py b/turinglab/emulator.py index 2a090a8..38050ec 100644 --- a/turinglab/emulator.py +++ b/turinglab/emulator.py @@ -1,54 +1,135 @@ from collections import defaultdict +from sortedcontainers import SortedDict +from copy import copy, deepcopy +from enum import IntEnum -DIRECTIONS = { - '>': 1, - '<': -1, - '.': 0 -} + +class Movemement(IntEnum): + R = 1 + L = -1 + E = 0 + + +class Action: + def __init__( + self, + state, + symbol: chr, + movement: Movemement + ) -> None: + self.state = state + self.symbol = symbol + self.movement = movement + + +class Program: + data = dict() + + state_count = 0 + symbol_count = 0 + symbol_dict = dict() + + blank_symbol = 'λ' + + def __init__(self, blank_symbol: chr = 'λ') -> None: + self.blank_symbol = blank_symbol + + def is_blank(self, symbol) -> bool: + return symbol == self.blank_symbol + + def set(self, state, symbol, action) -> None: + if state not in self.data: + self.data[state] = dict() + self.state_count += 1 + + self.symbol_dict[symbol] = None + self.data[state][symbol] = action + + def get(self, state, symbol) -> Action or None: + if symbol not in self.data[state]: + return None + + return self.data[state][symbol] + + def get_symbols(self): + return list(self.symbol_dict.keys()) class Emulator(): - ''' + """ A class used to emulate an Turing machine - ''' + """ - def __init__(self, instructions, input_string: str = '', blank_symbol: chr = 'λ'): + class Tape: + + l_index = 0 + r_index = 0 + + def __init__(self, input_string: str, blank_symbol: str) -> None: + self.tape = SortedDict(dict(enumerate(input_string))) + self.blank_symbol = blank_symbol + + r_index = len(input_string) + pass + + def __deepcopy__(self, memo): + id_self = id(self) + _copy = memo.get(id_self) + if _copy is None: + _copy = type(self)('', self.blank_symbol) + + _copy.tape = deepcopy(self.tape) + _copy.blank_symbol = self.blank_symbol + _copy.l_index = self.l_index + _copy.r_index = self.r_index + + memo[id_self] = _copy + return _copy + + def __setitem__(self, key, value): + self.tape[key] = value + self.l_index = min(self.l_index, key) + self.r_index = max(self.r_index, key) + + def __getitem__(self, key): + if key not in self.tape: + return self.blank_symbol + + return self.tape[key] + + def values(self): + return self.tape.values() + + def __init__(self, program: Program, input_string: str = ''): self.head = 0 - self.current_state = 0 - - self.blank_symbol = blank_symbol - self.instructions = instructions - + self.state = 0 self.stopped = False - self.tape = defaultdict(lambda: self.blank_symbol, dict(enumerate(input_string))) + self.program = program + self.tape = Emulator.Tape(input_string, program.blank_symbol) def step(self): - if self.stopped == True: + if self.stopped is True: raise RuntimeError('Turing machine is stopped!') - symbol = self.tape[self.head] - try: - symbol, direction, state = self.instructions[symbol][self.current_state] - self.tape[self.head] = symbol + action = self.program.get(self.state, self.tape[self.head]) - self.current_state = state - self.head += DIRECTIONS[direction] - + self.tape[self.head] = action.symbol + self.state = action.state + self.head += int(action.movement) self.tape[self.head] = self.tape[self.head] - - if self.head < 0: - self.tape = defaultdict(lambda: self.blank_symbol, (sorted(self.tape.items()))) - if self.current_state == -1: + if self.state == -1: self.stopped = True - except: + except Exception as err: self.stopped = True + print('Error', err) def info(self): - return [self.head, self.current_state, defaultdict(lambda: self.blank_symbol, self.tape)] - - - + return [ + self.head, + self.state, + deepcopy(self.tape), + ] diff --git a/turinglab/image.py b/turinglab/image.py deleted file mode 100644 index 7ab4341..0000000 --- a/turinglab/image.py +++ /dev/null @@ -1,60 +0,0 @@ -import pydot -import requests -import shutil - -def get_image(filename, program): - g = pydot.Dot('my_graph', nodesep=0.5) - g.set_node_defaults( - width='1.5', - fontsize='18', - shape='circle' - ) - - rows = 0 - cols = len(program.keys()) - - for x in program.keys(): - if len(list(program[x].keys())) == 0: - continue - rows = max(list(program[x].keys())[-1] + 1, rows) - - program_table = [None] * rows - - for i in range(rows): - program_table[i] = [None] * cols - - symbols = [None] * cols - - for j, (symbol, value) in enumerate(program.items()): - symbols[j] = symbol - for i, (state, action) in enumerate(value.items()): - program_table[state][j] = action - - g.add_node(pydot.Node(f'gz', label='z>', shape='circle')) - - for i in range(rows): - g.add_node(pydot.Node(f'g{i}', label=f'{i}>', shape='circle')) - for j in range(cols): - action = program_table[i][j] - if action is None: continue - - symbol, direction, state = action - label = symbols[j] + '/' + symbol + '/' + ('R' if direction == '>' else 'L') - src = f'g{i}' - dst = f'g{"z" if state == -1 else state}' - if src == dst: - src = src + ':ne' - dst = dst + ':se' - g.add_edge(pydot.Edge(src, dst, label = label)) - - - g.write_raw(f'{filename}.dot', encoding='utf-8') - - try: - g.write_svg(f'{filename}.svg', encoding='utf-8') - except Exception: - output_raw_dot = g.to_string() - img_data = requests.get("https://quickchart.io/graphviz?graph=" + output_raw_dot).content - with open(f'{filename}_quickchart.svg', 'wb') as handler: - handler.write(img_data) - \ No newline at end of file diff --git a/turinglab/input.py b/turinglab/input.py index 53ed842..2ace6b9 100644 --- a/turinglab/input.py +++ b/turinglab/input.py @@ -1,78 +1,38 @@ -import csv import re +from turinglab.emulator import Program, Movemement, Action -def from_tur(filename): - f = open(filename, "rb") - raw = [] - byte = f.read(1) - while byte: - raw.append(byte) - byte = f.read(1) - indx = [0] - empty = [b'\x00', b'\x00', b'\x00'] - for i in range(len(raw) - 2): - if raw[i:i+3] == empty: - indx.append(i) - indx.append(i+3) - - data = [] - for i in range(1, len(indx)): - packet = raw[indx[i - 1]:indx[i]] - if packet != empty: - data.append(packet) - - task = b''.join(data[1]).decode('cp1251') - description = b''.join(data[4]).decode('cp1251') - solution = b''.join(data[3][:-2]).decode('cp1251') - solution = list(csv.reader(solution.split('\r\n'), delimiter='\t')) - - header = solution[:1][0][1:] - - solution_dict = [dict() for x in range(len(header))] - - for x in solution[1:]: - symbol = x[0] if x[0] != ' ' else 'λ' - - for (i, action) in enumerate(x[1:]): - action = list(action) - if len(action) == 0: - continue - - if action[0] == '_': - action[0] = 'λ' - - action[2] = int(action[2]) - 1 - - - solution_dict[i][symbol] = action - - return solution_dict, task, description def from_file(filename: str): + f = open(filename, "r") - program = dict() + program = Program(blank_symbol="_") + + # program = dict() for line in f: symbol, *actions = line.replace("\n", "").split('\t') if symbol == " ": - symbol = "λ" - - program[symbol] = dict() + symbol = "_" for i, action in enumerate(actions): - if not action: + if not action: continue new_symbol, direction, new_state = re.split('([<|>|.])', action) - - if new_symbol == "_": - new_symbol = 'λ' new_state = int(new_state) - 1 - program[symbol][i] = ([new_symbol, direction, new_state]) + movement = { + '>': Movemement.R, + '.': Movemement.E, + '<': Movemement.L + }[direction] + + program.set(i, symbol, Action(new_state, new_symbol, movement)) + + # program[symbol][i] = ([new_symbol, direction, new_state]) f.close() - return program \ No newline at end of file + return program diff --git a/turinglab/output.py b/turinglab/output.py index 5157408..35d02f2 100644 --- a/turinglab/output.py +++ b/turinglab/output.py @@ -1,107 +1,123 @@ -from turinglab.image import get_image +import pydot +import requests +import shutil +from os import path from docx import Document -from docx.oxml import OxmlElement -from docx.oxml.ns import qn -from docx.shared import Pt, Inches +from docx.shared import Pt from docx.enum.text import WD_LINE_SPACING -from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.enum.text import WD_ALIGN_PARAGRAPH +from turinglab.emulator import Movemement, Program -def add_state(p, state): - p.add_run('q') - index_text = p.add_run(str('z' if state == -1 else state)) - index_text.font.subscript = True -def create_paragraph(document): - p = document.add_paragraph() - style_paragraph(p) - return p +def output( + directory, + program: Program, + tests, + docx_name="report", + graph_name="graph" +): + """Create output of a program and tests. It's includes a docx file with + a 'command system', 'functional table' and tests. + """ -def style_paragraph(p): - p.paragraph_format.space_after = Pt(0) - p.paragraph_format.space_before = Pt(0) - p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE + def create_paragraph(document, text=""): + p = document.add_paragraph(text) + style_paragraph(p) + return p + + def style_paragraph(p): + p.paragraph_format.space_after = Pt(0) + p.paragraph_format.space_before = Pt(0) + p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE + + def state_index(state): + return str('z' if state == -1 else state) + + def add_state(p, state): + p.add_run('q') + index_text = p.add_run(state_index(state)) + index_text.font.subscript = True + + def get_symbol(x): + return x if program.is_blank(x) is False else 'λ' -def to_docx(filename, program, data): document = Document() - - style = document.styles['Normal'] - font = style.font - font.name = 'Times New Roman' - font.size = Pt(14) - rows = 0 - cols = len(program.keys()) + # + # Command system + # + create_paragraph(document, "Система команд:") - for x in program.keys(): - if len(list(program[x].keys())) == 0: - continue - rows = max(list(program[x].keys())[-1] + 1, rows) + symbols = program.get_symbols() - program_table = [None] * rows + cols = len(symbols) + rows = program.state_count + program_table = [[[None] for n in range(cols)] for m in range(rows)] for i in range(rows): - program_table[i] = [None] * cols + for j in range(cols): + program_table[i][j] = program.get(i, symbols[j]) - symbols = [None] * cols - - for j, (symbol, value) in enumerate(program.items()): - symbols[j] = symbol - for i, (state, action) in enumerate(value.items()): - program_table[state][j] = action - for i in range(rows): for j in range(cols): action = program_table[i][j] - if action is None: continue + if action is None: + continue p = create_paragraph(document) add_state(p, i) - symbol, direction, state = action + movement = Movemement(action.movement).name - p.add_run(symbols[j] + ' → ') - add_state(p, state) - p.add_run(symbol + ('R' if direction == '>' else 'L')) + p.add_run(get_symbol(symbols[j]) + ' → ') + add_state(p, action.state) + p.add_run(get_symbol(action.symbol) + movement) - - p = create_paragraph(document) + # + # Functional table + # + create_paragraph(document) + create_paragraph(document, "Функциональная таблица:") table = document.add_table(rows + 1, cols + 1) table.style = 'Table Grid' table.autofit = True table.allow_autofit = True - + for i in range(0, rows): p = table.rows[i + 1].cells[0].paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER style_paragraph(p) add_state(p, i) - - for i, x in enumerate(program.keys()): + + for i, x in enumerate(symbols): p = table.rows[0].cells[i + 1].paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER style_paragraph(p) - p.add_run(x) + p.add_run(get_symbol(x)) for i in range(rows): for j in range(cols): action = program_table[i][j] - if action is None: continue + if action is None: + continue p = table.rows[i + 1].cells[j + 1].paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER style_paragraph(p) - symbol, direction, state = action - add_state(p, state) - p.add_run(symbol) - p.add_run('R' if direction == '>' else 'L') + movement = Movemement(action.movement).name + add_state(p, action.state) + p.add_run(get_symbol(action.symbol) + movement) + # + # Tests + # p = create_paragraph(document) - for i, test in enumerate(data): + for i, test in enumerate(tests): p = create_paragraph(document) - p.add_run(f'Test {i + 1}:') + p.add_run(f'Тест {i + 1}') for j, data in enumerate(test): p = create_paragraph(document) @@ -113,15 +129,76 @@ def to_docx(filename, program, data): p.add_run(': ') head, state, tape = data - offset = list(tape.keys())[0] - head -= offset + tape_str = ''.join(tape.values()) + head -= tape.l_index - p.add_run(tape_str[:head].lstrip('λ') + 'q') + left_str = tape_str[:head].lstrip(program.blank_symbol) + left_str = left_str.replace(program.blank_symbol, 'λ') + right_str = tape_str[head + 1:].rstrip(program.blank_symbol) + right_str = right_str.replace(program.blank_symbol, 'λ') - index_text = p.add_run(str('z' if state == -1 else state)) - index_text.font.subscript = True + p.add_run(left_str) + add_state(p, state) + p.add_run(get_symbol(tape_str[head])) + p.add_run(right_str) - p.add_run(tape_str[head] + tape_str[head + 1:].rstrip('λ')) - - document.save(filename) \ No newline at end of file + document.save(path.join(directory, docx_name + ".docx")) + + # + # Image + # + + g = pydot.Dot("mygraph", nodesep=0.75) + + g.set_node_defaults( + width='1.5', + fontsize='40', + shape='circle' + ) + g.set_edge_defaults( + fontsize='20' + ) + + end_node = pydot.Node(f'gz', label='z>', shape='circle') + g.add_node(end_node) + + for i in range(rows): + node = pydot.Node(f'g{i}', label=f'{i}>', shape='circle') + g.add_node(node) + for j in range(cols): + action = program_table[i][j] + if action is None: + continue + + movement = Movemement(action.movement).name + + label = get_symbol(symbols[j]) +\ + '/' + get_symbol(action.symbol) +\ + '/' + movement + + src = f'g{i}' + dst = f'g{state_index(action.state)}' + if src == dst: + src = src + ':ne' + dst = dst + ':se' + + g.add_edge(pydot.Edge(src, dst, label=label)) + + dot_path = path.join(directory, f'{graph_name}.dot') + g.write_raw(dot_path, encoding='utf-8') + + try: + svg_path = path.join(directory, f'{graph_name}.svg') + + g.write_svg(svg_path, encoding='utf-8') + except Exception: + output_raw_dot = g.to_string() + img_data = requests.get( + "https://quickchart.io/graphviz?graph=" + output_raw_dot + ).content + + svg_path = path.join(directory, f'{graph_name}_quickchart.svg') + + with open(svg_path, 'wb') as handler: + handler.write(img_data)