diff --git a/simra.py b/simra.py index baf971395d30dd1fbcad759c9793710646fc5e5f..452c5f0323ff098f6c1f1bb550cebdf83bd28303 100755 --- a/simra.py +++ b/simra.py @@ -1,10 +1,12 @@ -/#!/usr/bin/env python3 +#!/usr/bin/env python3 import collections +import copy from functools import reduce from itertools import count import tkinter as TK # ################################################################ +# General utilities def dict_filter(dictionary, keys): """ @@ -12,7 +14,17 @@ def dict_filter(dictionary, keys): """ return dict((key, dictionary[key]) for key in keys) +def integers(n): + """ + Return the list of integers from 0 (included) to n-1 (included). + """ + return list(range(n)) + +def flatten(l): + return [item for sublist in l for item in sublist] + # ################################################################ +# Automata network themselves class Node: """ @@ -37,13 +49,17 @@ class Node: self.reset_state() # Sets self.state def reset_state(self): - self.state = self.init_state.copy() + self.state = self.init_state def add_dependency(self, new_dependency): self.dependencies.append(new_dependency) def update(self, neigh_vals): - self.state = self.update_func(neigh_vals) + f = self.update_func + self.state = f(neigh_vals) + + def __repr__(self): + return "{{node {}: {}}}".format(self.name, self.state) class AN: """ @@ -59,11 +75,11 @@ class AN: Other fields: up -- Stands for Update Pointer. - It is an index in update_mode thant “points” to the next update + It is an index in update_mode that “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 + history -- A circular buffer of deep copies of the previous values of _nodes (see below). It can be publicly accessed. Please do not alter the history by yourself (Stalin did nothing right). @@ -85,12 +101,17 @@ class AN: @property def nodes(self): """Return the set of all nodes.""" - return self._nodes.items() + return self._nodes.values() @nodes.setter def nodes(self, new_nodes): self._nodes = {} for node in new_nodes: self.add_node(node) + + @property + def state(self): + """Return a dictionary mapping node names to node states.""" + return dict((name, self._nodes[name].state) for name in self._nodes.keys()) def add_node(self, node): """Adds a new node to the AN.""" @@ -122,11 +143,18 @@ class AN: new_history.append(self.history[-(i+1)]) self.history = new_history + def __repr__(self): + result = "{AN \n" + for node in self.nodes: + result += " " + str(node) + "\n" + result += "}" + return result + # ################################################################ def reset_state(self): """Set each node to its initial value, and the up to 0.""" - new_nodes = self._nodes.deep_copy() + new_nodes = self._nodes.copy() for node in new_nodes: node.reset_state() @@ -136,9 +164,9 @@ class AN: def update(self): """Update the AN once.""" - new_nodes = self._nodes.deep_copy() + new_nodes = copy.deepcopy(self._nodes) for n in self.update_mode[self.up]: - local_view = dict_filter(self.nodes, self.nodes[n].dependencies) + local_view = dict_filter(self.state, self._nodes[n].dependencies) new_nodes[n].update(local_view) self.history.append(self.nodes) @@ -147,7 +175,7 @@ class AN: def unupdate(self): """Roll back one step of the AN, within limits of history_len.""" - self._nodes = self.history.pop() + self._nodes = copy.deepcopy(self.history.pop()) self.up -= 1 def run(self, steps): @@ -160,6 +188,112 @@ class AN: self.update() # ################################################################ +# Update modes + +def parallel_mode(node_names): + """Return a parallel update mode.""" + return [node_names] + +def local_clocks(period, deltas): + """ + Return a local clocks update mode. + + period -- The global period update. + deltas -- A dictionary mapping node names to phases. + """ + result = [] * global_periods + for k in deltas.keys(): + result[deltas[k]].append(k) + return result + +def is_block_seq(mode): + """ + Return whether an update mode is block sequential + (no node is updated twice). + """ + already_seen = set() + for node_name in flatten(mode): + if node_name in already_seen: return False + else: already_seen.add(name) + return True + +def block_seq(mode): + """ + Checks that `mode` is block-sequential. If so, return it. + If not, raise a ValueError exception. Useful for functoinal programming. + """ + if not is_block_seq(mode): raise ValueError() + return mode + +def periodic_mode(mode): + """ + The identity function, with another name. + Useful for functionnal programming. + """ + return mode + +# ################################################################ +# Trivial nodes + +def node_fixpoint(name, init_val=0): + def identity(args): + for k in args.keys(): + return args[k] + return Node(name, identity, [name], init_val) + +def node_constant(name, value=0, init_val=value): + def constant(args): + nonlocal value + return value + return Node(name, constant, [], init_val) + +# ################################################################ +# Cycle ANs + +def cycle_update_func(sign=True): + """ + Return an update function for a node in a cycle. + All update functions assume only one neighbour. + If sign=True, the returned update function is identity. + If sign=False, the returned update function is negation. + """ + def positive(neighs): + for k in neighs.keys(): + return neighs[k] + + def negative(neighs): + for k in neighs.keys(): + return not neighs[k] + + if sign: return positive + else: return negative + +def cycle(signs, init_vals=None, update_mode=None): + """ + Return a cyclic AN. + Node names are 0,…,n-1, with n=len(signs). Node i depends on node i-1 mod n. + signs -- signs[i] is True if i depends positively on i-1, False if + negatively + init_vals -- list of initial values (booleans); defaults to all True + update_mode -- a periodic update mode; defaults to parallel + """ + num_nodes = len(signs) + node_names = integers(num_nodes) + + if not init_vals: + init_vals = [True]*num_nodes + if not update_mode: + update_mode = parallel_mode(node_names) + + nodes = [] + for (sign, init_val, i) in zip(signs, init_vals, count()): + update_func = cycle_update_func(sign) + node = Node(i, update_func, [(i-1)%num_nodes], init_val) + nodes.append(node) + return AN(nodes, update_mode) + +# ################################################################ +# And-not symmetric networks def land(b1, b2): """Like `and`, but a function (usable with `reduce`)""" @@ -182,19 +316,19 @@ def and_not_update_func(dep_list): def and_not(dep_lists, init_vals, update_mode): """ - Builds an and-not network. + Return an and-not network. Nodes are named 1, …, N where N=len(dep_lists)==len(init_vals). - dep_lists[0] and init_vals[0] are ignored. - dep_lists[i] is the list of neighbours of i (so a list of integers). - A negative neighbour means a negated neighbour. + dep_lists -- dep_lists[0] is ignored; dep_lists[i] is the list of + neighbours of i (so a list of integers). + A negative neighbour means a negated neighbour. + For example if dep_list[1] contains 4, then 1 depends positively on 4; if dep_list[1] contains -4, then 1 depends negatively on 4. - init_vals is a list of booleans, so that init_vals[i] is the initial - value of node i. - - update_mode is a periodic update mode, i.e. a list of sets of integers. + init_vals -- init_vals[0] is ignored; init_vals[i] is the initial value of + node i + update_mode -- periodic update mode, i.e. a list of sets of integers """ nodes = [] for (dep_list, init_val, i) in zip(dep_lists[1:], init_vals[1:], count(1)): @@ -234,12 +368,6 @@ example_update = [ # ################################################################ -def parallel_mode(AN): - """Return a parallel update mode for AN.""" - return [[node.name for node in AN.nodes]] - -# ################################################################ - def button_press(event): print("clicked at ({},{})".format(event.x, event.y))