final presentation, restructure of the repository, updated code to have weighted probabilities and more

This commit is contained in:
Tom
2026-07-24 17:33:59 +02:00
parent c129ba7206
commit 1a4e53b30a
20 changed files with 3290 additions and 2969 deletions

View File

@@ -7,6 +7,19 @@ import numpy as np
import scipy.linalg
def _wrap_bool_function(
function: Callable[Concatenate[bool, ...], bool],
) -> Callable[Concatenate[bool, ...], bool]:
def wrap(*args, **kwargs) -> bool:
result = function(*args, **kwargs)
assert type(result) is bool, (
f"Function error: Boolean network functions must always return a bool, however got type {type(result)}, {result=}"
)
return result
return wrap
class BooleanNetwork:
def __init__(self, size: int) -> None:
assert type(size) is int and size > 0, (
@@ -17,8 +30,7 @@ class BooleanNetwork:
self.__has_update_functions = False
self.__has_update_scheme = False
self.__has_sequence = False
self.__has_flip_chance = False
self.flip_chance: float = 0
self.__has_probabilistic_functions = False
self.sequence: list[int] = list()
self.seed: int | None = None
self.time_step = 0
@@ -29,21 +41,23 @@ class BooleanNetwork:
lambda x: x for _ in range(size)
]
# asynchronous_random: relative likelihood that node i is the one picked
# to update on a given time step. Doesn't need to sum to 1 - it is
# normalised (weight_i / sum(weights)) whenever it is used.
self.node_selection_weights: list[float] = [1.0 for _ in range(size)]
# probabilistic: each node may have any number of candidate update
# functions. On every update, one candidate per node is drawn
# according to its weight (again normalised at use-time, not
# required to sum to 1) and applied synchronously.
self.probabilistic_functions: list[
list[Callable[Concatenate[bool, ...], bool]]
] = [list() for _ in range(size)]
self.probabilistic_weights: list[list[float]] = [list() for _ in range(size)]
def SetFunctions(
self, functions: Iterable[Callable[Concatenate[bool, ...], bool]]
) -> Self:
def wrapper(
function: Callable[Concatenate[bool, ...], bool],
) -> Callable[Concatenate[bool, ...], bool]:
def wrap(*args, **kwargs) -> bool:
result = function(*args, **kwargs)
assert type(result) is bool, (
f"Function error: Boolean network functions must always return a bool, however got type {type(result)}, {result=}"
)
return result
return wrap
funcs: list[Callable[Concatenate[bool, ...], bool]] = list(functions)
assert len(funcs) == self.size, (
@@ -56,7 +70,7 @@ class BooleanNetwork:
f"Function error: Function arg amount mismatch. Given function takes {len(inspect.signature(func).parameters)} arguments, expected {self.size}"
)
self.functions[i] = wrapper(func)
self.functions[i] = _wrap_bool_function(func)
self.__has_update_functions = True
return self
@@ -64,22 +78,101 @@ class BooleanNetwork:
def SetFunction(
self, index: int, function: Callable[Concatenate[bool, ...], bool]
) -> Self:
def wrapper(
function: Callable[Concatenate[bool, ...], bool],
) -> Callable[Concatenate[bool, ...], bool]:
def wrap(*args, **kwargs) -> bool:
result = function(*args, **kwargs)
assert type(result) is bool, (
f"Function error: Boolean network functions must always return a bool, however got type {type(result)}, {result=}"
)
return result
return wrap
assert 0 <= index < self.size, (
f"Function error: cannot set function at index {index} - out of bound."
)
self.functions[index] = wrapper(function)
assert len(inspect.signature(function).parameters) == self.size, (
f"Function error: Function arg amount mismatch. Given function takes {len(inspect.signature(function).parameters)} arguments, expected {self.size}"
)
self.functions[index] = _wrap_bool_function(function)
return self
def SetNodeSelectionWeight(self, index: int, weight: float) -> Self:
assert 0 <= index < self.size, (
f"Weight error: cannot set selection weight at index {index} - out of bound."
)
assert type(weight) is float and weight > 0, (
f"Weight error: weight must be a positive float. got {weight=}"
)
self.node_selection_weights[index] = weight
return self
def AddProbabilisticFunction(
self,
index: int,
function: Callable[Concatenate[bool, ...], bool],
weight: float = 1.0,
) -> Self:
assert 0 <= index < self.size, (
f"Function error: cannot add function at index {index} - out of bound."
)
assert len(inspect.signature(function).parameters) == self.size, (
f"Function error: Function arg amount mismatch. Given function takes {len(inspect.signature(function).parameters)} arguments, expected {self.size}"
)
assert type(weight) is float and weight > 0, (
f"Weight error: weight must be a positive float. got {weight=}"
)
self.probabilistic_functions[index].append(_wrap_bool_function(function))
self.probabilistic_weights[index].append(weight)
self.__has_probabilistic_functions = all(
len(functions) > 0 for functions in self.probabilistic_functions
)
return self
def SetProbabilisticFunction(
self,
index: int,
function_index: int,
function: Callable[Concatenate[bool, ...], bool],
) -> Self:
assert 0 <= index < self.size, (
f"Function error: cannot set function at index {index} - out of bound."
)
assert 0 <= function_index < len(self.probabilistic_functions[index]), (
f"Function error: node {index} has no function at position {function_index}."
)
assert len(inspect.signature(function).parameters) == self.size, (
f"Function error: Function arg amount mismatch. Given function takes {len(inspect.signature(function).parameters)} arguments, expected {self.size}"
)
self.probabilistic_functions[index][function_index] = _wrap_bool_function(
function
)
return self
def SetProbabilisticFunctionWeight(
self, index: int, function_index: int, weight: float
) -> Self:
assert 0 <= index < self.size, (
f"Weight error: cannot set weight at index {index} - out of bound."
)
assert 0 <= function_index < len(self.probabilistic_weights[index]), (
f"Weight error: node {index} has no function at position {function_index}."
)
assert type(weight) is float and weight > 0, (
f"Weight error: weight must be a positive float. got {weight=}"
)
self.probabilistic_weights[index][function_index] = weight
return self
def RemoveProbabilisticFunction(self, index: int, function_index: int) -> Self:
assert 0 <= index < self.size, (
f"Function error: cannot remove function at index {index} - out of bound."
)
assert 0 <= function_index < len(self.probabilistic_functions[index]), (
f"Function error: node {index} has no function at position {function_index}."
)
assert len(self.probabilistic_functions[index]) > 1, (
f"Function error: node {index} must keep at least one probabilistic function."
)
del self.probabilistic_functions[index][function_index]
del self.probabilistic_weights[index][function_index]
self.__has_probabilistic_functions = all(
len(functions) > 0 for functions in self.probabilistic_functions
)
return self
def UseSynchronousScheme(self) -> Self:
@@ -127,16 +220,9 @@ class BooleanNetwork:
self.__has_update_scheme = True
return self
def UseProbabilisticScheme(self, flip_chance: float) -> Self:
if flip_chance is not None:
assert type(flip_chance) is float, (
f"Probabilistic error: given flip_chance is not a float: got {flip_chance}"
)
self.flip_chance = flip_chance
def UseProbabilisticScheme(self) -> Self:
self.updateScheme = "probabilistic"
self.__has_update_scheme = True
self.__has_flip_chance = True
return self
def __synchronous_update(self) -> None:
@@ -150,15 +236,21 @@ class BooleanNetwork:
self.nodes[i] = self.functions[i](*self.nodes)
def __asynchronous_random_update(self) -> None:
index = random.randrange(0, self.size)
index = random.choices(
range(self.size), weights=self.node_selection_weights, k=1
)[0]
self.nodes[index] = self.functions[index](*self.nodes)
def __probabilistic_update(self) -> None:
self.__synchronous_update()
temp = list()
for i in range(self.size):
rng = random.random()
if rng <= self.flip_chance:
self.nodes[i] = not self.nodes[i]
chosen = random.choices(
self.probabilistic_functions[i],
weights=self.probabilistic_weights[i],
k=1,
)[0]
temp.append(chosen(*self.nodes))
self.nodes = temp
def SetState(self, state: str | list[bool] | tuple[bool, ...]) -> Self:
assert isinstance(state, (str, list, tuple)), (
@@ -192,7 +284,9 @@ class BooleanNetwork:
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.updateScheme == "probabilistic" or 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"
@@ -208,7 +302,9 @@ class BooleanNetwork:
case "asynchronous_random":
selected_update = self.__asynchronous_random_update
case "probabilistic":
assert self.__has_flip_chance, "Update error: no flip_chance defined"
assert self.__has_probabilistic_functions, (
"Update error: no probabilistic functions defined for every node"
)
selected_update = self.__probabilistic_update
case _:
raise Exception("Update error: update scheme selection went wrong")
@@ -261,36 +357,37 @@ class BooleanNetwork:
matrix: np.ndarray = np.zeros((dimension, dimension))
if self.updateScheme == "probabilistic":
flipChance = self.flip_chance
self.UseSynchronousScheme()
for i, state in enumerate(product((False, True), repeat=self.size)):
self.SetState(state)
self.Update()
for flips in product((False, True), repeat=self.size):
flipped = int(
"".join(
str(
int(
self.nodes[j] if not flips[j] else not self.nodes[j]
)
)
for j in range(self.size)
),
2,
)
choice_ranges = [
range(len(self.probabilistic_functions[n]))
for n in range(self.size)
]
for combo in product(*choice_ranges):
prob = np.float64(1)
for flip in flips:
prob *= flipChance if flip else 1 - flipChance
matrix[i][flipped] = prob
self.UseProbabilisticScheme(flipChance)
result: list[bool] = []
for n in range(self.size):
weights = self.probabilistic_weights[n]
total_weight = sum(weights)
chosen_index = combo[n]
prob *= weights[chosen_index] / total_weight
result.append(
self.probabilistic_functions[n][chosen_index](*state)
)
result_index = int(
"".join(str(int(b)) for b in result), 2
)
matrix[i][result_index] += prob
return matrix
if self.updateScheme == "asynchronous_random":
total_weight = sum(self.node_selection_weights)
for i, state in enumerate(product((False, True), repeat=self.size)):
for j in range(self.size):
self.SetState(state)
self.nodes[j] = self.functions[j](*self.nodes)
matrix[i][int(self.state, 2)] += np.float64(1) / self.size
matrix[i][int(self.state, 2)] += (
np.float64(self.node_selection_weights[j]) / total_weight
)
return matrix
for i, state in enumerate(product((False, True), repeat=self.size)):