diff --git a/code/Dockerfile b/code/Dockerfile new file mode 100644 index 0000000..6df9a97 --- /dev/null +++ b/code/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.14-slim + +WORKDIR /app + +COPY ./main.py /app +COPY ./simulator.py /app +COPY ./requirements.txt /app + +RUN pip install -r requirements.txt + +CMD ["python", "main.py"] diff --git a/code/main.py b/code/main.py index 590d22f..731d311 100644 --- a/code/main.py +++ b/code/main.py @@ -2,8 +2,10 @@ from functools import partial from types import FunctionType from typing import Callable, Optional +import numpy as np from simple_term_menu import TerminalMenu from simulator import BooleanNetwork +from typing_extensions import Concatenate def ShowMenu(options: list[str], title: str = "", highlight_entry: int = 0) -> int: @@ -22,8 +24,9 @@ def ShowMenu(options: list[str], title: str = "", highlight_entry: int = 0) -> i return selectedIndex # pyright: ignore[reportReturnType] -def Continue() -> None: - ShowMenu(options=[None, "Continue"]) +def Continue(title="") -> None: + ShowMenu(title=title, options=[None, "Continue"], highlight_entry=1) + print() # def MainMenu() -> None: @@ -47,70 +50,56 @@ def MainMenu() -> None: def SelectExampleMenu() -> None: def bn4nodeSync() -> None: - bn = ( - BooleanNetwork(4) - .UseSynchronousScheme() - .SetFunctions( - [ - lambda x1, x2, x3, x4: not x2, - lambda x1, x2, x3, x4: x1, - lambda x1, x2, x3, x4: x1 ^ x4, - lambda x1, x2, x3, x4: x3, - ] - ) - ) + functions = [ + lambda x1, x2, x3, x4: not x2, + lambda x1, x2, x3, x4: x1, + lambda x1, x2, x3, x4: x1 ^ x4, + lambda x1, x2, x3, x4: x3, + ] + bn = BooleanNetwork(4).UseSynchronousScheme().SetFunctions(functions) global currentFunction funcStrings = ["not x2", "x1", "x1 ^ x4", "x3"] - currentFunction = partial(BooleanNetworkMenu, bn, funcStrings) + currentFunction = partial(BooleanNetworkMenu, bn, functions, funcStrings) def bn4nodeSeq() -> None: + functions = [ + lambda x1, x2, x3, x4: not x2, + lambda x1, x2, x3, x4: x1, + lambda x1, x2, x3, x4: x1 ^ x4, + lambda x1, x2, x3, x4: x3, + ] bn = ( BooleanNetwork(4) .UseSequentialScheme([1, 2, 3, 4]) - .SetFunctions( - [ - lambda x1, x2, x3, x4: not x2, - lambda x1, x2, x3, x4: x1, - lambda x1, x2, x3, x4: x1 ^ x4, - lambda x1, x2, x3, x4: x3, - ] - ) + .SetFunctions(functions) ) global currentFunction funcStrings = ["not x2", "x1", "x1 ^ x4", "x3"] - currentFunction = partial(BooleanNetworkMenu, bn, funcStrings) + currentFunction = partial(BooleanNetworkMenu, bn, functions, funcStrings) def bn3nodeSync() -> None: - bn = ( - BooleanNetwork(3) - .UseSynchronousScheme() - .SetFunctions( - [ - lambda x1, x2, x3: not x3, - lambda x1, x2, x3: not x1, - lambda x1, x2, x3: not x2, - ] - ) - ) + functions = [ + lambda x1, x2, x3: not x3, + lambda x1, x2, x3: not x1, + lambda x1, x2, x3: not x2, + ] + bn = BooleanNetwork(3).UseSynchronousScheme().SetFunctions(functions) global currentFunction funcStrings = ["not x3", "not x1", "not x2"] - currentFunction = partial(BooleanNetworkMenu, bn, funcStrings) + currentFunction = partial(BooleanNetworkMenu, bn, functions, funcStrings) def bn3nodeSeq() -> None: + functions = [ + lambda x1, x2, x3: not x3, + lambda x1, x2, x3: not x1, + lambda x1, x2, x3: not x2, + ] bn = ( - BooleanNetwork(3) - .UseSequentialScheme([1, 2, 3]) - .SetFunctions( - [ - lambda x1, x2, x3: not x3, - lambda x1, x2, x3: not x1, - lambda x1, x2, x3: not x2, - ] - ) + BooleanNetwork(3).UseSequentialScheme([1, 2, 3]).SetFunctions(functions) ) global currentFunction funcStrings = ["not x3", "not x1", "not x2"] - currentFunction = partial(BooleanNetworkMenu, bn, funcStrings) + currentFunction = partial(BooleanNetworkMenu, bn, functions, funcStrings) def ReturnHelper() -> None: global currentFunction @@ -148,22 +137,23 @@ def MainMenu() -> None: def BooleanNetworkMenu( - bn: Optional[BooleanNetwork] = None, funcStrings: Optional[list[str]] = None + bn: Optional[BooleanNetwork] = None, + funcs: Optional[list[Callable[Concatenate[bool, ...], bool]]] = None, + funcStrings: Optional[list[str]] = None, ) -> None: global currentFunction boolNetwork: BooleanNetwork = BooleanNetwork(3) - verbose: bool = False + writeToFile: bool = False done: bool = False functions: list = [None for _ in range(3)] functionStrings: list = [None for _ in range(3)] functionsDirtyFlag = False current_highlight = 0 - if bn is not None and funcStrings is not None: + if bn is not None and funcStrings is not None and funcs is not None: boolNetwork = bn - verbose = False - functions = [None for _ in range(3)] + functions = funcs functionStrings = funcStrings def Menu() -> None: @@ -178,10 +168,11 @@ def BooleanNetworkMenu( for i in range(1, boolNetwork.size + 1) ], None, - f"Toggle verbose update: (current: {verbose})", - "Update once", + "Update once (hold ENTER for continuous updates)", "Update multiple times", - "Get stationairy distribution", + f"Write to file (works only with multi-update) (current: {writeToFile})", + "Get Markov-Chain matrix (resets timeStep and state)", + "Get stationairy distribution (resets timeStep and state)", None, "Return to main menu", ] @@ -192,14 +183,19 @@ def BooleanNetworkMenu( SetUpdateSchemeHelper, *[partial(SetFunctionHelper, i) for i in range(boolNetwork.size)], None, - ToggleVerboseHelper, UpdateHelper, MultiUpdateHelper, + ToggleWriteToFileHelper, + GetMarkovChainHelper, GetStableDistrHelper, None, ReturnHelper, ] + if boolNetwork.updateScheme in ("synchronous", "sequential", None): + options.pop(-3) + actions.pop(-3) + selectedIndex = ShowMenu( options=options, title=title, highlight_entry=current_highlight ) @@ -235,7 +231,6 @@ def BooleanNetworkMenu( return try: boolNetwork.SetState(state) - return except AssertionError as e: print("Invalid input:", e) @@ -310,7 +305,7 @@ def BooleanNetworkMenu( print("Error while simulating once:", e) def MultiUpdateHelper() -> None: - nonlocal boolNetwork, verbose, functions, functionsDirtyFlag + nonlocal boolNetwork, writeToFile, functions, functionsDirtyFlag if functionsDirtyFlag: try: boolNetwork.SetFunctions(functions) @@ -335,17 +330,23 @@ def BooleanNetworkMenu( except ValueError: print("Invalid input") try: - boolNetwork.Update(n, verbose=verbose) - if verbose: - Continue() - print("\n") + boolNetwork.Update( + n, verbose=not writeToFile or n < 1000, writeToFile=writeToFile + ) + Continue("Writing to file completed") if writeToFile else Continue() except Exception as e: print("Error while simulating:", e) + def GetMarkovChainHelper() -> None: + nonlocal boolNetwork + print(boolNetwork.GetTransitionMatrix()) + Continue() + boolNetwork.SetState([False for _ in range(boolNetwork.size)]) + def GetStableDistrHelper() -> None: print(boolNetwork.GetStableProbabilityDistribution()) Continue() - print("\n") + boolNetwork.SetState([False for _ in range(boolNetwork.size)]) def SetFunctionHelper(index: int) -> None: nonlocal boolNetwork, functions, functionStrings, functionsDirtyFlag @@ -374,9 +375,9 @@ def BooleanNetworkMenu( except Exception as e: print("Error while parsing function:", e) - def ToggleVerboseHelper(): - nonlocal verbose - verbose = not verbose + def ToggleWriteToFileHelper(): + nonlocal writeToFile + writeToFile = not writeToFile def ReturnHelper(): nonlocal done @@ -391,7 +392,7 @@ def BooleanNetworkMenu( def main() -> None: global currentFunction currentFunction = MainMenu - + np.set_printoptions(precision=4, linewidth=300, sign=" ") while True: currentFunction() diff --git a/code/requirements.txt b/code/requirements.txt index 52158a0..085c333 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -1,3 +1,4 @@ scipy numpy simple-term-menu +typing_extensions diff --git a/code/simulator.py b/code/simulator.py index a882027..3b6717b 100644 --- a/code/simulator.py +++ b/code/simulator.py @@ -171,6 +171,7 @@ class BooleanNetwork: if type(state) is str: for i in range(self.size): self.nodes[i] = bool(int(state[i])) + self.time_step = 0 return self if isinstance(state, (list, tuple)): @@ -178,19 +179,23 @@ class BooleanNetwork: f"SetState error: given state list contains elements of type different from bool. All elements must be bools. got {state}" ) self.nodes = list(state).copy() + self.time_step = 0 return self raise Exception( "SetState error: end of function reached. given state is not of type string nor list[bool]." ) - def Update(self, n: int = 1, /, verbose=False) -> None: + def Update( + self, n: int = 1, /, verbose: bool = False, writeToFile: bool = False + ) -> None: assert type(n) is int and n >= 0, ( f"Update error: amount of updates must be an integer and positive. got {n=}" ) assert self.__has_update_functions, "Update error: no update functions defined" assert self.__has_update_scheme, "Update error: no update scheme defined" assert type(verbose) is bool, "Update error: verbose must be a bool" + assert type(writeToFile) is bool, "Update error: writeToFile must be a bool" selected_update: Callable @@ -208,11 +213,29 @@ class BooleanNetwork: case _: raise Exception("Update error: update scheme selection went wrong") - for _ in range(n): - selected_update() - self.time_step += 1 - if verbose: - print(self) + match (verbose, writeToFile): + case (False, False): + for _ in range(n): + selected_update() + self.time_step += 1 + case (False, True): + with open("output.txt", "w") as f: + for _ in range(n): + selected_update() + self.time_step += 1 + f.writelines([self.state, "\n"]) + case (True, False): + for _ in range(n): + selected_update() + self.time_step += 1 + print(self) + case (True, True): + with open("output.txt", "w") as f: + for _ in range(n): + selected_update() + self.time_step += 1 + f.writelines([self.state, "\n"]) + print(self) def __str__(self) -> str: return f"{self.time_step:>5} | {''.join(str(int(node)) for node in self.nodes)}" @@ -222,13 +245,18 @@ class BooleanNetwork: return "".join(str(int(node)) for node in self.nodes) def GetStableProbabilityDistribution(self) -> np.ndarray: - matrix: np.ndarray = self.__get_transition_matrix() - eigenvalues, eigenvectors = scipy.linalg.eig(matrix.T) - stationary_vector = np.real(eigenvectors[:, np.isclose(eigenvalues, 1)]) - stationary_distribution = stationary_vector / np.sum(stationary_vector) - return stationary_distribution.flatten() + matrix: np.ndarray = self.GetTransitionMatrix() - def __get_transition_matrix(self) -> np.ndarray: + eigenvalues, eigenvectors = scipy.linalg.eig(matrix.T) + + idx = np.argmin(np.abs(eigenvalues - 1.0)) + pi = eigenvectors[:, idx].real + + pi = pi / pi.sum() + + return pi + + def GetTransitionMatrix(self) -> np.ndarray: dimension = 2**self.size matrix: np.ndarray = np.zeros((dimension, dimension))