diff --git a/code/python/.gitignore b/code/python/.gitignore index 199bcf1..adea656 100644 --- a/code/python/.gitignore +++ b/code/python/.gitignore @@ -22,12 +22,16 @@ !/src/**/*.py !/main.py +!/models +!/models/*-schema.yaml +!/models/README.md + !/tests !/tests/**/ !/tests/**/*.py !/assets -!/assets/**/ +!/assets/*.yaml !/dist !/dist/VERSION diff --git a/code/python/README.md b/code/python/README.md index a311a6c..01b7049 100644 --- a/code/python/README.md +++ b/code/python/README.md @@ -12,7 +12,7 @@ die Methoden mit Daten ausprobieren. 1. Der Python-Compiler **`^3.10.*`** wird benötigt. 2. Es ist auch empfehlenswert, **`justfile`** zu installieren (siehe ). -## Setup -> Test -> Run ## +## Build -> Test -> Run ## In einem IDE in dem Repo zu diesem Ordner navigieren.
@@ -23,7 +23,7 @@ Wer das **justfile**-Tool hat: # Zeige alle Befehle: just # Zur Installation der Requirements (nur nach Änderungen): -just setup; +just build; # Zur Ausführung der unit tests: just tests; # Zur Ausführung des Programms @@ -43,3 +43,9 @@ python3 main.py Auf Windows verwendet man `py -3` od. `py -310` statt `python3`. Man kann auch mit einem guten Editor/IDE die Tests einzeln ausführen. + +## Testfälle durch Config-Datei ## + +Statt den Code immer anfassen zu müssen, kann man Fälle für die verschiedenen Algorithmen +in der **[./assets/commands](assets/commands.yaml)** erfassen und (mittels `just run`) +das Programm ausführen. diff --git a/code/python/assets/commands.yaml b/code/python/assets/commands.yaml new file mode 100644 index 0000000..1b648c2 --- /dev/null +++ b/code/python/assets/commands.yaml @@ -0,0 +1,33 @@ +## Beispiel für Seminarwoche 9 (Blatt 8) +- name: TSP + dist: + - [0, 7, 4, 3] + - [7, 0, 5, 6] + - [2, 5, 0, 5] + - [2, 7, 4, 0] + optimise: MIN + verbose: true +## Beispiele für Seminarwoche 10 (Blatt 9) +- &command_hirschberg + name: HIRSCHBERG + once: true + verbose: + - COSTS + - MOVES + # show: + # # - ATOMS + # - TREE + word1: 'happily ever after' + word2: 'apples' +- <<: *command_hirschberg + word1: 'ANSTRENGEN' + word2: 'ANSPANNEN' +- <<: *command_hirschberg + word1: 'ACGAAG' + word2: 'AGAT' +- <<: *command_hirschberg + word1: 'happily ever, lol' + word2: 'apple' +- <<: *command_hirschberg + word1: 'happily' + word2: 'applses' diff --git a/code/python/assets/config.yaml b/code/python/assets/config.yaml new file mode 100644 index 0000000..ca65d67 --- /dev/null +++ b/code/python/assets/config.yaml @@ -0,0 +1,8 @@ +info: + author: Raj Dahya + title: Algorithmen und Datenstrukturen 2 + description: |- + Ein Code-Projekt, das Algorithmen und Datenstrukturen aus dem Kurs + ADS2 an der Universität Leipzig (Sommersemester 2022) + implementiert. +options: {} diff --git a/code/python/dist/VERSION b/code/python/dist/VERSION index 77d6f4c..6e8bf73 100644 --- a/code/python/dist/VERSION +++ b/code/python/dist/VERSION @@ -1 +1 @@ -0.0.0 +0.1.0 diff --git a/code/python/justfile b/code/python/justfile index e2aab57..1d625ef 100644 --- a/code/python/justfile +++ b/code/python/justfile @@ -11,6 +11,7 @@ _default: # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PYTHON := if os_family() == "windows" { "py -3" } else { "python3" } +GEN_MODELS := "datamodel-codegen" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Macros @@ -42,6 +43,19 @@ _docker-build-and-log service: _docker-build-and-interact service container: @docker compose up --build -d {{service}} && docker attach {{container}} +_generate-models path name: + @{{GEN_MODELS}} \ + --input-file-type openapi \ + --encoding "UTF-8" \ + --disable-timestamp \ + --use-schema-description \ + --allow-population-by-field-name \ + --snake-case-field \ + --strict-nullable \ + --target-python-version 3.9 \ + --input {{path}}/{{name}}-schema.yaml \ + --output {{path}}/generated/{{name}}.py + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # TARGETS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -50,8 +64,16 @@ _docker-build-and-interact service container: # TARGETS: build # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -build: +build: _build-requirements _build-skip-requirements +_build-skip-requirements: build-models +_build-requirements: @{{PYTHON}} -m pip install --disable-pip-version-check -r requirements.txt +build-models: _check-system-requirements _build-models-nochecks +_build-models-nochecks: + @echo "Generate data models from schemata." + @just _create-folder-if-not-exists "models/generated" + @- just _generate-models "models" "config" + @- just _generate-models "models" "commands" # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # TARGETS: run @@ -145,3 +167,8 @@ _display-logs: check-system: @echo "Operating System detected: {{os_family()}}." @echo "Python command used: {{PYTHON}}." +_check-system-requirements: + @if ! ( {{GEN_MODELS}} --help >> /dev/null 2> /dev/null ); then \ + echo "Command '{{GEN_MODELS}}' did not work. Ensure that the installation of 'datamodel-code-generator' worked and that system paths are set." \ + exit 1; \ + fi diff --git a/code/python/main.py b/code/python/main.py index 702643b..8802794 100644 --- a/code/python/main.py +++ b/code/python/main.py @@ -6,18 +6,23 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import os; -import sys; +import sys +from token import MINUS; -os.chdir(os.path.join(os.path.dirname(__file__), '..')); +os.chdir(os.path.join(os.path.dirname(__file__))); sys.path.insert(0, os.getcwd()); -from src.core.log import *; from src.thirdparty.maths import *; + +from models.generated.commands import *; +from src.core.log import *; +from src.setup.config import *; from src.graphs.graph import *; from src.graphs.tarjan import *; -from src.travel.naive import *; +from src.tsp import *; from src.hirschberg import *; + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # GLOBAL CONSTANTS/VARIABLES # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -29,38 +34,21 @@ from src.hirschberg import *; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def enter(): - # ## Beispiel für Seminarwoche 9 (Blatt 8): - # tsp_naive_algorithm( - # dist = np.asarray([ - # [0, 7, 4, 3], - # [7, 0, 5, 6], - # [2, 5, 0, 5], - # [2, 7, 4, 0], - # ], dtype=float), - # optimise=min, - # verbose=True, - # ); - ## Beispiel für Seminarwoche 10 (Blatt 9): - hirschberg_algorithm( - # Y = 'ANSPANNEN', - # X = 'ANSTRENGEN', - # Y = 'AGAT', - # X = 'ACGAAG', - Y = 'apples', - X = 'happily ever after', - # Y = 'applses', - # X = 'happily ever, lol', - # Y = 'apple', - # X = 'happily', - # once = True, - # verb = VerboseMode.COSTS, - # verb = VerboseMode.MOVES, - verb = VerboseMode.COSTS_AND_MOVES, - show = [ - # DisplayOptions.ATOMS, - DisplayOptions.TREE, - ], - ); + for command in COMMANDS: + if isinstance(command, CommandTsp): + tsp_algorithm( + dist = np.asarray(command.dist, dtype=float), + optimise = min if command.optimise == EnumTspOptimise.min else max, + verbose = command.verbose, + ); + elif isinstance(command, CommandHirschberg): + hirschberg_algorithm( + X = command.word1, + Y = command.word2, + once = command.once, + verb = command.verbose, + show = command.show, + ); return; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/code/python/models/README.md b/code/python/models/README.md new file mode 100644 index 0000000..62249ae --- /dev/null +++ b/code/python/models/README.md @@ -0,0 +1,51 @@ +# Models # + +- In this folder the configuration files for the models of data classes (as `*.yaml`-files) are stored. +- These are interpreted by `openapi` to generate the classes for the source code during the `build` phase of the programme. +- Once the models are generated, the app configurations are read at run time and interpreted as these models. +- Generating instead of manually encoding the classes is safer/more stable as it allows for validation. + +## Folder structure ## + +As per the 3rd point, the `yaml`-file/s for the models (schemata) +and the `yaml`-file/s for actual configuration values for the app +are to be kept separately. + +All models are to be stored in [./models](../models/), +whereas configuration files are to be stored in [./assets](../assets/). + +Note that the generated python files are not stored in the repository. +When deployed on a server, these are generated as part of the `build`-process. + +## Developer notes ## + +### Prerequisites ### + +For the python source code, we currently use: + +- Python: `v3.10.*` +- Modules: + - `datamodel-code-generator==0.12.0` + +(This is all taken care of during the `build` process.) + +### Building the models ### + +To build the models before run time, use the following command + options: +```bash +datamodel-codegen + --input-file-type openapi + --encoding "UTF-8" + --disable-timestamp + --use-schema-description + --snake-case-field + --strict-nullable + --input + --output +``` +(cf. https://pydantic-docs.helpmanual.io/datamodel_code_generator/). + +Alternatively, call: +``` +just build +``` diff --git a/code/python/models/commands-schema.yaml b/code/python/models/commands-schema.yaml new file mode 100644 index 0000000..32a6a23 --- /dev/null +++ b/code/python/models/commands-schema.yaml @@ -0,0 +1,140 @@ +openapi: 3.0.3 +info: + version: 0.1.0 + title: Schemata for command instructions +servers: [] +paths: {} +components: + schemas: + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Commands + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Commands: + description: |- + List of commands to test algorithms/datastructures. + type: array + items: + $ref: "#/components/schemas/Command" + default: [] + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Command + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Command: + description: |- + Instructions for command to call + required: + - name + properties: &ref_command_properties + name: + $ref: '#/components/schemas/EnumAlgorithmNames' + additionalProperties: true + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Command - Algorithm: Tarjan + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CommandTarjan: + description: |- + Instructions for execution of Tarjan-Algorithm + type: object + required: + - name + properties: + <<: *ref_command_properties + # required: + # properties: + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Command - Algorithm: TSP + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CommandTsp: + description: |- + Instructions for execution of TSP-Algorithm + type: object + required: + - name + - optimise + - dist + properties: + <<: *ref_command_properties + dist: + type: array + items: + type: array + items: + type: number + optimise: + $ref: '#/components/schemas/EnumTSPOptimise' + verbose: + type: boolean + default: false + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Command - Algorithm: Hirschberg + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CommandHirschberg: + description: |- + Instructions for execution of Hirschberg-Algorithm + type: object + required: + - name + - horizontal + - vertical + properties: + <<: *ref_command_properties + word1: + description: Word that gets placed vertically in algorithm. + type: string + word2: + description: Word that gets placed horizontally in algorithm + type: string + once: + type: boolean + default: false + verbose: + type: array + items: + $ref: '#/components/schemas/EnumHirschbergVerbosity' + default: [] + show: + type: array + items: + $ref: '#/components/schemas/EnumHirschbergShow' + default: [] + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Enum Algorithm Names + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + EnumAlgorithmNames: + description: |- + Enumeration of possible algorithm options. + type: string + enum: + - TARJAN + - TSP + - HIRSCHBERG + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Enum TSP - Optimise Mode + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + EnumTSPOptimise: + description: |- + Enumeration of optimisation options for TSP + type: string + enum: + - MIN + - MAX + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Enum Hirschberg - Verbosity options + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + EnumHirschbergVerbosity: + description: |- + Enumeration of verbosity options for Hirschberg + type: string + enum: + - COSTS + - MOVES + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Enum Hirschberg - display options + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + EnumHirschbergShow: + description: |- + Enumeration of verbosity options for Hirschberg + type: string + enum: + - TREE + - ATOMS diff --git a/code/python/models/config-schema.yaml b/code/python/models/config-schema.yaml new file mode 100644 index 0000000..2ac4c5a --- /dev/null +++ b/code/python/models/config-schema.yaml @@ -0,0 +1,49 @@ +openapi: 3.0.3 +info: + version: 0.1.0 + title: Schemata for config models +servers: [] +paths: {} +components: + schemas: + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Config + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Config: + descripton: |- + Data model for all parts of the configuration. + type: object + required: + - info + - options + - calls + properties: + info: + $ref: "#/components/schemas/Info" + options: + $ref: "#/components/schemas/AppOptions" + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Info + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Info: + description: |- + Contains meta data about project. + type: object + required: + - title + - description + - author + properties: + title: + type: string + description: + type: string + author: + type: string + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # App Options + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + AppOptions: + description: |- + Options pertaining to the rudimentary setup of the app. + type: object diff --git a/code/python/requirements.txt b/code/python/requirements.txt index 34d5e2b..1990250 100644 --- a/code/python/requirements.txt +++ b/code/python/requirements.txt @@ -1,7 +1,35 @@ -pip>=22.0.4 +pip>=22.1.2 +wheel>=0.37.1 + +# running +anyio>=3.5.0 +aiohttp>=3.8.1 +asyncio>=3.4.3 +codetiming>=1.3.0 + +# testing + dev +coverage[toml]>=6.4 pytest>=7.1.1 +pytest-asyncio>=0.18.3 +pytest-cov>=3.0.0 pytest-lazy-fixture>=0.6.3 +pytest-order>=1.0.1 +testfixtures>=6.18.5 + +# config +python-dotenv>=0.2.0 +jsonschema>=4.4.0 +lazy-load>=0.8.2 +pyyaml>=6.0 +pydantic>=1.9.0 +datamodel-code-generator>=0.12.0 + +# misc +lorem>=0.1.1 +safetywrap>=1.5.0 typing>=3.7.4.3 + +# maths numpy>=1.22.3 pandas>=1.4.1 tabulate>=0.8.9 diff --git a/code/python/src/hirschberg/__init__.py b/code/python/src/hirschberg/__init__.py index 2f1a1ce..8547acc 100644 --- a/code/python/src/hirschberg/__init__.py +++ b/code/python/src/hirschberg/__init__.py @@ -15,6 +15,4 @@ from src.hirschberg.display import *; __all__ = [ 'hirschberg_algorithm', - 'VerboseMode', - 'DisplayOptions', ]; diff --git a/code/python/src/hirschberg/algorithms.py b/code/python/src/hirschberg/algorithms.py index 88d02c2..5eb07e9 100644 --- a/code/python/src/hirschberg/algorithms.py +++ b/code/python/src/hirschberg/algorithms.py @@ -8,6 +8,7 @@ from src.thirdparty.types import *; from src.thirdparty.maths import *; +from models.generated.commands import *; from src.hirschberg.constants import *; from src.hirschberg.display import *; from src.hirschberg.matrix import *; @@ -30,8 +31,8 @@ __all__ = [ def simple_algorithm( X: str, Y: str, - verb: VerboseMode = VerboseMode.NONE, - show: List[DisplayOptions] = [], + verb: List[EnumHirschbergVerbosity] = [], + show: List[EnumHirschbergShow] = [], ) -> Tuple[str, str]: ''' Dieser Algorithmus berechnet die Edit-Distanzen + optimale Richtungen ein Mal. @@ -40,7 +41,7 @@ def simple_algorithm( Costs, Moves = compute_cost_matrix(X = '-' + X, Y = '-' + Y); path = reconstruct_optimal_path(Moves=Moves); word_x, word_y = reconstruct_words(X = '-' + X, Y = '-' + Y, moves=[Moves[coord] for coord in path], path=path); - if verb != VerboseMode.NONE: + if verb != []: repr = display_cost_matrix(Costs=Costs, path=path, X = '-' + X, Y = '-' + Y, verb=verb); display = word_y + f'\n{"-"*len(word_x)}\n' + word_x; print(f'\n{repr}\n\n\x1b[1mOptimales Alignment:\x1b[0m\n\n{display}\n'); @@ -50,8 +51,8 @@ def hirschberg_algorithm( X: str, Y: str, once: bool = False, - verb: VerboseMode = VerboseMode.NONE, - show: List[DisplayOptions] = [], + verb: List[EnumHirschbergVerbosity] = [], + show: List[EnumHirschbergShow] = [], ) -> Tuple[str, str]: ''' Der Hirschberg-Algorithmus berechnet nur die Edit-Distanzen (Kostenmatrix) @@ -72,8 +73,8 @@ def hirschberg_algorithm( word_y = align.as_string2(); # verbose output hier behandeln (irrelevant für Algorithmus): - if verb != VerboseMode.NONE: - if DisplayOptions.TREE in show: + if verb != []: + if EnumHirschbergShow.tree in show: display = align.astree(braces=True); else: display_x = align.as_string1(braces=True); @@ -87,8 +88,8 @@ def hirschberg_algorithm_step( X: str, Y: str, depth: int = 0, - verb: VerboseMode = VerboseMode.NONE, - show: List[DisplayOptions] = [], + verb: List[EnumHirschbergVerbosity] = [], + show: List[EnumHirschbergShow] = [], ) -> Alignment: ''' Der rekursive Schritt der Hirschberg-Algorithmus teil eines der Wörter in zwei @@ -105,7 +106,7 @@ def hirschberg_algorithm_step( word_x, word_y = reconstruct_words(X = '-' + X, Y = '-' + Y, moves=[Moves[coord] for coord in path], path=path); # verbose output hier behandeln (irrelevant für Algorithmus): - if verb != VerboseMode.NONE and (DisplayOptions.ATOMS in show): + if verb != [] and (EnumHirschbergShow.atoms in show): repr = display_cost_matrix(Costs=Costs, path=path, X = '-' + X, Y = '-' + Y, verb=verb); print(f'\n\x1b[1mRekursionstiefe: {depth}\x1b[0m\n\n{repr}') @@ -126,7 +127,7 @@ def hirschberg_algorithm_step( Costs2, Moves2 = compute_cost_matrix(X = '-' + X2, Y = '-' + Y2); # verbose output hier behandeln (irrelevant für Algorithmus): - if verb != VerboseMode.NONE: + if verb != []: path1, path2 = reconstruct_optimal_path_halves(Costs1=Costs1, Costs2=Costs2, Moves1=Moves1, Moves2=Moves2); repr = display_cost_matrix_halves( Costs1 = Costs1, diff --git a/code/python/src/hirschberg/constants.py b/code/python/src/hirschberg/constants.py index 945339d..2579407 100644 --- a/code/python/src/hirschberg/constants.py +++ b/code/python/src/hirschberg/constants.py @@ -12,8 +12,6 @@ from src.thirdparty.types import *; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ __all__ = [ - 'VerboseMode', - 'DisplayOptions', 'Directions', 'gap_penalty', 'missmatch_penalty', @@ -23,16 +21,6 @@ __all__ = [ # ENUMS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -class VerboseMode(Enum): - NONE = -1; - COSTS = 0; - MOVES = 1; - COSTS_AND_MOVES = 2; - -class DisplayOptions(Enum): - TREE = 0; - ATOMS = 1; - class Directions(Enum): UNSET = -1; # Prioritäten hier setzen diff --git a/code/python/src/hirschberg/display.py b/code/python/src/hirschberg/display.py index 08f47c3..02331d5 100644 --- a/code/python/src/hirschberg/display.py +++ b/code/python/src/hirschberg/display.py @@ -8,6 +8,7 @@ from src.thirdparty.types import *; from src.thirdparty.maths import *; +from models.generated.commands import *; from src.hirschberg.constants import *; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -29,7 +30,7 @@ def represent_cost_matrix( path: List[Tuple[int, int]], X: str, Y: str, - verb: VerboseMode, + verb: List[EnumHirschbergVerbosity], pad: bool = False, ) -> np.ndarray: # NDArray[(Any, Any), Any]: m = len(X); # display vertically @@ -42,28 +43,27 @@ def represent_cost_matrix( table = np.full(shape=(3 + m, 3 + n), dtype=object, fill_value=''); # topmost rows: - table[0, 3:(3+n)] = [str(j) for j in range(n)]; - table[1, 3:(3+n)] = [y for y in Y]; + table[0, 3:(3+n)] = [ f'\x1b[2m{j}\x1b[0m' for j in range(n) ]; + table[1, 3:(3+n)] = [ f'\x1b[1m{y}\x1b[0m' for y in Y ]; table[2, 3:(3+n)] = '--'; # leftmost columns: - table[3:(3+m), 0] = [str(i) for i in range(m)]; - table[3:(3+m), 1] = [x for x in X]; + table[3:(3+m), 0] = [ f'\x1b[2m{i}\x1b[0m' for i in range(m) ]; + table[3:(3+m), 1] = [ f'\x1b[1m{x}\x1b[0m' for x in X ]; table[3:(3+m), 2] = '|'; if pad: table[-3, 3:(3+n)] = '--'; table[3:(3+m), -1] = '|'; - match verb: - case VerboseMode.MOVES: - table[3:(3+m), 3:(3+n)] = '\x1b[2m.\x1b[0m'; + if EnumHirschbergVerbosity.costs in verb: + table[3:(3+m), 3:(3+n)] = Costs.copy(); + if EnumHirschbergVerbosity.moves in verb: for (i, j) in path: - table[3 + i, 3 + j] = '\x1b[31;1m*\x1b[0m'; - case VerboseMode.COSTS | VerboseMode.COSTS_AND_MOVES: - table[3:(3+m), 3:(3+n)] = Costs.copy(); - if verb == VerboseMode.COSTS_AND_MOVES: - for (i, j) in path: - table[3 + i, 3 + j] = f'\x1b[31;4;1m{table[3 + i, 3 + j]}\x1b[0m'; + table[3 + i, 3 + j] = f'\x1b[31;4;1m{table[3 + i, 3 + j]}\x1b[0m'; + elif EnumHirschbergVerbosity.moves in verb: + table[3:(3+m), 3:(3+n)] = '\x1b[2m.\x1b[0m'; + for (i, j) in path: + table[3 + i, 3 + j] = '\x1b[31;1m*\x1b[0m'; return table; @@ -72,7 +72,7 @@ def display_cost_matrix( path: List[Tuple[int, int]], X: str, Y: str, - verb: VerboseMode, + verb: EnumHirschbergVerbosity, ) -> str: ''' Zeigt Kostenmatrix + optimalen Pfad. @@ -99,7 +99,7 @@ def display_cost_matrix_halves( X2: str, Y1: str, Y2: str, - verb: VerboseMode, + verb: EnumHirschbergVerbosity, ) -> str: ''' Zeigt Kostenmatrix + optimalen Pfad für Schritt im D & C Hirschberg-Algorithmus diff --git a/code/python/src/travel/__init__.py b/code/python/src/setup/__init__.py similarity index 100% rename from code/python/src/travel/__init__.py rename to code/python/src/setup/__init__.py diff --git a/code/python/src/setup/config.py b/code/python/src/setup/config.py new file mode 100644 index 0000000..6b053f4 --- /dev/null +++ b/code/python/src/setup/config.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# IMPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +from src.thirdparty.misc import *; +from src.thirdparty.config import *; +from src.thirdparty.code import *; +from src.thirdparty.types import *; + +from models.generated.config import *; +from models.generated.commands import *; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# EXPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +__all__ = [ + 'INFO', + 'OPTIONS', + 'COMMANDS', +]; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# CONSTANTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PATH_ASSETS_CONFIG: str = 'assets/config.yaml'; +PATH_ASSETS_COMMANDS: str = 'assets/commands.yaml'; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# LAZY LOADED RESOURCES +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def load_assets_config(path: str) -> Config: # pragma: no cover + with open(path, 'r') as fp: + assets = yaml_load(fp, Loader=yaml_FullLoader); + assert isinstance(assets, dict); + return Config(**assets); + +def create_commands(path: str) -> List[Command]: # pragma: no cover + commands = []; + with open(path, 'r') as fp: + assets = yaml_load(fp, Loader=yaml_FullLoader); + for command in assets: + match Command(**command).name: + case EnumAlgorithmNames.tarjan: + commands.append(CommandTarjan(**command)); + case EnumAlgorithmNames.tsp: + commands.append(CommandTsp(**command)); + case EnumAlgorithmNames.hirschberg: + commands.append(CommandHirschberg(**command)); + return commands; + +# use lazy loaing to ensure that values only loaded (once) when used +CONFIG: Config = lazy(load_assets_config, path=PATH_ASSETS_CONFIG); +INFO: Info = lazy(lambda x: x.info, CONFIG); +OPTIONS: AppOptions = lazy(lambda x: x.options, CONFIG); +COMMANDS: List[Command] = lazy(create_commands, path=PATH_ASSETS_COMMANDS); diff --git a/code/python/src/thirdparty/code.py b/code/python/src/thirdparty/code.py new file mode 100644 index 0000000..441b58a --- /dev/null +++ b/code/python/src/thirdparty/code.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# IMPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +from functools import wraps; +from functools import partial; +from dataclasses import dataclass; +from dataclasses import field; +from dataclasses import Field; +from dataclasses import asdict; +from dataclasses import MISSING; +from itertools import product as itertools_product; +# cf. https://github.com/mplanchard/safetywrap +from safetywrap import Ok; +from safetywrap import Err; +from safetywrap import Nothing; +from safetywrap import Result; +from safetywrap import Option; +from safetywrap import Some; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# EXPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +__all__ = [ + 'partial', + 'wraps', + 'asdict', + 'dataclass', + 'field', + 'Field', + 'MISSING', + 'itertools_product', + 'Err', + 'Nothing', + 'Ok', + 'Option', + 'Result', + 'Some', +]; diff --git a/code/python/src/thirdparty/config.py b/code/python/src/thirdparty/config.py index 93f1cc5..e2358c8 100644 --- a/code/python/src/thirdparty/config.py +++ b/code/python/src/thirdparty/config.py @@ -6,10 +6,12 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import json; +import jsonschema; +from lazy_load import lazy; from yaml import add_constructor; -from yaml import load; -from yaml import Loader; -from yaml import FullLoader; +from yaml import load as yaml_load; +from yaml import FullLoader as yaml_FullLoader; +from yaml import add_path_resolver as yaml_add_path_resolver; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EXPORTS @@ -17,8 +19,10 @@ from yaml import FullLoader; __all__ = [ 'json', + 'jsonschema', + 'lazy', 'add_constructor', - 'load', - 'Loader', - 'FullLoader', + 'yaml_load', + 'yaml_FullLoader', + 'yaml_add_path_resolver', ]; diff --git a/code/python/src/tsp/__init__.py b/code/python/src/tsp/__init__.py new file mode 100644 index 0000000..6837097 --- /dev/null +++ b/code/python/src/tsp/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# IMPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +from src.tsp.algorithms import *; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# EXPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +__all__ = [ + 'tsp_algorithm', +]; diff --git a/code/python/src/travel/naive.py b/code/python/src/tsp/algorithms.py similarity index 95% rename from code/python/src/travel/naive.py rename to code/python/src/tsp/algorithms.py index f98a54f..93275e5 100644 --- a/code/python/src/travel/naive.py +++ b/code/python/src/tsp/algorithms.py @@ -5,7 +5,6 @@ # IMPORTS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -from __future__ import annotations; from src.thirdparty.types import *; from src.thirdparty.maths import *; @@ -14,15 +13,15 @@ from src.thirdparty.maths import *; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ __all__ = [ - 'tsp_naive_algorithm', + 'tsp_algorithm', ]; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# METHOD tsp_naive_algorithm +# METHOD tsp_algorithm # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -def tsp_naive_algorithm( - dist: NDArray[(Any, Any), float], +def tsp_algorithm( + dist: np.ndarray, # NDArray[(Any, Any), float], optimise = min, verbose: bool = False, ) -> tuple[float, list[list[int]]]: