I am working with an undirected, weighted graph. It contains two types of nodes, called "A-type" and "B-type". I am starting with a set
of tuples of the following form that describe the graph edges:
(string: Name of First Node, string: Name of Second Node, float: Weight)
Example graph:
My goal is to reduce the graph by removing any nodes of B-type that have exactly two neighbors, and their associated edges, and replacing them with a single edge that has a weight equal to the sum of the weights of removed edges. The reduced version of the example graph would look like this:
And printing the set of edges after reduction would produce this:
{('a1', 'a2', 1.1),
('a1', 'a4', 1.0),
('a1', 'a5', 1.1),
('a2', 'a3', 0.4),
('a3', 'a4', 2.3),
('a3', 'b5', 1.6),
('a3', 'b6', 3.0),
('a4', 'a5', 1.2),
('a4', 'b5', 1.7),
('a5', 'b5', 1.8)}
Note that...
- Nodes B6 and B5 were not removed, even though they are B-type nodes, because they do not have exactly two neighbors.
- A2 was not removed because it is not a B-type node
- The weights of the new edges are the sum of the weights of the removed edges.
import re
import pprint
import operator
import collections
from itertools import chain
def series_reduction(edge_set):
"""
Reduces a set of graph edges by eliminating B-type nodes with two neighbors
Nodes of type B are extracted from the nodes in the set of edges, and become
candidates for removal if they have only two neighbors. The set of removal
candidates is iterated over, and each node it contains becomes the target
node. The target node's edges are removed and replaced with a new edge that
connects the target node's neighbors to each other. The weight of the new
edge is equal to the sum of the removed edges' weights.
@param edge_set: graph edges to perform series reduction on
@type edge_set: set
@rtype: set
"""
neighbor_counter = collections.defaultdict(int)
# identify nodes of B type and count their neighbors
for node in chain.from_iterable(
(node1, node2)
for node1, node2, w in edge_set):
if re.search('\Ab', node):
neighbor_counter[node] += 1
# identify nodes that should be removed because they only have two neighbors
reduce_nodes = set(
node
for node, occurences in neighbor_counter.items()
if occurences == 2)
# create final edge set - initialize with edges that have no nodes to be
# reduced
final_edge_set = set(
(node1, node2, weight)
for node1, node2, weight in edge_set
if node1 not in reduce_nodes and node2 not in reduce_nodes)
# create the set of edges that need to be reduced
reduce_edge_set = edge_set - final_edge_set
# iterate over nodes that will be removed
for target_node in reduce_nodes:
# find the edges with the target node
target_edges = set(
(node1, node2, weight)
for node1, node2, weight in reduce_edge_set
if node1 == target_node or node2 == target_node)
# find the nodes from those edges that are NOT the node we're removing
first_nodes, second_nodes, weights = zip(*target_edges)
end_node1, end_node2 = [
node
for node in first_nodes+second_nodes
if node != target_node]
# removed the old edges from the set of edges to be reduced
for edge in target_edges:
reduce_edge_set.remove(edge)
# add the new edge to the set of remaining edges - it may contain nodes
# that need to be reduced further, so we can't put it in the final set
# yet
reduce_edge_set.add((end_node1, end_node2, sum(weights)))
# were done removing edges, so update the final set with the reduced edges
final_edge_set.update(reduce_edge_set)
return final_edge_set
I am looking for suggestions regarding:
- Any optimizations - memory usage, runtime, length of code required, etc
- Pythonic code, idioms, PEP8 guidelines
- Maintainability
- Any other suggestions
Note
This code is going to be part of a company-internal project, so I had to remove descriptions of what the actual node types are, and have described them as "A-type" and "B-type". The only differences between this code and what will be used are the regular expression for determining node type, and the node type references in the docstring/comments.