From 04cdeb4e184d82a8831f08fbb442c0910d691504 Mon Sep 17 00:00:00 2001 From: raj_mathe Date: Mon, 18 Apr 2022 19:04:42 +0200 Subject: [PATCH] master > master: code-py - tarjan + stacks + logging --- code/python/src/core/__init__.py | 0 code/python/src/core/log.py | 64 +++++++++++ code/python/src/graphs/graph.py | 26 ++++- code/python/src/graphs/tarjan.py | 182 ++++++++++++++++++++++--------- code/python/src/main.py | 28 +++-- code/python/src/stacks/stack.py | 30 +++-- 6 files changed, 254 insertions(+), 76 deletions(-) create mode 100644 code/python/src/core/__init__.py create mode 100644 code/python/src/core/log.py diff --git a/code/python/src/core/__init__.py b/code/python/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/python/src/core/log.py b/code/python/src/core/log.py new file mode 100644 index 0000000..41912cf --- /dev/null +++ b/code/python/src/core/log.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# IMPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +# + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# EXPORTS +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +__all__ = [ + 'log_info', + 'log_debug', + 'log_warn', + 'log_error', + 'log_fatal', +]; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# METHODS logging +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def log_info(*text: str): + ''' + Prints an info message + ''' + for line in text: + print("[\x1b[94;1mINFO\x1b[0m] {}".format(line)); + return; + +def log_debug(*text: str): + ''' + Prints a debug message + ''' + for line in text: + print("[\x1b[96;1mDEBUG\x1b[95;0m] {}".format(line)); + return; + +def log_warn(*text: str): + ''' + Prints a warning message + ''' + for line in text: + print("[\x1b[93;1mWARNING\x1b[0m] {}".format(line)); + return; + +def log_error(*text: str): + ''' + Prints an error message + ''' + for line in text: + print("[\x1b[91;1mERROR\x1b[0m] {}".format(line)); + return; + +def log_fatal(*text: str): + ''' + Prints a fatal error message + crashes + ''' + for line in text: + print("[\x1b[91;1mFATAL\x1b[0m] {}".format(line)); + exit(1); diff --git a/code/python/src/graphs/graph.py b/code/python/src/graphs/graph.py index 5d4c0de..08b3b97 100644 --- a/code/python/src/graphs/graph.py +++ b/code/python/src/graphs/graph.py @@ -5,7 +5,9 @@ # IMPORTS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# +from __future__ import annotations; + +from src.local.typing import *; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EXPORTS @@ -23,22 +25,34 @@ class Graph(object): ''' a data structure for graphs ''' - nodes: list[str]; - edges: list[tuple[str,str]] + nodes: list[Any]; + edges: list[tuple[Any,Any]] - def __init__(self, nodes: list[str], edges: list[tuple[str,str]]): + def __init__(self, nodes: list[Any], edges: list[tuple[Any,Any]]): self.nodes = nodes; self.edges = edges; return; - def successor(self, u: str): + def __len__(self) -> int: + return len(self.nodes); + + def subgraph(self, nodes: list[Any]) -> Graph: + ''' + @returns graph induced by subset of nodes + ''' + return Graph( + nodes = [ u for u in self.nodes if u in nodes ], + edges = [ (u, v) for u, v in self.edges if u in nodes and v in nodes ], + ); + + def successors(self, u: str): ''' @returns list of successor nodes ''' return [ v for (u_, v) in self.edges if u == u_ ]; - def predecessor(self, v: str): + def predecessors(self, v: str): ''' @returns list of predecessor nodes diff --git a/code/python/src/graphs/tarjan.py b/code/python/src/graphs/tarjan.py index 30e79d5..bbc2e26 100644 --- a/code/python/src/graphs/tarjan.py +++ b/code/python/src/graphs/tarjan.py @@ -5,12 +5,17 @@ # IMPORTS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +from __future__ import annotations; from enum import Enum; +from dataclasses import dataclass; +from dataclasses import field +from platform import node; from src.local.typing import *; +from src.core.log import *; from src.stacks.stack import *; -from src.graphs.graph import * +from src.graphs.graph import *; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # EXPORTS @@ -33,59 +38,134 @@ class State(Enum): # Tarjan Algorithm # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -class NodeInformation: - root: int; - index: int; - state: State; - - def __init__(self): - self.root = self.index = 0; - self.state = State.UNTOUCHED; - -def tarjan_algorithm(G: Graph) -> List[Any]: +def tarjan_algorithm(G: Graph, debug: bool = False) -> List[Any]: ''' - runs the Tarjan-Algorithm to compute the strongly connected components + # Tarjan Algorithm # + Runs the Tarjan-Algorithm to compute the strongly connected components. + ''' + # initialise state - mark all nodes as UNTOUCHED: + ctx = Context(G, debug=debug); + # loop through all nodes and carry out Tarjan-Algorithm, provided node not already visitted. + for u in G.nodes: + if ctx.get_state(u) == State.UNTOUCHED: + tarjan_visit(G, u, ctx); + + return ctx.components; + +def tarjan_visit(G: Graph, u: Any, ctx: Context): + ''' + Recursive depth-first search algorithm to compute strongly components of a graph. ''' - # sonst Stack erstellen und Bearbeitungszustand der Knoten im Speicher notieren: - S = Stack(); - index = 0; - infos = { u: NodeInformation() for u in G.nodes }; - components = []; + # Place node on stack + initialise visit-index + component-index. + ctx.max_index += 1; + ctx.push(u); + ctx.set_least_index(u, ctx.max_index); + ctx.set_index(u, ctx.max_index); + ctx.set_state(u, State.PENDING); - def tarjan_visit(v: Any): - ''' - recursive depth-first search algorithm to compute components - ''' - nonlocal G, S, index, components; + ''' + Compute strongly connected components of each child node. + NOTE: Child nodes remain on stack, if and only if parent is in same component. + ''' + for v in G.successors(u): + # Visit child node only if untouched: + if ctx.get_state(v) == State.UNTOUCHED: + tarjan_visit(G, v, ctx); + ctx.set_least_index(u, min(ctx.get_least_index(u), ctx.get_least_index(v))); + # Otherwise update associated component-index of parent node, if in same component as child: + elif ctx.stack_contains(v): + ctx.set_least_index(u, min(ctx.get_least_index(u), ctx.get_index(v))); - if not(infos[v].state == State.UNTOUCHED): + ctx.set_state(u, State.FINISHED); + ctx.log_info(u); + + # If at least-index of component pop everything from stack up to least index and add component: + if ctx.get_index(u) == ctx.get_least_index(u): + component = []; + while True: + v = ctx.top(); + ctx.pop(); + component.append(v); + if u == v: + break; + ctx.components.append(component); + return; + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# AUXILIARY context variables for algorithm +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +@dataclass +class NodeInformationDefault: + node: Any = field(default=None); + least_index: int = field(default=0); + index: int = field(default=0); + state: State = field(default=State.UNTOUCHED, repr=False); + +class NodeInformation(NodeInformationDefault): + def __init__(self, u: Any): + super().__init__(); + self.node = u; + +@dataclass +class ContextDefault: + max_index: int = field(default=0); + debug: bool = field(default=False); + stack: Stack = field(default_factory=lambda: Stack()); + components: list[list[Any]] = field(default_factory=lambda: []); + infos: dict[Any, NodeInformation] = field(default_factory=lambda: dict()); + +class Context(ContextDefault): + def __init__(self, G: Graph, debug: bool): + super().__init__(); + self.debug = debug; + self.infos = { u: NodeInformation(u) for u in G.nodes }; + + def push(self, u: Any): + self.stack.push(u); + + def top(self) -> Any: + return self.stack.top(); + + def pop(self) -> Any: + return self.stack.pop(); + + def update_infos(self, u: Any, info: NodeInformation): + self.infos[u] = info; + + def set_state(self, u: Any, state: State): + info = self.infos[u]; + info.state = state; + self.update_infos(u, info); + + def set_least_index(self, u: Any, least_index: int): + info = self.infos[u]; + info.least_index = least_index; + self.update_infos(u, info); + + def set_index(self, u: Any, index: int): + info = self.infos[u]; + info.index = index; + self.update_infos(u, info); + + def stack_contains(self, u: Any) -> bool: + return self.stack.contains(u); + + def get_info(self, u: Any) -> NodeInformation: + return self.infos[u]; + + def get_state(self, u: Any) -> State: + return self.get_info(u).state; + + def get_least_index(self, u: Any) -> int: + return self.get_info(u).least_index; + + def get_index(self, u: Any) -> int: + return self.get_info(u).index; + + def log_info(self, u: Any): + if not self.debug: return; - - index += 1; - infos[v].index = infos[v].root = index; - S.push(v); - - infos[v].state = State.PENDING; - # depth first search: - for u in G.successor(v): - tarjan_visit(u); - # remains relevant for v, provided u still in Stack: - if u in S: - infos[v].root = min(infos[v].root, infos[u].root); - infos[v].state = State.FINISHED; - - # if at root of component pop everything from stack up to root and add component: - if infos[v].index == infos[v].root: - component = []; - condition_reached_root = False; - while not condition_reached_root: - u = S.pop(); - component.append(u); - condition_reached_root = (u == v); - components.append(component); - return; - - for v in G.nodes: - tarjan_visit(v); - return components; + info = self.get_info(u); + log_debug(info); diff --git a/code/python/src/main.py b/code/python/src/main.py index d6857b1..6f01478 100644 --- a/code/python/src/main.py +++ b/code/python/src/main.py @@ -11,6 +11,7 @@ import sys; os.chdir(os.path.join(os.path.dirname(__file__), '..')); sys.path.insert(0, os.getcwd()); +from src.core.log import *; from src.graphs.graph import *; from src.graphs.tarjan import *; @@ -26,14 +27,27 @@ from src.graphs.tarjan import *; def enter(): ## Beispiel aus Seminarblatt 1 - ## TODO -> schieben in config datei - G = Graph( - nodes=[1,2,3,4,5,6,7], - edges=[(1,2), (1,3), (2,3), (3,4), (4,5), (5,2), (5,6), (5,7), (6,7)], - ); - components = tarjan_algorithm(G); + nodes = [1,2,3,4,5,6,7,8]; + edges = [ + (1,2), + (1,3), + (2,4), + (2,5), + (3,5), + (3,6), + (3,8), + (4,5), + (4,7), + (5,1), + (5,8), + (6,8), + (7,8), + (8,6), + ]; + G = Graph(nodes=nodes, edges=edges); + components = tarjan_algorithm(G, debug=True); for component in components: - print(component); + log_debug(component); return; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/code/python/src/stacks/stack.py b/code/python/src/stacks/stack.py index 85cc025..0481f72 100644 --- a/code/python/src/stacks/stack.py +++ b/code/python/src/stacks/stack.py @@ -23,10 +23,10 @@ class Stack: ''' A data structure for stacks ''' - values: list[Any]; + elements: list[Any]; def __init__(self): - self.values = []; + self.elements = []; return; def __len__(self): @@ -34,16 +34,25 @@ class Stack: @returns number of elements in stack ''' - return len(self.values); + return len(self.elements); def __contains__(self, value: Any) -> bool: - return value in self.values; + return value in self.elements; def push(self, value: Any): ''' add element to stack ''' - self.values.append(value); + self.elements.append(value); + + def top(self) -> Any: + ''' + @returns + top element from stack without removal + ''' + if len(self.elements) == 0: + raise Exception('Stack is empty!'); + return self.elements[-1]; def pop(self) -> Any: ''' @@ -51,14 +60,11 @@ class Stack: top element from stack and removes it ''' value = self.top(); - self.values = self.values[:-1]; + self.elements = self.elements[:-1]; return value; - def top(self) -> Any: + def contains(self, element: Any) -> bool: ''' - @returns - top element from stack without removal + checks if element in stack: ''' - if len(self.values) == 0: - raise Exception('Stack is empty!'); - return self.values[-1]; + return element in self.elements;