diff --git a/simra.py b/simra.py index 4c9cf3807a2e339940baba1b61f2a8916f7c659f..06185d3aa3e0a5f3bab83534923176f50b83ff72 100755 --- a/simra.py +++ b/simra.py @@ -2,198 +2,160 @@ import tkinter as TK import collections -flatten = lambda l: [item for sublist in l for item in sublist] - -class Mergeable_dict(dict): +def dict_filter(dictionary, keys): + """ + Return a subdictionary of `dictionary` comprising only the given `keys`. """ - Subclass of dict, except that mergeable_dict += dict shall update all - entries of foo with the corresponding values in bar. - This requires that bar.keys() is a subset of foo.keys(), otherwise - IndexError is raised. + return dict((key, dictionary[key]) for key in keys) + +def flatten(l): + """Given a list of lists l, concatenate all elements of l.""" + return [item for sublist in l for item in sublist] + +class Node: """ - def __iadd__(self, other): - for k in other.keys(): - if not k in self.keys(): raise IndexError() - self[k] = other[k] - return self + An Automata Network Node. -# BUG: changing the UP manually (not inc/dec) will break the assumption -# that an.unstep(); an.step() is no-op. -# The other way around, an.step(); an.unstep() is always working though. + Constructor fields: + name -- Any hashable value, used for printing and identification + update_func -- Update Function; it must take one parameter: a dictionary + mapping name of neighbours to their values + dependencies -- List of dependency names (other nodes' names) + init_state -- The initial value of the node (may be any value) + + Other fields: + state -- Curent value of the node + """ + + def __init__(self, name, update_func=None, dependencies=[], init_state=0): + self.name = name + self.update_func = update_func + self.dependencies = dependencies + self.init_state = init_state + self.reset_state() # Sets self.state + + def reset_state(self): + self.state = self.init_state.copy() + + def add_dependency(self, new_dependency): + self.dependencies.append(new_dependency) + + def update(self, neigh_vals): + self.state = self.update_func(neigh_vals) class AN: """ An automata network. - Fields: - nodes -- set node "names", that can be arbitrary (hashable) values - dependencies -- dict mapping nodes to lists/sets of nodes, encoding the - dependency graph of the network - update_funcs -- dict mapping nodes to functions; each function takes a - dictionary (neighbour node→current value) as sole argument - (it is possible to do my_an.update_funcs += dict to merge - in a dictionary, i.e. to change the functions of only some - nodes) - update_mode -- list of sets (or list of lists) interpreted as a periodic - update mode - init_state -- dict mapping nodes to values, that tells which values - should be used when the network is (re)initialized - state -- dict nodes→values holding the current state (value of each - node) of the network - history -- circular buffer of the last self.history_len states - traversed by the network - up -- Update Pointer; an index for self.update_mode; it - “points” to the next update to do + Constructor fields: + nodes -- A set (or list…) of nodes. + update_mode -- A list of sets (or set-like objects) of node names, viewed + as a periodic update mode. + history_len -- How many states to remember for the rollback feature. + This field may be altered later, and the history will be + resized accordingly. + + Other fields: + up -- Stands for Update Pointer. + It is an index in update_mode thant “points” to the next update + to perform. Note that foo.up can be publicly read and written. + A modulo len(update_mode) is implicitly applied, so it is safe + to do, e.g., foo.up +=1 on an update. + history -- A circular buffer of deep copies of the previous values of _node + (see below). + It can be publicly accessed. Please do not alter the history + by yourself (Stalin did nothing right). + _nodes -- A dictionary mapping node names to nodes. + When the field `nodes` is read, it is actually generated + on-the-fly as the set of items of _nodes. Conversely when + `nodes` is written, a dictionary is implicitly generated and + written to `_nodes`. """ - def __init__(self, nodes, dependencies, update_funcs, update_mode, init_state=None, history_len=10): - self.nodes = set(nodes) - self.dependencies = dependencies - self.update_funcs = update_funcs + + def __init__(self, nodes={}, update_mode=[], history_len=10): + self.nodes = nodes self.update_mode = update_mode - self.init_state = init_state # None is properly handled by a property - self.reinit_state() # Sets self.state, self.up + self.up = 0 self.history = collections.deque(maxlen=history_len) - + # ################################################################ - - @property - def update_funcs(self): - return self._update_funcs - - @update_funcs.setter - def update_funcs(self, new_update_funcs): - if not self.nodes.issubset(set(new_update_funcs.keys())): - raise ValueError("update function is missing for some nodes") - self._update_funcs = Mergeable_dict() - self.set_multiple_update_funcs(self, new_update_funcs) - def set_multiple_update_funcs(self, new_update_funcs): - """ - If update_funcs is a dictionary node→function, change the update - function of each node to the corresponding function. All nodes in - update_funcs.keys() need to exist; but not all nodes from the network - need to be changed by this operation. - """ - for n in keys(new_update_funcs): - self.set_update_func(n, new_update_funcs[n]) - - def set_update_func(self, node, new_update_func): - """Set the update function of a node.""" - if not node in self.nodes: - raise ValueError("trying to set update_func of nonexisting node") - self._update_funcs[node] = new_update_func - @property - def dependencies(self): - return self._dependencies + def nodes(self): + """Return the set of all nodes.""" + return self._nodes.items() - @dependencies.setter - def dependencies(self, new_dependencies): - if not self.nodes == set(new_dependencies.keys()): - raise ValueError("""trying to set a dependency for a nonexistent - node, or missing a dependency list for a node.""") - self._dependencies = new_dependencies + @nodes.setter + def nodes(self, new_nodes): + self._nodes = {} + for node in new_nodes: self.add_node(node) - @property - def update_mode(self): - return self._update_mode - - @update_mode.setter - def update_mode(self, new_update_mode): - """ - Change the update mode. - update_mode needs to be a list of sets (or list of lists) and is - interpreted as a periodic update mode. - This resets up to 0 and clears the history as side-effects. - """ - if not set(flatten(new_update_mode)).issubset(self.nodes): - raise ValueError("update_mode tries to update a nonexistent node") - self._update_mode = new_update_mode - self.up = 0 - self.history.clear() + def add_node(self, node): + """Adds a new node to the AN.""" + self._nodes[node.name] = node - @property - def init_state(self): - return self._init_state - - @init_state.setter - def init_state(self, new_init_state): + def del_node(self, node_name): """ - Change initial state. - Initial state is used when reinitializing the network. - If init_state is None, `0` for each node is used; otherwise, init_state - must be a dictionary node->value, where init_state.keys() is equal to - the set of nodes of the network. + Remove a node from the AN (by node name). Returns the deleted node. + If no node with such name is found, raises KeyError. """ - if new_init_state: - if set(new_init_state.keys()) != self.nodes: - raise ValueError("""init_state either misses state for a node -or has a sate for a nonexistent node""") - self._init_state = new_init_state.copy() - else: - self._init_state = {} - for n in self.nodes: - self._init_state[n] = 0 + return self._nodes.pop(node_name) @property def up(self): return self._up - + @up.setter def up(self, new_up): - self._up = new_up % len(self.history) - + self._up = new_up % len(self.update_mode) + @property def history_len(self): return len(self.history) - + @history_len.setter def history_len(self, new_history_len): new_history = collections.deque(new_history_len) for i in range(min(new_history_len, self.history_len)): new_history.append(self.history[-(i+1)]) self.history = new_history - + # ################################################################ - def reinitialize(self): - """ - Reset the current state to a copy of the initial state, - and the UP to 0. - """ - self.history.append(self.state) - self.state = self.init_state.copy() + def reset_state(self): + """Set each node to its initial value, and the up to 0.""" + new_nodes = self._nodes.deep_copy() + for node in new_nodes: + node.reset_state() + + self.history.append(self._nodes) + self._nodes = new_nodes self.up = 0 - def step(self): - """Execute one step of the AN.""" - new_state = self.state.copy() - for node in self.update_mode[self.up]: - local_view = dict((neighbour, self.state[neighbour]) for neighbour in self.dependencies[node]) - new_state[n] = (self.update_func[n])(self.state) + def update(self): + """Update the AN once.""" + new_nodes = self._nodes.deep_copy() + for n in self.update_mode[self.up]: + local_view = dict_filter(self.nodes, self.nodes[n].dependencies) + new_nodes[n].update(local_view) - self.history.append(self.state) - self.state = new_state + self.history.append(self.nodes) + self._nodes = new_nodes self.up += 1 - def run(self, steps): - """Execute `steps` steps of the AN.""" - for i in range(steps): - self.step() - - def unstep(self): + def unupdate(self): """Roll back one step of the AN, within limits of history_len.""" - self.state = self.history.pop() + self._nodes = self.history.pop() self.up -= 1 - - # ################################################################ - - def __iter__(self): - return self.nodes - - def __call__(self): - self.step() - return self.state.copy() + + def run(self, steps): + """Update the AN `steps` times. If `steps` is negative, roll back.""" + if(steps < 0): + for i in range(-steps): + self.unupdate() + else: + for i in range(steps): + self.update() # ################################################################