Mini Shell
"""Gathers together a collection of helper functions and classes that the library needs,
but end users won't care about."""
import copy
from ..classes import UndirectedGraph, DirectedGraph
# Graph Conversions
def make_subgraph(graph, vertices, edges):
"""Converts a subgraph given by a list of vertices and edges into a graph object."""
# Copy the entire graph
local_graph = copy.deepcopy(graph)
# Remove all the edges that aren't in the list
edges_to_delete = [x for x in local_graph.get_all_edge_ids() if x not in edges]
for e in edges_to_delete:
local_graph.delete_edge_by_id(e)
# Remove all the vertices that aren't in the list
nodes_to_delete = [x for x in local_graph.get_all_node_ids() if x not in vertices]
for n in nodes_to_delete:
local_graph.delete_node(n)
return local_graph
def convert_graph_directed_to_undirected(dg):
"""Converts a directed graph into an undirected graph. Directed edges are made undirected."""
udg = UndirectedGraph()
# Copy the graph
# --Copy nodes
# --Copy edges
udg.nodes = copy.deepcopy(dg.nodes)
udg.edges = copy.deepcopy(dg.edges)
udg.next_node_id = dg.next_node_id
udg.next_edge_id = dg.next_edge_id
# Convert the directed edges into undirected edges
for edge_id in udg.get_all_edge_ids():
edge = udg.get_edge(edge_id)
target_node_id = edge['vertices'][1]
target_node = udg.get_node(target_node_id)
target_node['edges'].append(edge_id)
return udg
def remove_duplicate_edges_directed(dg):
"""Removes duplicate edges from a directed graph."""
# With directed edges, we can just hash the to and from node id tuples and if
# a node happens to conflict with one that already exists, we delete it
# --For aesthetic, we sort the edge ids so that lower edge ids are kept
lookup = {}
edges = sorted(dg.get_all_edge_ids())
for edge_id in edges:
e = dg.get_edge(edge_id)
tpl = e['vertices']
if tpl in lookup:
dg.delete_edge_by_id(edge_id)
else:
lookup[tpl] = edge_id
def remove_duplicate_edges_undirected(udg):
"""Removes duplicate edges from an undirected graph."""
# With undirected edges, we need to hash both combinations of the to-from node ids, since a-b and b-a are equivalent
# --For aesthetic, we sort the edge ids so that lower edges ids are kept
lookup = {}
edges = sorted(udg.get_all_edge_ids())
for edge_id in edges:
e = udg.get_edge(edge_id)
tpl_a = e['vertices']
tpl_b = (tpl_a[1], tpl_a[0])
if tpl_a in lookup or tpl_b in lookup:
udg.delete_edge_by_id(edge_id)
else:
lookup[tpl_a] = edge_id
lookup[tpl_b] = edge_id
def get_vertices_from_edge_list(graph, edge_list):
"""Transforms a list of edges into a list of the nodes those edges connect.
Returns a list of nodes, or an empty list if given an empty list.
"""
node_set = set()
for edge_id in edge_list:
edge = graph.get_edge(edge_id)
a, b = edge['vertices']
node_set.add(a)
node_set.add(b)
return list(node_set)
def get_subgraph_from_edge_list(graph, edge_list):
"""Transforms a list of edges into a subgraph."""
node_list = get_vertices_from_edge_list(graph, edge_list)
subgraph = make_subgraph(graph, node_list, edge_list)
return subgraph
def merge_graphs(main_graph, addition_graph):
"""Merges an ''addition_graph'' into the ''main_graph''.
Returns a tuple of dictionaries, mapping old node ids and edge ids to new ids.
"""
node_mapping = {}
edge_mapping = {}
for node in addition_graph.get_all_node_objects():
node_id = node['id']
new_id = main_graph.new_node()
node_mapping[node_id] = new_id
for edge in addition_graph.get_all_edge_objects():
edge_id = edge['id']
old_vertex_a_id, old_vertex_b_id = edge['vertices']
new_vertex_a_id = node_mapping[old_vertex_a_id]
new_vertex_b_id = node_mapping[old_vertex_b_id]
new_edge_id = main_graph.new_edge(new_vertex_a_id, new_vertex_b_id)
edge_mapping[edge_id] = new_edge_id
return node_mapping, edge_mapping
def create_graph_from_adjacency_matrix(adjacency_matrix):
"""Generates a graph from an adjacency matrix specification.
Returns a tuple containing the graph and a list-mapping of node ids to matrix column indices.
The graph will be an UndirectedGraph if the provided adjacency matrix is symmetric.
The graph will be a DirectedGraph if the provided adjacency matrix is not symmetric.
Ref: http://mathworld.wolfram.com/AdjacencyMatrix.html"""
if is_adjacency_matrix_symmetric(adjacency_matrix):
graph = UndirectedGraph()
else:
graph = DirectedGraph()
node_column_mapping = []
num_columns = len(adjacency_matrix)
for _ in range(num_columns):
node_id = graph.new_node()
node_column_mapping.append(node_id)
for j in range(num_columns):
for i in range(num_columns):
if adjacency_matrix[j][i]:
jnode_id = node_column_mapping[j]
inode_id = node_column_mapping[i]
# Because of our adjacency matrix encoding, [j][i] in our code corresponds to [i][j] in a traditional matrix interpretation
# Thus, we need to put an edge from node i to node j if [j][i] in our code is non-zero
graph.new_edge(inode_id, jnode_id)
return (graph, node_column_mapping)
def is_adjacency_matrix_symmetric(adjacency_matrix):
"""Determines if an adjacency matrix is symmetric.
Ref: http://mathworld.wolfram.com/SymmetricMatrix.html"""
# Verify that the matrix is square
num_columns = len(adjacency_matrix)
for column in adjacency_matrix:
# In a square matrix, every row should be the same length as the number of columns
if len(column) != num_columns:
return False
# Loop through the bottom half of the matrix and compare it to the top half
# --We do the bottom half because of how we construct adjacency matrices
max_i = 0
for j in range(num_columns):
for i in range(max_i):
# If i == j, we can skip ahead so we don't compare with ourself
if i == j:
continue
# Compare the value in the bottom half with the mirrored value in the top half
# If they aren't the same, the matrix isn't symmetric
if adjacency_matrix[j][i] != adjacency_matrix[i][j]:
return False
max_i += 1
# If we reach this far without returning false, then we know that everything matched,
# which makes this a symmetric matrix
return True
Zerion Mini Shell 1.0