def generalized_degree(self, g: MultiGraph, f: set, active_node, node) -> (int, set):
        assert g.has_node(node), "Calculating gd for node which is not in g!"

        k = set(g.neighbors(node))
        k.remove(active_node)
        k = k.intersection(f)

        gx = self.compress(g, k, node)

        neighbors = list(gx.neighbors(node))
        neighbors.remove(active_node)

        return len(neighbors), neighbors
Example #2
0
def generalized_degree(g: MultiGraph, f: set, active_node, node) -> (int, set):
	assert g.has_node(node), "Calculating gd for node which is not in g!"

	k = set(g.neighbors(node))
	k.remove(active_node)
	k = k.intersection(f)

	gx = compress(g, k, node)

	neighbors = gx.neighbors(node)
	neighbors.remove(active_node)

	return (len(neighbors), neighbors)
class ShuttlingGraph(list):
    def __init__(self, shuttlingEdges=list() ):
        super(ShuttlingGraph, self).__init__(shuttlingEdges) 
        self.currentPosition = None
        self.currentPositionName = None
        self.nodeLookup = dict()
        self.currentPositionObservable = Observable()
        self.graphChangedObservable = Observable()
        self.initGraph()
        self._hasChanged = True
        
    def initGraph(self):
        self.shuttlingGraph = MultiGraph()
        for edge in self:
            self.shuttlingGraph.add_node(edge.startName)
            self.shuttlingGraph.add_node(edge.stopName)
            self.shuttlingGraph.add_edge(edge.startName, edge.stopName, key=hash(edge), edge=edge,
                                         weight=abs(edge.stopLine-edge.startLine))
            self.nodeLookup[edge.startLine] = edge.startName
            self.nodeLookup[edge.stopLine] = edge.stopName

    def rgenerateNodeLookup(self):
        self.nodeLookup.clear()
        for edge in self:
            self.nodeLookup[edge.startLine] = edge.startName
            self.nodeLookup[edge.stopLine] = edge.stopName

    @property
    def hasChanged(self):
        return self._hasChanged
    
    @hasChanged.setter
    def hasChanged(self, value):
        self._hasChanged = value
            
    def position(self, line):
        return self.nodeLookup.get(line)
    
    def setPosition(self, line):
        if self.currentPosition!=line:
            self.currentPosition = line
            self.currentPositionName = self.position(line)
            self.currentPositionObservable.fire( line=line, text=firstNotNone(self.currentPositionName, "") )

    def getMatchingPosition(self,graph):
        """Try to match node name/position to the current settings in the provided ShuttlingGraph."""
        if not graph:
            return self.currentPosition # no change
        # Matching node name. Need to set the corresponding position
        for edge in self:
            if edge.startName == graph.currentPositionName:
                return edge.startLine
            if edge.stopName == graph.currentPositionName:
                return edge.stopLine
        #if graph.currentPosition:
        #    return graph.currentPosition #just use the graph's position
        return self.currentPosition

    def addEdge(self, edge):
        self._hasChanged = True
        self.append(edge)
        self.shuttlingGraph.add_edge(edge.startName, edge.stopName, key=hash(edge), edge=edge, weight=abs(edge.stopLine-edge.startLine))
        self.nodeLookup[edge.startLine] = edge.startName
        self.nodeLookup[edge.stopLine] = edge.stopName
        self.graphChangedObservable.firebare()
        self.setPosition(self.currentPosition)
            
    def isValidEdge(self, edge):
        return ((edge.startLine not in self.nodeLookup or self.nodeLookup[edge.startLine] == edge.startName)
                and (edge.stopLine not in self.nodeLookup or self.nodeLookup[edge.stopLine] == edge.stopName))
        
    def getValidEdge(self):
        index = 0
        while self.shuttlingGraph.has_node("Start_{0}".format(index)):
            index += 1
        startName = "Start_{0}".format(index)
        index = 0
        while self.shuttlingGraph.has_node("Stop_{0}".format(index)):
            index += 1
        stopName = "Stop_{0}".format(index)
        index = 0
        startLine = (max( self.nodeLookup.keys() )+1) if self.nodeLookup else 1
        stopLine = startLine + 1
        return ShuttleEdge(startName, stopName, startLine, stopLine, 0, 0, 0, 0)
    
    def removeEdge(self, edgeno):
        self._hasChanged = True
        edge = self.pop(edgeno)
        self.shuttlingGraph.remove_edge(edge.startName, edge.stopName, hash(edge))
        if self.shuttlingGraph.degree(edge.startName) == 0:
            self.shuttlingGraph.remove_node(edge.startName)
        if self.shuttlingGraph.degree(edge.stopName) == 0:
            self.shuttlingGraph.remove_node(edge.stopName)
        self.graphChangedObservable.firebare()
        self.rgenerateNodeLookup()
        self.setPosition(self.currentPosition)
    
    def setStartName(self, edgeno, startName):
        self._hasChanged = True
        startName = str(startName)
        edge = self[edgeno]
        if edge.startName != startName:
            self.shuttlingGraph.remove_edge(edge.startName, edge.stopName, key=hash(edge))
            if self.shuttlingGraph.degree(edge.startName) == 0:
                self.shuttlingGraph.remove_node(edge.startName)
            edge.startName = startName
            self.shuttlingGraph.add_edge(edge.startName, edge.stopName, key=hash(edge), edge=edge,
                                         weight=abs(edge.stopLine-edge.startLine) )
            self.graphChangedObservable.firebare()
            self.setPosition(self.currentPosition)
            self.rgenerateNodeLookup()
        return True
    
    def setStopName(self, edgeno, stopName):
        self._hasChanged = True
        stopName = str(stopName)
        edge = self[edgeno]
        if edge.stopName != stopName:
            self.shuttlingGraph.remove_edge(edge.startName, edge.stopName, key=hash(edge))
            if self.shuttlingGraph.degree(edge.stopName) == 0:
                self.shuttlingGraph.remove_node(edge.stopName)
            edge.stopName = stopName
            self.shuttlingGraph.add_edge(edge.startName, edge.stopName, key=hash(edge), edge=edge,
                                         weight=abs(edge.stopLine-edge.startLine) )
            self.graphChangedObservable.firebare()
            self.rgenerateNodeLookup()
            self.setPosition(self.currentPosition)
        return True
    
    def setStartLine(self, edgeno, startLine):
        self._hasChanged = True
        edge = self[edgeno]
        if startLine != edge.startLine and (startLine not in self.nodeLookup or self.nodeLookup[startLine] == edge.startName):
            self.nodeLookup.pop(edge.startLine)
            edge.startLine = startLine
            self.shuttlingGraph.edge[edge.startName][edge.stopName][hash(edge)]['weight'] = abs(edge.stopLine-edge.startLine)
            self.rgenerateNodeLookup()
            self.graphChangedObservable.firebare()
            self.setPosition(self.currentPosition)
            return True    
        return False  
    
    def setStopLine(self, edgeno, stopLine):
        self._hasChanged = True
        edge = self[edgeno]
        if stopLine != edge.stopLine and (stopLine not in self.nodeLookup or self.nodeLookup[stopLine] == edge.stopName):
            self.nodeLookup.pop(edge.stopLine)
            edge.stopLine = stopLine
            self.shuttlingGraph.edge[edge.startName][edge.stopName][hash(edge)]['weight'] = abs(edge.stopLine-edge.startLine)
            self.rgenerateNodeLookup()
            self.graphChangedObservable.firebare()
            self.setPosition(self.currentPosition)
            return True  
        return False
    
    def setIdleCount(self, edgeno, idleCount):
        self._hasChanged = True
        self[edgeno].idleCount = idleCount
        return True      

    def setSteps(self, edgeno, steps):
        self._hasChanged = True
        self[edgeno].steps = steps
        return True      
    
    def shuttlePath(self, fromName, toName ):
        fromName = firstNotNone(fromName, self.currentPositionName)
        fromName = fromName if fromName else self.position(float(self.currentPosition))
        if fromName not in self.shuttlingGraph:
            raise ShuttlingGraphException("Shuttling failed, origin '{0}' is not a valid shuttling node".format(fromName))
        if toName not in self.shuttlingGraph:
            raise ShuttlingGraphException("Shuttling failed, target '{0}' is not a valid shuttling node".format(toName))
        sp = shortest_path(self.shuttlingGraph, fromName, toName)
        path = list()
        for a, b in pairs_iter(sp):
            edge = sorted(self.shuttlingGraph.edge[a][b].values(), key=itemgetter('weight'))[0]['edge']
            path.append((a, b, edge, self.index(edge)))
        return path
    
    def nodes(self):
        return self.shuttlingGraph.nodes()
    
    def toXmlElement(self, root):
        mydict = dict( ( (key, str(getattr(self, key))) for key in ('currentPosition', 'currentPositionName') if getattr(self, key) is not None  ) ) 
        myElement = ElementTree.SubElement(root, "ShuttlingGraph", attrib=mydict )
        for edge in self:
            edge.toXmlElement( myElement )
        return myElement
    
    def setStartType(self, edgeno, Type):
        self._hasChanged = True
        self[edgeno].startType = str(Type)
        return True
    
    def setStopType(self, edgeno, Type):
        self._hasChanged = True
        self[edgeno].stopType = str(Type)
        return True
    
    def setStartLength(self, edgeno, length):
        edge = self[edgeno]
        if length!=edge.startLength:
            if length+edge.stopLength<edge.sampleCount:
                self._hasChanged = True
                edge.startLength = int(length)
            else:
                return False
        return True
    
    def setStopLength(self, edgeno, length):
        edge = self[edgeno]
        if length!=edge.stopLength:
            if edge.startLength+length<edge.sampleCount:
                self._hasChanged = True
                edge.stopLength = int(length)
            else:
                return False
        return True
    
    @staticmethod
    def fromXmlElement( element ):
        edgeElementList = element.findall("ShuttleEdge")
        edgeList = [ ShuttleEdge.fromXmlElement(e) for e in edgeElementList ]
        return ShuttlingGraph(edgeList)
Example #4
0
class MultiEdgeUndirectedTopology(BaseTopology):
    """Class representing a topology with mutltiple undirected edges.

    :param iter nodes: Nodes in the graph
    :param iter edges: Edges in the graph
    """

    # Names used to hold the node longitude and latitude
    NODE_LONG = 'long'
    NODE_LAT = 'lat'

    # Name used to hold the edge type
    EDGE_TYPE = 'type'

    def __init__(self, nodes=None, edges=None):

        self.graph = MultiGraph()

        # Nodes
        nodes = nodes or {}
        for node_id, (lon, lat) in nodes.items():
            self.add_node(node_id, lon, lat)

        # Edges
        edges = edges or {}
        for (n1, n2), (edge_type, dict_attrs) in edges.items():
            self.add_edge(n1, n2, edge_type, **dict_attrs)

    @property
    def num_of_nodes(self):
        """The number of nodes in the topology."""
        return self.graph.number_of_nodes()

    @property
    def num_of_edges(self):
        """The number of nodes in the topology."""
        return self.graph.number_of_edges()

    @property
    def nodes(self):
        """The nodes in the graph."""
        return self.graph.nodes

    def add_node(self, node_id, longitude, latitude, **node_attrs):
        """Add a node defined by its label.

        :param int node_id: Node id.
        :param dict node_attrs: Node attributes.

        :raises:
            :py:class:`RuntimeError`: if a node with the same ID already exists.
        """

        # If there is no node with the same ID already, raise exception
        if self.graph.has_node(node_id):
            raise RuntimeError("Node %s already exists." % node_id)

        # Adding longitude and latitude to attributes
        node_attrs[self.NODE_LONG] = longitude
        node_attrs[self.NODE_LAT] = latitude
        # Create node
        self.graph.add_node(node_id, **node_attrs)

    def get_node(self, node_id):
        """Return node from its ID."""

        try:
            return self.graph.nodes[node_id]
        except KeyError:
            raise KeyError("Node %s does not exist." % node_id)

    def distance(self, n1, n2):
        """
        Calculate the distance between two nodes on the Earth.

        :param int n1: ID of the first node.
        :param int n2: ID of the second node.
        :returns: Distance in cm
        :rtype: float
        """
        n1 = self.get_node(n1)
        n2 = self.get_node(n2)

        return utility_distance(
            n1[self.NODE_LONG],
            n1[self.NODE_LAT],
            n2[self.NODE_LONG],
            n2[self.NODE_LAT]
        )

    def add_edge(self, node1, node2, edge_type, **edge_attrs):
        """Add an edge between two nodes in the graph.

        :param obj node1: Label of the first node.
        :param obj node2: Label of the second node.
        :param obj edge_type: Edge type.
        :param dict edge_attrs: Additional attributes of the edge.

        :raises:
            :py:class:`RuntimeError`: if an edge with the same type already exists.
        """

        _ = self.get_node(node1)
        _ = self.get_node(node2)

        # If an edge of the given type between the two nodes already exits: error
        # We take into account that the graph in undirected
        with suppress(KeyError):
            types_existing_edges = [e[self.EDGE_TYPE] == edge_type
                                    for e in self.get_edges(node1, node2).values()]

            if any(types_existing_edges):
                raise RuntimeError(
                    'Already existing edge %s between nodes %s and %s' % (edge_type, node1, node2)
                )

        # Adding type to attributes
        edge_attrs[self.EDGE_TYPE] = edge_type
        # Create edge
        self.graph.add_edge(node1, node2, **edge_attrs)

    def get_edges(self, node1, node2, edge_types=None):
        """Return all the edges between two nodes.

        :param obj node1: Label of the first node.
        :param obj node2: Label of the second node.
        :param iterable edge_types: If provided, return only the edges for these types.
        :returns: A dictionary where the keys are the edge number
            and the values a dictionary of the edges attributes.
        :rtype: dict
        :raises: :py:class:`ValueError`: if there is no edge between the nodes.
        """
        try:
            edges = dict(self.graph[node1][node2])
        except KeyError:
            # undirected graph
            try:
                edges = dict(self.graph[node2][node1])
            except KeyError:
                raise KeyError('No edge between nodes %s and %s' % (node1, node2))

        # Filter if necessary
        if edge_types:
            edges = {e: v for e, v in edges.items()
                     if edges[e][self.EDGE_TYPE] in edge_types}

            # If dict is empty, there is no edge
            if not edges:
                raise KeyError('No edge between nodes %s and %s' % (node1, node2))

        return edges

    def _get_min_weight(self, weight, allowed_types, best_types, node1, node2, _d):
        """
        Find the edge with the minimum weight between two nodes.

        :param str weight: Weight name.
        :param allowed_types: Edge type(s) allowed to build path.
        :type allowed_types: dict-like object with TransportType as keys.
        :param dict best_types: dictionnary containing the best type for each pair of nodes.

        :note: other arguments are the ones needed by the NetworkX API.
        """

        # The algorithm uses the full (symmetric) matrix
        # instead of considering the upper triangular only.
        # This means that for two nodes u ≠ v, the weight is extracted twice:
        # once for (u,v), once for (v,u). We do not need to do the work for both.
        if (node2, node1) in best_types:
            return None

        # Get all edges between the two nodes
        edges = self.get_edges(node1, node2)

        # Get all the weights but keep only those that are allowed
        # This will throw a KeyError is some edges do not have the weight specified
        try:
            weights = [(attrs[self.EDGE_TYPE], attrs[weight]) for attrs in edges.values()
                       if attrs[self.EDGE_TYPE] in allowed_types]
        except KeyError:
            raise KeyError(
                "No edge with attribute %s found between nodes %s and %s" % (
                    weight, node1, node2
                ))

        # If there is no valid edge
        if not weights:
            return None

        # If the weights need to be weighted. Skip if they are all None.
        if list(set(allowed_types.values()))[0]:
            weights = [(t, w * allowed_types[t]) for t, w in weights]

        # Otherwise, save type and return minimum weight
        min_type, min_weight = min(weights, key=lambda t: t[1])
        best_types[(node1, node2)] = min_type
        return min_weight

    def get_shortest_path(self, node1, node2, criterion, allowed_types, edge_data=None):
        """Find the shortest path between two nodes using specified edges.

        :param obj node1: Label of the first node.
        :param obj node2: Label of the second node.
        :param str criterion: Criterion used to find shortest path. Must be an edge attribute.
        :param dict allowed_types: Edge type(s) allowed to build path.
            The keys are the edge types, the values (if any) indicate the preference for each type.
        :param iter(str) edge_data: Edge attributes for which data along the path is requested.
        :returns: A 3-tuple containing in this order

            * the score of the optimal path
            * the list of nodes along the path
            * a dict containing the edge type and values for the attributes specified in edge_data
              (data are stored in numpy arrays)

        :rtype: tuple
        :raises:
            :py:class:`.ValueError`: if nodes dont exists or no path has been found between them.

        :note: If `allowed_types` is a dict-like object, the weight of an edge will be weighted
            by the value of the given edge type.
        """

        # Check that the nodes exist
        for node in node1, node2:
            if not self.graph.has_node(node):
                raise ValueError("Node %s does not exist." % node)

        # Using partial function to pass the edge types
        # The weight_func will be called for each edge on the graph
        # Even those which will not been part of the optimial path
        # So best_types cannot be a simple list to which we append values
        # Instead it will be a dict where the key is a pair of nodes
        # We will extract the relevant values and attributes afterwards
        best_types = {}
        weight_func = partial(self._get_min_weight, criterion, allowed_types, best_types)

        # Calculate path
        try:
            score, path = single_source_dijkstra(
                self.graph, node1, node2, weight=weight_func)
        except NetworkXNoPath:
            raise RuntimeError("No path found with type %s between %s and %s." %
                               (allowed_types, node1, node2))

        # Build the dict containing the attributes data
        # Because we do not want to assume anything about the data,
        # we build an intermediate list first.
        data = {}
        # Always extract the edge types
        edge_types = []
        for u, v in zip(path, path[1:]):
            # The key could be (u,v) or (v,u)
            try:
                edge_types.append(best_types[(u, v)])
            except KeyError:
                edge_types.append(best_types[(v, u)])
        data[self.EDGE_TYPE] = np.array(edge_types)

        # Additional data if needed
        edge_data = edge_data or []
        for attr in edge_data:
            try:
                temp_list = [
                    list(self.get_edges(u, v, [t]).values())[0][attr]
                    for u, v, t in zip(
                        path, path[1:], data[self.EDGE_TYPE])]
                data[attr] = np.array(temp_list)
            except KeyError:
                raise KeyError("Some edges do not have the attribute '%s'." % attr)

        return (score, path, data)

    def add_energy_based_edges(
            self, edge_types, num_edges_per_step, max_iterations,
            degree_energy_factor, distance_energy_factor, rng,
            attribute_name='distance'):
        """
        This algorithm creates edges based on an energy sampling mechanism.
        At each iteration, the total energy (degree energy + potential energy)
        is calculated between all nodes and transformed into a Boltzmann distribution.
        Edges are more likely to be created between nodes which are:

        * close
        * already connected to other nodes

        A fixed number of random connections are then created based on this distribution,
        masking previous connections and self-edges.
        The process is repeated until either the maximum number of iterations is reached,
        or all nodes have been connected at least once to another node.

        :param iter edge_types: Types of the edges to add.
        :param int num_edges_per_step: Number of edges to create at each step
        :param int max_iterations: Maximum number of iterations.
            If None, the algorithm stops when each node has been connected at least once.
        :param float degree_energy_factor: Multiplier applied to the degree enery component
            during the search for new edges (higher means more prominent).
        :param float distance_energy_factor: Multiplier applied to the distance energy component
            during the search for new edges (higher means more prominent).
        :param rng: Random number generator.
        :type rng: :py:class:`RandomGenerator<city_graph.utils.RandomGenerator>`

        :note: The algorithm does take into account already exisiting edges
            to compute the probabilities.
        """

        print("[Topology] Starting energy sampling algorithm to build edges.")

        # Step 1: prepare
        # Compute all distances: graph is undirected so we want all combinations with replacements
        # so we should have in total C(2,n) + n distances
        # TODO: We compute things twice for now, so things might be improved
        array_shape = (self.num_of_nodes,) * 2
        distances = np.zeros(shape=array_shape)
        array_size = distances.size
        for (i, n1), (j, n2) in product(enumerate(self.nodes), repeat=2):
            distances[i, j] = self.distance(n1, n2)

        # Normalize by maximum distance - save scaling factor as it will be needed
        max_distance = distances.max()
        distances /= max_distance

        # Adjacency matrix (not taking into account previously existing edges)
        adjacency_matrix = np.zeros(shape=array_shape)

        # Step 2: sampling
        step = 0
        termination = TerminationReason.NO_TERMINATION
        while termination == TerminationReason.NO_TERMINATION:

            # This loop could be put in a separate function.
            # We will see if it is necessary

            # Sampling step
            # a) compute the degree energies
            degree_energy = -1 / 2 * (
                adjacency_matrix.sum(0, keepdims=True) +
                adjacency_matrix.sum(1, keepdims=True)
            )
            if degree_energy_factor:
                degree_energy = degree_energy_factor * degree_energy

            # b) compute the distance energies
            distance_energy = distances
            if distance_energy_factor:
                distance_energy = distance_energy_factor * distances

            # c) compute edge sampling probability distribution
            total_energy = degree_energy + distance_energy

            # mask existing edges
            total_energy[adjacency_matrix > EPS_PRECISION] = np.inf
            # mask self-edges
            np.fill_diagonal(total_energy, np.inf)

            # compute Boltzmann distribution and normalize it
            bolz_dist = np.exp(-total_energy)
            bolz_dist /= bolz_dist.sum()

            # d) sample step
            # sample indices to activate
            sample_indices = rng.choice(
                np.arange(array_size), size=(num_edges_per_step,),
                p=bolz_dist.ravel(), replace=True)

            # unravel indices - get a tuple
            unravel_indices = np.unravel_index(sample_indices, array_shape)

            # add sample edges to the adjacency matrix
            # and make matric symmetric
            adjacency_matrix[unravel_indices] = 1
            adjacency_matrix[unravel_indices[::-1]] = 1

            # Increase counter
            step += 1

            # Check termination
            # Maximum step number reached
            if max_iterations:
                if step >= max_iterations:
                    termination = TerminationReason.MAX_ITER
            # All nodes are connected
            if adjacency_matrix.sum(0).min() > EPS_PRECISION:
                termination = TerminationReason.ALL_NODES_CONNECTED

        # Inform the user why the algorithm has stopped
        print("[Topology] Sampling finished after", step, "steps. Reason:", termination)

        # Build edges from the adjacency matrix
        # Rescale the distances
        distances *= max_distance

        # Get pair of nodes to connect
        # Here we use indices because we will need them later on for the distances
        pairs = [(i, j) for i, j in zip(*adjacency_matrix.nonzero())]

        # Create edges
        old_num_edges = self.num_of_edges
        node_ids = list(self.nodes)
        for i, j in pairs:
            for edge_type in edge_types:

                # TODO: exception might be raised here because we now check
                # That there is no outgoing/incoming edges.
                # Should we fix when we use an actual triangular matrix
                with suppress(RuntimeError):
                    self.add_edge(node_ids[i], node_ids[j], edge_type,
                                  **{attribute_name: distances[i, j]})

        # Inform that edges have been built
        print("[Topology] %i edges have been created" % (self.num_of_edges - old_num_edges))

    def add_edges_between_centroids(self, edge_types, num_centroids, rng, attribute_name='distance'):
        """
        This algorithm creates edges between central nodes. These central nodes
        are determined by a clustering algorithm (here the Fluid Communities algorithm).

        :param iter edge_types: Types of the edges to add.
        :param int num_centroids: Number of centroids.
        :param rng: Random number generator.
        :type rng: :py:class:`RandomGenerator<city_graph.utils.RandomGenerator>`
        """

        print("[Topology] Starting building edges between %s central nodes." % num_centroids)

        # Calculate clusters
        # TODO: I think it would make sense to instead use e.g. a k-means for two reasons:
        #  * this algo assumes that the clusters have the same density, which is not necessarily
        # application to cities (some areas are more crowded)
        #  *  the implementation needs the graph to be fully connected to begin with. It seems
        # to be a limitation
        clusters = (list(c) for c in asyn_fluidc(
            self.graph, k=num_centroids, seed=rng.rand_int()))

        # Extract centroids: we take the node with the highest degree
        centroids = [c[int(np.argmax([self.graph.degree[n] for n in c]))] for c in clusters]

        # Create temporary graph for the centroids
        tmp_graph = Graph()
        tmp_graph.add_nodes_from(centroids)
        # We need the combinations because the graph is undirected and we dont want self-edges

        for n1, n2 in combinations(centroids, 2):
            tmp_graph.add_edge(n1, n2, **{attribute_name: self.distance(n1, n2)})

        # Calculate subgraph with the minimum sum of edge weights
        # TODO: to investigate why we do this here...
        subgraph = minimum_spanning_tree(tmp_graph, weight=attribute_name)

        # Build edges
        # Here we can reuse the previously calculated distances
        old_num_edges = self.num_of_edges
        for (n1, n2) in subgraph.edges:
            for edge_type in edge_types:

                # TODO: exception might be raised here because we now check
                # That there is no outgoing/incoming edges.
                # Should we fix when we use an actual triangular matrix
                with suppress(RuntimeError):
                    self.add_edge(n1, n2, edge_type, **subgraph[n1][n2])

        # Inform that edges have been built
        print("[Topology] %i edges have been created" % (self.num_of_edges - old_num_edges))