class Graph(object): '''The graph. Contains all our nodes.''' def __init__(self): # ID -> node self._nodes = {} self._root_node_id = None self._selected_nodes = set() # Nodes self.node_added = Signal() self.node_removed = Signal() self.node_parent_changed = Signal() # Parameters self.parameter_added = Signal() self.parameter_removed = Signal() self.parameter_sink_changed = Signal() self.parameter_source_changed = Signal() # Connections self.connection_added = Signal() self.connection_removed = Signal() # node attributes self.node_id_changed = Signal() self.node_label_changed = Signal() self.node_attributes_changed = Signal() self.node_pos_changed = Signal() self.node_selected_changed = Signal() self.node_name_changed = Signal() self.node_added.connect(self._node_added_callback) self.node_removed.connect(self._node_removed_callback) self.node_selected_changed.connect( self._node_selected_changed_callback) @property def nodes(self): return self._nodes.values() @nodes.setter def nodes(self, new_nodes): cur_nodes = set(self._nodes.values()) new_nodes = set(new_nodes) # Don't delete the root node! new_nodes.add(self.root_node) nodes_to_add = new_nodes.difference(cur_nodes) nodes_to_remove = cur_nodes.difference(new_nodes) for node in nodes_to_add: node.graph = self for node in nodes_to_remove: node.graph = None @nodes.deleter def nodes(self): self.clear() def create_node(self, *args, **kwargs): # convenience kwargs['graph'] = self return node.Node(*args, **kwargs) def clear(self): self.nodes = [] @property def selected_nodes(self): return set(self._selected_nodes) def __getitem__(self, node_id): return self._nodes.get(node_id, None) def __contains__(self, node_id): return node_id in self._nodes @property def selected_nodes(self): return set(self._selected_nodes) @selected_nodes.setter def selected_nodes(self, selected_nodes): selected_nodes = set(selected_nodes) nodes_to_select = selected_nodes.difference(self._selected_nodes) nodes_to_unselect = self._selected_nodes.difference(selected_nodes) for node in nodes_to_select: node.selected = True for node in nodes_to_unselect: node.selected = False def clear_selection(self): for node in self.get_selected_nodes(): node.set_selected(False) def _uniquefy_node_id(self, node_id): if node_id not in self: return node_id # The node_id can be either a string or int (or anything else you want, # but we need to be able to make a unique type) if type(node_id) == str: attempt = 0 unique_node_id = node_id while unique_node_id in self: attempt += 1 unique_node_id = node_id + '_' + str(attempt) return unique_node_id elif type(node_id) == int or type(node_id) == float: while node_id in self: node_id += 1 return node_id print('Warning! Unable to unquefy node_id type %s' % str(type(node_id))) return node_id @property def root_node(self): if self._root_node_id is None: self._root_node_id = self._uniquefy_node_id('root') self._root_node = node.SubgraphNode(node_id=self._root_node_id, graph=self) return self._root_node return self[self._root_node_id] def _node_added_callback(self, node): node_id = node.node_id if node_id in self: node_id = self._uniquefy_node_id(node_id) node.node_id = node_id if node.parent is None and node_id != self._root_node_id: node.parent = self.root_node self._nodes[node_id] = node def _node_removed_callback(self, node): if node.node_id not in self: return if node.node_id == self._root_node_id: print('Warning: deleting root node') self._root_node_id = None del self._nodes[node.node_id] def _node_selected_changed_callback(self, node): if not node.selected and node in self.selected_nodes: self._selected_nodes.remove(node) elif node.selected and node not in self.selected_nodes: self._selected_nodes.add(node)
class Parameter(parentable.Parentable, connectable.Connectable): def __init__(self, label=None, parameter_id=0, parent=None, index=0, node=None, source=False, sink=False): super(Parameter, self).__init__() if parent is not None: if parent.node is not node and node is not None: print("Warning: Specified node differs from parent's node. " "Using parent's node") node = parent.node # Properties self._parameter_id = parameter_id self._label = label self._inbex = index # Attached node self._node = node # Outward and inward connections self._connections_out = set() self._connections_in = set() # property signals self.parameter_id_changed = Signal() self.label_changed = Signal() self.index_changed = Signal() self.connection_added = Signal() self.connection_removed = Signal() self.sink_changed = Signal() self.source_changed = Signal() self.parent = parent # TODO: this needs to fill in the other sink/source value # self.connectionModeChanged = Signal() # self.sink_changed.connect(self.connectionModeChanged.emit) # self.source_changed.connect(self.connectionModeChanged.emit) self.source = source self.sink = sink self.register_callbacks() if self._node is not None: self._node.parameter_added.emit(parameter=self, param=self) def delete(self): self.parent = None self.node = None def register_callbacks(self): if self._node is None: return self.connection_added.connect(self.node.connection_added.emit, parameter=self, param=self) self.connection_removed.connect(self.node.connection_removed.emit, parameter=self, param=self) self.sink_changed.connect(self.node.parameter_sink_changed.emit, parameter=self, param=self) self.source_changed.connect(self.node.parameter_source_changed.emit, parameter=self, param=self) def deregister_callbacks(self): if self._node is None: return self.connection_added.disconnect(self.node.connection_added.emit) self.connection_removed.disconnect(self.node.connection_removed.emit) self.sink_changed.disconnect(self.node.parameter_sink_changed.emit) self.source_changed.disconnect(self.node.parameter_source_changed.emit) def __repr__(self): '''fake convenience repr''' node_str = 'No node' if self.node is not None: node_str = 'Node: %s' % repr(self.node) return '<%s: %s/%s (%s)>' % (self.__class__.__name__, repr(self.parameter_id), self.label, node_str) @property def parameter_id(self): return self._parameter_id @parameter_id.setter def parameter_id(self, new_val): self._parameter_id = new_val self.parameter_id_changed.emit(parameter_id=self._parameter_id) @parameter_id.deleter def parameter_id(self): del self._parameter_id @property def label(self): return self._label @label.setter def label(self, new_val): self._label = new_val self.label_changed.emit(label=self._label) @label.deleter def label(self): del self._label @property def index(self): return self._index @index.setter def index(self, new_val): self._index = new_val self.index_changed.emit(index=self._index) @index.deleter def index(self): del self._index @property def node(self): return self._node @node.setter def node(self, new_node): if self._parent is not None: self.parent = None if self._node is not None: self.deregister_callbacks() self.parameter_removed.emit(parameter=self, param=self) self._node = new_node self.register_callbacks() self.parameter_added.emit(parameter=self, param=self) def is_transitively_connected(self, param): # TODO: two-sided BFS a la path tracing return param in self.connections @property def connection_subgraph(self): return self.node.parent def loft(self): # In ambiguous cases, loft the sink if self.sink: return self.loft_sink() if self.source: return self.loft_source() def loft_sink(self): if not self.sink: return None if self.node.parent is None: return None for tunnel_param in self.node.parent.tunnel_parameters: if self in tunnel_param.connections: return self.node.parent.get_parameter_for_tunnel(tunnel_param) lofted_param = Parameter(node=self.node.parent) lofted_param.sink = True self.node.parent.get_tunnel_parameter(lofted_param).connect(self) return lofted_param def loft_source(self): if not self.source: return None if self.node.parent is None: return None for tunnel_param in self.node.parent.tunnel_parameters: if self in tunnel_param.connections: return self.node.parent.get_parameter_for_tunnel(tunnel_param) lofted_param = Parameter(node=self.node.parent) lofted_param.source = True self.node.parent.get_tunnel_parameter(lofted_param).connect(self) return lofted_param def connect(self, param): # Easy case, connection in the same subgraph if self.connection_subgraph == param.connection_subgraph: return super(Parameter, self).connect(param) # Hard case, different parents # Loft up the one with more ancestors. # (in the case of equal length but different ancestors just choose # one and the other will be lofted in recursion) # Determine directionality # In ambiguous cases, prefer that we are the source self_is_source = True if self.source and param.sink: self_is_source = True elif self.sink and param.source: self_is_source = False else: print('unable to connect') deep_param = self surface_param = param deep_is_source = self_is_source if len(self.node.ancestors) < len(param.node.ancestors): deep_param = param surface_param = self deep_is_source = not self_is_source if deep_is_source: lofted_param = deep_param.loft_source() else: lofted_param = deep_param.loft_sink() if lofted_param is not None: return lofted_param.connect(surface_param)