fixed some bugs, added dockerfile for running on windows. output files must be extracted manually.

This commit is contained in:
Tom Zuidberg
2026-06-22 17:27:42 +02:00
parent b96e5f7e38
commit 8e0d2842af
4 changed files with 119 additions and 78 deletions

11
code/Dockerfile Normal file
View File

@@ -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"]

View File

@@ -2,8 +2,10 @@ from functools import partial
from types import FunctionType from types import FunctionType
from typing import Callable, Optional from typing import Callable, Optional
import numpy as np
from simple_term_menu import TerminalMenu from simple_term_menu import TerminalMenu
from simulator import BooleanNetwork from simulator import BooleanNetwork
from typing_extensions import Concatenate
def ShowMenu(options: list[str], title: str = "", highlight_entry: int = 0) -> int: 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] return selectedIndex # pyright: ignore[reportReturnType]
def Continue() -> None: def Continue(title="") -> None:
ShowMenu(options=[None, "Continue"]) ShowMenu(title=title, options=[None, "Continue"], highlight_entry=1)
print()
# def MainMenu() -> None: # def MainMenu() -> None:
@@ -47,70 +50,56 @@ def MainMenu() -> None:
def SelectExampleMenu() -> None: def SelectExampleMenu() -> None:
def bn4nodeSync() -> None: def bn4nodeSync() -> None:
bn = ( functions = [
BooleanNetwork(4) lambda x1, x2, x3, x4: not x2,
.UseSynchronousScheme() lambda x1, x2, x3, x4: x1,
.SetFunctions( lambda x1, x2, x3, x4: x1 ^ x4,
[ lambda x1, x2, x3, x4: x3,
lambda x1, x2, x3, x4: not x2, ]
lambda x1, x2, x3, x4: x1, bn = BooleanNetwork(4).UseSynchronousScheme().SetFunctions(functions)
lambda x1, x2, x3, x4: x1 ^ x4,
lambda x1, x2, x3, x4: x3,
]
)
)
global currentFunction global currentFunction
funcStrings = ["not x2", "x1", "x1 ^ x4", "x3"] funcStrings = ["not x2", "x1", "x1 ^ x4", "x3"]
currentFunction = partial(BooleanNetworkMenu, bn, funcStrings) currentFunction = partial(BooleanNetworkMenu, bn, functions, funcStrings)
def bn4nodeSeq() -> None: 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 = ( bn = (
BooleanNetwork(4) BooleanNetwork(4)
.UseSequentialScheme([1, 2, 3, 4]) .UseSequentialScheme([1, 2, 3, 4])
.SetFunctions( .SetFunctions(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,
]
)
) )
global currentFunction global currentFunction
funcStrings = ["not x2", "x1", "x1 ^ x4", "x3"] funcStrings = ["not x2", "x1", "x1 ^ x4", "x3"]
currentFunction = partial(BooleanNetworkMenu, bn, funcStrings) currentFunction = partial(BooleanNetworkMenu, bn, functions, funcStrings)
def bn3nodeSync() -> None: def bn3nodeSync() -> None:
bn = ( functions = [
BooleanNetwork(3) lambda x1, x2, x3: not x3,
.UseSynchronousScheme() lambda x1, x2, x3: not x1,
.SetFunctions( lambda x1, x2, x3: not x2,
[ ]
lambda x1, x2, x3: not x3, bn = BooleanNetwork(3).UseSynchronousScheme().SetFunctions(functions)
lambda x1, x2, x3: not x1,
lambda x1, x2, x3: not x2,
]
)
)
global currentFunction global currentFunction
funcStrings = ["not x3", "not x1", "not x2"] funcStrings = ["not x3", "not x1", "not x2"]
currentFunction = partial(BooleanNetworkMenu, bn, funcStrings) currentFunction = partial(BooleanNetworkMenu, bn, functions, funcStrings)
def bn3nodeSeq() -> None: def bn3nodeSeq() -> None:
functions = [
lambda x1, x2, x3: not x3,
lambda x1, x2, x3: not x1,
lambda x1, x2, x3: not x2,
]
bn = ( bn = (
BooleanNetwork(3) BooleanNetwork(3).UseSequentialScheme([1, 2, 3]).SetFunctions(functions)
.UseSequentialScheme([1, 2, 3])
.SetFunctions(
[
lambda x1, x2, x3: not x3,
lambda x1, x2, x3: not x1,
lambda x1, x2, x3: not x2,
]
)
) )
global currentFunction global currentFunction
funcStrings = ["not x3", "not x1", "not x2"] funcStrings = ["not x3", "not x1", "not x2"]
currentFunction = partial(BooleanNetworkMenu, bn, funcStrings) currentFunction = partial(BooleanNetworkMenu, bn, functions, funcStrings)
def ReturnHelper() -> None: def ReturnHelper() -> None:
global currentFunction global currentFunction
@@ -148,22 +137,23 @@ def MainMenu() -> None:
def BooleanNetworkMenu( 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: ) -> None:
global currentFunction global currentFunction
boolNetwork: BooleanNetwork = BooleanNetwork(3) boolNetwork: BooleanNetwork = BooleanNetwork(3)
verbose: bool = False writeToFile: bool = False
done: bool = False done: bool = False
functions: list = [None for _ in range(3)] functions: list = [None for _ in range(3)]
functionStrings: list = [None for _ in range(3)] functionStrings: list = [None for _ in range(3)]
functionsDirtyFlag = False functionsDirtyFlag = False
current_highlight = 0 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 boolNetwork = bn
verbose = False functions = funcs
functions = [None for _ in range(3)]
functionStrings = funcStrings functionStrings = funcStrings
def Menu() -> None: def Menu() -> None:
@@ -178,10 +168,11 @@ def BooleanNetworkMenu(
for i in range(1, boolNetwork.size + 1) for i in range(1, boolNetwork.size + 1)
], ],
None, None,
f"Toggle verbose update: (current: {verbose})", "Update once (hold ENTER for continuous updates)",
"Update once",
"Update multiple times", "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, None,
"Return to main menu", "Return to main menu",
] ]
@@ -192,14 +183,19 @@ def BooleanNetworkMenu(
SetUpdateSchemeHelper, SetUpdateSchemeHelper,
*[partial(SetFunctionHelper, i) for i in range(boolNetwork.size)], *[partial(SetFunctionHelper, i) for i in range(boolNetwork.size)],
None, None,
ToggleVerboseHelper,
UpdateHelper, UpdateHelper,
MultiUpdateHelper, MultiUpdateHelper,
ToggleWriteToFileHelper,
GetMarkovChainHelper,
GetStableDistrHelper, GetStableDistrHelper,
None, None,
ReturnHelper, ReturnHelper,
] ]
if boolNetwork.updateScheme in ("synchronous", "sequential", None):
options.pop(-3)
actions.pop(-3)
selectedIndex = ShowMenu( selectedIndex = ShowMenu(
options=options, title=title, highlight_entry=current_highlight options=options, title=title, highlight_entry=current_highlight
) )
@@ -235,7 +231,6 @@ def BooleanNetworkMenu(
return return
try: try:
boolNetwork.SetState(state) boolNetwork.SetState(state)
return
except AssertionError as e: except AssertionError as e:
print("Invalid input:", e) print("Invalid input:", e)
@@ -310,7 +305,7 @@ def BooleanNetworkMenu(
print("Error while simulating once:", e) print("Error while simulating once:", e)
def MultiUpdateHelper() -> None: def MultiUpdateHelper() -> None:
nonlocal boolNetwork, verbose, functions, functionsDirtyFlag nonlocal boolNetwork, writeToFile, functions, functionsDirtyFlag
if functionsDirtyFlag: if functionsDirtyFlag:
try: try:
boolNetwork.SetFunctions(functions) boolNetwork.SetFunctions(functions)
@@ -335,17 +330,23 @@ def BooleanNetworkMenu(
except ValueError: except ValueError:
print("Invalid input") print("Invalid input")
try: try:
boolNetwork.Update(n, verbose=verbose) boolNetwork.Update(
if verbose: n, verbose=not writeToFile or n < 1000, writeToFile=writeToFile
Continue() )
print("\n") Continue("Writing to file completed") if writeToFile else Continue()
except Exception as e: except Exception as e:
print("Error while simulating:", 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: def GetStableDistrHelper() -> None:
print(boolNetwork.GetStableProbabilityDistribution()) print(boolNetwork.GetStableProbabilityDistribution())
Continue() Continue()
print("\n") boolNetwork.SetState([False for _ in range(boolNetwork.size)])
def SetFunctionHelper(index: int) -> None: def SetFunctionHelper(index: int) -> None:
nonlocal boolNetwork, functions, functionStrings, functionsDirtyFlag nonlocal boolNetwork, functions, functionStrings, functionsDirtyFlag
@@ -374,9 +375,9 @@ def BooleanNetworkMenu(
except Exception as e: except Exception as e:
print("Error while parsing function:", e) print("Error while parsing function:", e)
def ToggleVerboseHelper(): def ToggleWriteToFileHelper():
nonlocal verbose nonlocal writeToFile
verbose = not verbose writeToFile = not writeToFile
def ReturnHelper(): def ReturnHelper():
nonlocal done nonlocal done
@@ -391,7 +392,7 @@ def BooleanNetworkMenu(
def main() -> None: def main() -> None:
global currentFunction global currentFunction
currentFunction = MainMenu currentFunction = MainMenu
np.set_printoptions(precision=4, linewidth=300, sign=" ")
while True: while True:
currentFunction() currentFunction()

View File

@@ -1,3 +1,4 @@
scipy scipy
numpy numpy
simple-term-menu simple-term-menu
typing_extensions

View File

@@ -171,6 +171,7 @@ class BooleanNetwork:
if type(state) is str: if type(state) is str:
for i in range(self.size): for i in range(self.size):
self.nodes[i] = bool(int(state[i])) self.nodes[i] = bool(int(state[i]))
self.time_step = 0
return self return self
if isinstance(state, (list, tuple)): 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}" 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.nodes = list(state).copy()
self.time_step = 0
return self return self
raise Exception( raise Exception(
"SetState error: end of function reached. given state is not of type string nor list[bool]." "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, ( assert type(n) is int and n >= 0, (
f"Update error: amount of updates must be an integer and positive. got {n=}" 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_functions, "Update error: no update functions defined"
assert self.__has_update_scheme, "Update error: no update scheme 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(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 selected_update: Callable
@@ -208,11 +213,29 @@ class BooleanNetwork:
case _: case _:
raise Exception("Update error: update scheme selection went wrong") raise Exception("Update error: update scheme selection went wrong")
for _ in range(n): match (verbose, writeToFile):
selected_update() case (False, False):
self.time_step += 1 for _ in range(n):
if verbose: selected_update()
print(self) 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: def __str__(self) -> str:
return f"{self.time_step:>5} | {''.join(str(int(node)) for node in self.nodes)}" 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) return "".join(str(int(node)) for node in self.nodes)
def GetStableProbabilityDistribution(self) -> np.ndarray: def GetStableProbabilityDistribution(self) -> np.ndarray:
matrix: np.ndarray = self.__get_transition_matrix() matrix: np.ndarray = self.GetTransitionMatrix()
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()
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 dimension = 2**self.size
matrix: np.ndarray = np.zeros((dimension, dimension)) matrix: np.ndarray = np.zeros((dimension, dimension))