diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 15075df..b9e82c5 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -4,6 +4,5 @@ numpy>=1.19.5 quantities>=0.14.1 matplotlib>=3.3.2 seaborn>=0.9.0 -bokeh>=3.0.0 holoviews>=1.16.0 networkx>=3.0.0 \ No newline at end of file diff --git a/viziphant/patterns.py b/viziphant/patterns.py index 26a2afc..3ebc734 100644 --- a/viziphant/patterns.py +++ b/viziphant/patterns.py @@ -399,7 +399,8 @@ def plot_patterns(spiketrains, patterns, circle_sizes=(3, 50, 70), axes.yaxis.set_label_coords(-0.01, 0.5) return axes -def plot_patterns_hypergraph(patterns, num_neurons=None): +def plot_patterns_hypergraph(patterns, pattern_size=None, num_neurons=None,\ + must_involve_neuron=None, node_size=3, node_color='white', node_linewidth=1): """ Hypergraph visualization of spike patterns. @@ -429,11 +430,22 @@ def plot_patterns_hypergraph(patterns, num_neurons=None): :func:`elephant.spade.spade` or :func:`elephant.cell_assembly_detection.cell_assembly_detection` pattern detectors. + node_size (optional): int + Change the size of the drawen nodes + pattern_size (optional): range + Only draw patterns that are in range of pattern_size num_neurons: None or int If None, only the neurons that are part of a pattern are shown. If an integer is passed, it identifies the total number of recorded neurons including non-pattern neurons to be additionally shown in the graph. Default: None + must_involve_neuron (optional) : int + Highlight pattern which includes neuron x + node_color (optional) : String + change the color of the nodes + + node_linewidth (optional) : int + change the line width of the nodes Returns ------- @@ -461,8 +473,7 @@ def plot_patterns_hypergraph(patterns, num_neurons=None): bst.rescale('ms') patterns = cell_assembly_detection(bst, max_lag=2) - fig = viziphant.patterns.plot_patterns_hypergraph(patterns) - plt.show() + viziphant.patterns.plot_patterns_hypergraph(patterns) """ # If only patterns of a single dataset are given, wrap them in a list to @@ -496,8 +507,15 @@ def plot_patterns_hypergraph(patterns, num_neurons=None): # Create one hyperedge from every pattern for pattern in patterns: # A hyperedge is the set of neurons of a pattern - hyperedges.append(pattern['neurons']) - + if pattern_size is None or len(pattern['neurons']) in pattern_size: + hyperedges.append(pattern['neurons']) + + if must_involve_neuron is not None and isinstance(must_involve_neuron, int): + hyperedges = [edge for edge in hyperedges if must_involve_neuron in edge] + + elif must_involve_neuron is not None and isinstance(must_involve_neuron, list): + hyperedges = [edge for edge in hyperedges if any(elem in edge for elem in must_involve_neuron)] + # Currently, all hyperedges receive the same weights weights = [weight] * len(hyperedges) @@ -507,9 +525,9 @@ def plot_patterns_hypergraph(patterns, num_neurons=None): weights=weights, repulse=repulsive) hypergraphs.append(hg) - - view = View(hypergraphs) - fig = view.show(subset_style=VisualizationStyle.COLOR, - triangulation_style=VisualizationStyle.INVISIBLE) + view = View(hypergraphs=hypergraphs, node_size=node_size, + node_color=node_color, node_linewidth=node_linewidth) + fig = view.show(subset_style=VisualizationStyle.styles['color'], + triangulation_style=VisualizationStyle.styles['invisible']) return fig diff --git a/viziphant/patterns_src/hypergraph.py b/viziphant/patterns_src/hypergraph.py index 6a2a0fe..9e00cb1 100644 --- a/viziphant/patterns_src/hypergraph.py +++ b/viziphant/patterns_src/hypergraph.py @@ -104,15 +104,23 @@ def complete_and_star_associated_graph(self): edges = [] weights = [] graph_vertices = list(self.vertices.copy()) + if isinstance(self.vertices[0], int): + max_vertex = max(max(hyperedge) for hyperedge in self.hyperedges) + else: + max_vertex = 0 + if isinstance(max_vertex, str): + max_vertex = 0 for i, hyperedge in enumerate(self.hyperedges): # Pseudo-vertex corresponding to hyperedge - graph_vertices.append(-i - 1) + pseudo_vertex = max_vertex + i + 1 + graph_vertices.append(pseudo_vertex) + for j, vertex in enumerate(hyperedge): # Every vertex of a hyperedge is adjacent to the pseudo-vertex # corresponding to the hyperedge - edges.append([-i - 1, vertex]) - # Weight is equal to the weight of the hyperedge (if - # applicable) + edges.append([pseudo_vertex, vertex]) + + # Weight is equal to the weight of the hyperedge (if applicable) if self.weights: weights.append(self.weights[i]) # Unique unordered combinations of vertices of this hyperedge diff --git a/viziphant/patterns_src/view.py b/viziphant/patterns_src/view.py index 82e1a6a..d1e4859 100644 --- a/viziphant/patterns_src/view.py +++ b/viziphant/patterns_src/view.py @@ -11,6 +11,7 @@ from holoviews import opts from holoviews.streams import Pipe import numpy as np +import matplotlib.pyplot as plt from viziphant.patterns_src.hypergraph import Hypergraph @@ -18,9 +19,12 @@ class VisualizationStyle: - INVISIBLE = 1 - NOCOLOR = 2 - COLOR = 3 + styles = { + 'invisible': 1, + 'nocolor': 2, + 'color': 3 + } + class View: @@ -34,7 +38,11 @@ class View: for the visualization of hypergraphs. """ - def __init__(self, hypergraphs, title=None): + # Class variables to save data + node_data = [] + polygons_data = [] + + def __init__(self, hypergraphs, node_size=5, node_color='white', node_linewidth=1, title=None): """ Constructs a View object that handles the visualization of the given hypergraphs. @@ -44,6 +52,15 @@ def __init__(self, hypergraphs, title=None): hypergraphs: list of Hypergraph objects Hypergraphs to be visualized. Each hypergraph should contain data of one data set. + + node_size (optional) : int + Size of the nodes in the Hypergraphs + + node_color (optional) : String + change the color of the nodes + + node_linewidth (optional) : int + change the line width of the nodes """ # Hyperedge drawings @@ -52,8 +69,17 @@ def __init__(self, hypergraphs, title=None): # Which color of the color map to use next self.current_color = 1 - # Size of the vertices - self.node_radius = 0.2 + # radius of the hyperedges + self.node_radius = .2 + + # Size of the nodes (vertices of hypergraph) + self.node_size = node_size + + # Color of the nodes + self.node_color = node_color + + # Width of the Node lines + self.node_linewidth = node_linewidth # Selected title of the figure self.title = title @@ -80,135 +106,12 @@ def __init__(self, hypergraphs, title=None): # Set up the visualizations and interaction widgets that need to be # displayed - self.dynamic_map, self.pipe = self._setup_graph_visualization() - self.dynamic_map_edges, self.edges_pipe = \ - self._setup_hyperedge_drawing() self.plot = None - def _setup_graph_visualization(self): - """ - Set up the holoviews DynamicMap object - that visualizes the nodes of a graph - Returns - ------- - dynamic_map: hv.DynamicMap - The DynamicMap object in which the nodes will be visualized. - This object needs to be displayed. - pipe: Pipe - The pipe which new data (i.e., new and changed nodes) are sent into - """ - - # Pipe that gets the updated data that are then sent to the dynamic map - pipe = Pipe(data=[], memoize=True) - - # Holoviews DynamicMap with a stream that gets data from the pipe - # The hv.Graph visualization is used for displaying the data - # hv.Graph displays the nodes (and optionally binary edges) of a graph - dynamic_map = hv.DynamicMap(hv.Graph, streams=[pipe]) - - # Define options for visualization - dynamic_map.opts( - # Some space around the Graph in order to avoid nodes being on the - # edges of the visualization - padding=0.5, - # # Interactive tools that are provided by holoviews - # tools=['box_select', 'lasso_select', 'tap', 'hover'], - # Do not show axis information (i.e., axis ticks etc.) - xaxis=None, yaxis=None - ).opts(opts.Graph( - # Where to get information on color - # TODO - # color_index='index', - # All in black - cmap=['#ffffff', '#ffffff'] * 50, - # Size of the nodes - node_size=self.node_radius)) - - return dynamic_map, pipe - - def _setup_hyperedge_drawing(self): - """ - Set up the holoviews DynamicMap object - that visualizes the hyperedges of a hypergraph - Returns - ------- - dynamic_map: hv.DynamicMap - The DynamicMap object in which the hyperedges will be visualized. - This object needs to be displayed. - pipe: Pipe - The pipe which new data (i.e., new and changed hyperedges) - are sent into - """ - - # Pipe that gets the updated data that are then sent to the dynamic map - pipe = Pipe(data=[], memoize=True) - - # Function that creates hv.Polygons from the defined points - # Every hyperedge drawing is one (or multiple for triangulation) of - # these polygons - def create_polygon(*args, **kwargs): - # Define holoviews polygons with additional metadata dimensions: - # value is the index into the color map - # alpha specifies the alpha value for the fill color - # line_alpha specifies the alpha value for the boundary - pol = hv.Polygons(*args, - vdims=['value', 'alpha'], - **kwargs) - # Define the mapping described above - pol.opts(alpha='alpha') - # TODO: check this - #line_alpha='line_alpha', - # color_index='value') - # The polygons are then displayed in the DynamicMap object - return pol - - # dynamic_map gets input from pipe and visualizes it as polygons using - # the create_polygon function - dynamic_map = hv.DynamicMap(create_polygon, streams=[pipe]) - - if self.n_hypergraphs <= 1: - # If there is only a single hypergraph, all hyperedges are colored - # differently - import colorcet - cmap = colorcet.glasbey[:len(self.hypergraphs[0].hyperedges)] - elif self.n_hypergraphs <= 10: - # Select Category10 colormap as default for up to 10 data sets - # This is an often used colormap - from bokeh.palettes import all_palettes - cmap = list(all_palettes['Category10'][10][1:self.n_hypergraphs+1])[::-1] - else: - # For larger numbers of data sets, select Glasbey colormap - import colorcet - cmap = colorcet.glasbey[:self.n_hypergraphs] - - # Setting limits for colormaps to make sure color index - # (hypergraph or hyperedge index) is used like a list index - # Generally indexing is equally spaced depending on the indexes - # that actually occur, e.g., if cmap has 5 entries and only indices - # 1, 2, 3 occur, colors 1, 3 and 5 will be used due to equal spacing - # Desired behavior here is always using colors 1, 2 and 3 - # for indices 1, 2 and 3. - # Setting the limit to 5 in the above example causes the colormap - # to be spaced from 1 to 5, mapping color 1 to index 1 and - # color 5 to index 5. - # Equal spacing in between makes sure all other indices - # are mapped correctly as well. - - # If there is more than one hypergraph, one color per hypergraph is - # needed - if self.n_hypergraphs > 1: - dynamic_map.opts(cmap=cmap, clim=(1, self.n_hypergraphs)) - # If there is only one hypergraph, one color per hyperedge is needed - else: - dynamic_map.opts(cmap=cmap, - clim=(1, len(self.hypergraphs[0].hyperedges))) - - return dynamic_map, pipe - def show(self, - subset_style=VisualizationStyle.COLOR, - triangulation_style=VisualizationStyle.INVISIBLE): + subset_style=VisualizationStyle.styles['color'], + triangulation_style=VisualizationStyle.styles['invisible']): """ Set up the correct arrangement and combination of the DynamicMap objects. @@ -224,17 +127,30 @@ def show(self, self.draw_hyperedges(subset_style=subset_style, triangulation_style=triangulation_style) + # Each Hypergraph has a different color + import colorcet + cmap = colorcet.glasbey[:len(self.hypergraphs[0].hyperedges)] + + # Create Graph and Polygon Objects representing the Nodes and Hypergraphs + graph = hv.Graph(self.node_data) + poly = hv.Polygons(self.polygons_data) + + # Changing parameters + graph.opts(node_size=self.node_size, node_color=self.node_color, node_linewidth=self.node_linewidth) + poly.opts(alpha=.2, facecolor=cmap) + # Visualization as an overlay of the graph visualization and the # hyperedge drawings - plot = self.dynamic_map * self.dynamic_map_edges + plot = graph * poly # Set size of the plot to a square to avoid distortions self.plot = plot.redim.range(x=(-1, 11), y=(-1, 11)) - - return hv.render(plot, backend="matplotlib") - - def draw_hyperedges(self, - subset_style=VisualizationStyle.COLOR, - triangulation_style=VisualizationStyle.INVISIBLE): + # TODO: how to get axes? currently figure + fig = hv.render(plot, backend="matplotlib") + return fig + + def draw_hyperedges(self, highlight_neuron=None, + subset_style=VisualizationStyle.styles['color'], + triangulation_style=VisualizationStyle.styles['invisible']): """ Handler for drawing the hyperedges of the hypergraphs in the given style @@ -243,14 +159,14 @@ def draw_hyperedges(self, ---------- subset_style: enum VisualizationStyle How to do the subset standard visualization of the hyperedges: - VisualizationStyle.INVISIBLE => not at all - VisualizationStyle.NOCOLOR => Only contours without color - VisualizationStyle.COLOR => Contours filled with color + VisualizationStyle.styles['invisible] => not at all + VisualizationStyle.styles['nocolor] => Only contours without color + VisualizationStyle.styles['color] => Contours filled with color triangulation_style: enum VisualizationStyle How to do the triangulation visualization of the hyperedges: - VisualizationStyle.INVISIBLE => not at all - VisualizationStyle.NOCOLOR => Only contours without color - VisualizationStyle.COLOR => Contours filled with color + VisualizationStyle.styles['invisible] => not at all + VisualizationStyle.styles['nocolor] => Only contours without color + VisualizationStyle.styles['color] => Contours filled with color """ # Collect all polygons to pass them to the visualization together @@ -267,11 +183,11 @@ def draw_hyperedges(self, # Options are: invisible, nocolor, color # If invisible, the corresponding visualization does not need # to be constructed - if subset_style != VisualizationStyle.INVISIBLE: + if subset_style != VisualizationStyle.styles['invisible']: polygons_subset.append(create_subset(hyperedge, self.positions, self.node_radius)) - if triangulation_style != VisualizationStyle.INVISIBLE: + if triangulation_style != VisualizationStyle.styles['invisible']: polygons_triang.extend( create_triangulation(hyperedge, self.positions)) @@ -297,10 +213,10 @@ def process_polygons(polygons, color=True): # Call the postprocessing function, color added only if needed polygons.extend(process_polygons( polygons_subset, - subset_style == VisualizationStyle.COLOR)) + subset_style == VisualizationStyle.styles['color'])) polygons.extend(process_polygons( polygons_triang, - triangulation_style == VisualizationStyle.COLOR)) + triangulation_style == VisualizationStyle.styles['color'])) # Save as instance attribute so widgets can work with the polygons self.polygons = polygons @@ -365,7 +281,7 @@ def _update_hyperedge_drawings(self, data): else: x['line_alpha'] = 0.2 - self.edges_pipe.send(data=data) + self.polygons_data = data def _update_nodes(self, data): """ @@ -399,9 +315,8 @@ def _update_nodes(self, data): nodes = hv.Nodes((pos_x, pos_y, vertex_ids, vertex_labels), extents=(0.01, 0.01, 0.01, 0.01), vdims='Label') - - new_data = ((edge_source, edge_target), nodes) - self.pipe.send(new_data) + + self.node_data = ((edge_source, edge_target), nodes) # Parameters heuristically tested to produce pleasing results diff --git a/viziphant/test/test_hypergraph.py b/viziphant/test/test_hypergraph.py new file mode 100644 index 0000000..6c412c4 --- /dev/null +++ b/viziphant/test/test_hypergraph.py @@ -0,0 +1,358 @@ +""" +Unit tests for the Hypergraph class. +""" + +import unittest +import numpy as np + +# import quantities as pq +from numpy.testing import assert_array_equal +from viziphant.patterns_src.hypergraph import Hypergraph +import networkx as nx + + +class HypergraphTestCase(unittest.TestCase): + """ + This test is constructed to test the Creation and Transformation of the Hypergraph Class. + In the Setup function different Hypergraphs are created and being tested on different + Graph Transformation algorithms. + """ + + def setUp(self): + self.hypergraphs = [] + # self.spiketrains = elephant.spike_train_generation.compound_poisson_process( + # rate=5 * pq.Hz, + # amplitude_distribution=[0] + [0.98] + [0] * 8 + [0.02], + # t_stop=10 * pq.s, + # ) + # self.patterns = elephant.spade.spade( + # spiketrains=self.spiketrains, + # binsize=1 * pq.ms, + # winlen=1, + # min_spikes=2, + # n_surr=100, + # dither=5 * pq.ms, + # psr_param=[0, 0, 0], + # output_format="patterns", + # )["patterns"] + + # Hypergraph with characters as vertices + self.hyperedges = [["a", "b"], ["b", "c"], ["c", "d"]] + self.vertices = ["a", "b", "c", "d"] + self.vertex_labels = ["A", "B", "C", "D"] + self.weights = [1, 2, 3] + self.positions = np.array([[0, 0], [1, 1], [2, 2], [3, 3]]) + self.repulse = True + + self.hypergraphs.append( + Hypergraph( + self.hyperedges, + self.vertices, + self.vertex_labels, + self.weights, + self.positions, + self.repulse, + ) + ) + + # normal Hypergraph + self.hyperedges = [[1, 2], [2, 3], [3, 4, 9], [5, 6, 7, 8]] + self.vertices = [1, 2, 3, 4, 5, 6, 7, 8, 9] + self.vertex_labels = [ + "neuron1", + "neuron2", + "neuron3", + "neuron4", + "neuron5", + "neuron6", + "neuron7", + "neuron8", + "neuron9", + ] + self.weights = [1, 2, 1, 1] + self.positions = np.array( + [ + [0, 0], + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + [8, 8], + [9, 9], + ] + ) + self.repulse = True + + self.hypergraphs.append( + Hypergraph( + self.hyperedges, + self.vertices, + self.vertex_labels, + self.weights, + self.positions, + self.repulse, + ) + ) + + # Hypergraph with negative Vertices + self.hyperedges = [[-1, -2], [-2, 3], [3, 4]] + self.vertices = [-1, -2, 3, 4] + self.vertex_labels = ["neuron1", "neuron2", "neuron3", "neuron4"] + self.weights = [3, 4, 5] + self.positions = np.array([[0, 0], [1, 1], [2, 2], [3, 3]]) + self.repulse = False + + self.hypergraphs.append( + Hypergraph( + self.hyperedges, + self.vertices, + self.vertex_labels, + self.weights, + self.positions, + self.repulse, + ) + ) + + # Hypergraph without weights and labels + self.hyperedges = [["a", "b"], ["b", "c"], ["c", "d"]] + self.vertices = ["a", "b", "c", "d"] + self.vertex_labels = ["","","",""] + self.weights = [] + self.positions = np.array([[0, 0], [1, 1], [2, 2], [3, 3]]) + self.repulse = False + + self.hypergraphs.append( + Hypergraph( + self.hyperedges, + self.vertices, + self.vertex_labels, + self.weights, + self.positions, + self.repulse, + ) + ) + + def test_hypergraph_init(self): + hyperedges = [["a", "b"], ["b", "c"], ["c", "d"]] + vertices = ["a", "b", "c", "d"] + vertex_labels = ["A", "B", "C", "D"] + weights = [1, 2, 3] + positions = np.array([[0, 0], [1, 1], [2, 2], [3, 3]]) + repulse = True + + hypergraph = Hypergraph( + hyperedges, vertices, vertex_labels, weights, positions, repulse, + ) + # check the Types + self.assertTrue(hypergraph.hyperedges == hyperedges) + self.assertTrue(hypergraph.vertices == vertices) + self.assertTrue(hypergraph.vertex_labels == vertex_labels) + self.assertTrue(hypergraph.weights == weights) + self.assertTrue(np.array_equal(hypergraph.vertices, vertices)) + self.assertTrue(hypergraph.repulse) + self.assertEqual(len(hypergraph.weights), len(hypergraph.hyperedges)) + + # Test of getter method + def test_get_union_of_vertices(self): + for hypergraph in self.hypergraphs: + new_vertices = hypergraph.get_union_of_vertices([hypergraph])[0] + new_vertex_labels = hypergraph.get_union_of_vertices([hypergraph])[1] + # check equality after creation + assert_array_equal(new_vertices, hypergraph.vertices) + assert_array_equal(new_vertex_labels, hypergraph.vertex_labels) + + def test_layout_hypergraph_len(self): + # nx.fruchterman_reingold_layout algorithm applied + for hypergraph in self.hypergraphs: + # check if data is consistent + self.assertEqual( + len(hypergraph.layout_hypergraph()), len(hypergraph.vertices) + ) + + # Test complete and star associated graph layouting algorithm + def test_complete_and_star_associated_graph(self): + for hypergraph in self.hypergraphs: + graph = hypergraph.complete_and_star_associated_graph() + self.assertIsInstance(graph, nx.Graph) + for vertex in hypergraph.vertices: + self.assertIn(vertex, list(graph.nodes)) + + edges = [] + for elem in list(graph.edges): + edges.extend(elem) + + for edge in hypergraph.hyperedges: + for node in edge: + self.assertIn(node, edges) + + # every hyperedge needs a weight + self.assertEqual(len(hypergraph.hyperedges), len(hypergraph.weights)) + + # nodes in graph equal nodes + len(hyperedges) because of pseudo vertices + self.assertEqual( + len(graph.nodes), len(hypergraph.vertices) + len(hypergraph.hyperedges) + ) + # self.assertEqual(len(graph.edges), 9) + + # Test complete and associated graph layouting algorithm + def test_complete_associated_graph(self): + for hypergraph in self.hypergraphs: + graph = hypergraph.complete_associated_graph() + # algorithm returns nx.Graph + self.assertIsInstance(graph, nx.Graph) + for vertex in hypergraph.vertices: + self.assertIn(vertex, list(graph.nodes)) + + edges = [] + for elem in list(graph.edges): + edges.extend(elem) + + for edge in hypergraph.hyperedges: + for node in edge: + self.assertIn(node, edges) + + self.assertEqual(len(hypergraph.hyperedges), len(hypergraph.weights)) + assert_array_equal(list(graph.nodes), hypergraph.vertices) + + # Test complete and weight associated graph layouting algorithm + def test_complete_weighted_associated_graph(self): + for hypergraph in self.hypergraphs: + graph = hypergraph.complete_weighted_associated_graph() + self.assertIsInstance(graph, nx.Graph) + for vertex in hypergraph.vertices: + self.assertIn(vertex, list(graph.nodes)) + + edges = [] + for elem in list(graph.edges): + edges.extend(elem) + + for edge in hypergraph.hyperedges: + for node in edge: + self.assertIn(node, edges) + + self.assertEqual(len(hypergraph.hyperedges), len(hypergraph.weights)) + assert_array_equal(list(graph.nodes), hypergraph.vertices) + + def test_empty_hypergraph(self): + # Hypergraph without hyperedges + hyperedges = [] + vertices = ["a", "b", "c", "d"] + vertex_labels = ["A", "B", "C", "D"] + weights = [1, 2, 3] + positions = np.array([[0, 0], [1, 1], [2, 2], [3, 3]]) + repulse = True + + hypergraph = Hypergraph( + hyperedges, vertices, vertex_labels, weights, positions, repulse, + ) + graph = hypergraph.complete_and_star_associated_graph() + self.assertIsInstance(graph, nx.Graph) + self.assertEqual(len(graph.nodes), 4) + self.assertEqual(len(graph.edges), 0) + + def test_single_hyperedge(self): + # one hyperedge + hyperedges = [[1, 3]] + vertices = [1, 2, 3, 4] + vertex_labels = ["A", "B", "C", "D"] + weights = [1, 1] + positions = np.array([[0, 0], [1, 1], [2, 2], [3, 3]]) + repulse = True + + hypergraph = Hypergraph( + hyperedges, vertices, vertex_labels, weights, positions, repulse, + ) + graph = hypergraph.complete_and_star_associated_graph() + self.assertEqual(len(graph.nodes), len(hypergraph.vertices)) + self.assertEqual(len(graph.edges), 3) + + def test_multiple_hyperedges(self): + # multiple hyperedges + hyperedges = [[1, 3], [2, 3, 4]] + vertices = [1, 2, 3, 4] + vertex_labels = ["A", "B", "C", "D"] + weights = [1, 1] + positions = np.array([[0, 0], [1, 1], [2, 2], [3, 3]]) + repulse = True + + hypergraph = Hypergraph( + hyperedges, vertices, vertex_labels, weights, positions, repulse, + ) + graph = hypergraph.complete_and_star_associated_graph() + self.assertEqual(len(graph.nodes), 6) + + def test_distance_equal(self): + # distance of edges should be equal because of equal weights + hyperedges = [[1, 2], [2, 3], [1, 3]] + vertices = [1, 2, 3] + vertex_labels = [1, 2, 3] + weights = [1, 1, 1] + positions = np.array([[0, 0], [1, 1], [2, 2]]) + repulse = True + + hypergraph = Hypergraph( + hyperedges, vertices, vertex_labels, weights, positions, repulse, + ) + graph = hypergraph.complete_and_star_associated_graph() + distance = [] + distance.append(nx.dijkstra_path_length(graph, 1, 2)) + distance.append(nx.dijkstra_path_length(graph, 2, 3)) + distance.append(nx.dijkstra_path_length(graph, 1, 3)) + self.assertEqual(distance, [distance[0]] * len(distance)) + + def test_lower_distance(self): + # distance of connected vertices should be less or equal to not connected vertices + hyperedges = [[1, 2, 3], [2, 3]] + vertices = [1, 2, 3, 4] + vertex_labels = [1, 2, 3, 4] + weights = [17] + positions = np.array([[0, 0], [1, 1], [2, 2]]) + repulse = True + + hypergraph = Hypergraph( + hyperedges, vertices, vertex_labels, weights, positions, repulse, + ) + + graph = hypergraph.complete_and_star_associated_graph() + self.assertLessEqual( + nx.dijkstra_path_length(graph, 1, 2), nx.dijkstra_path_length(graph, 1, 4) + ) + + # Two Hypergraphs with the same data should be equal + def test_consistency(self): + hyperedges = [[1, 2, 3], [2, 3]] + vertices = [1, 2, 3, 4] + vertex_labels = [1, 2, 3, 4] + weights = [17] + positions = np.array([[0, 0], [1, 1], [2, 2]]) + repulse = True + + hypergraph = Hypergraph( + hyperedges, + vertices, + vertex_labels, + weights, + positions, + repulse, + ) + + graph1 = hypergraph.complete_and_star_associated_graph() + graph2 = hypergraph.complete_and_star_associated_graph() + + self.assertEqual(len(graph1.edges), len(graph2.edges)) + self.assertEqual(len(graph1.nodes), len(graph2.nodes)) + self.assertEqual(graph1.edges, graph2.edges) + self.assertEqual(graph1.nodes, graph2.nodes) + self.assertEqual(nx.dijkstra_path_length(graph1, 1, 2), nx.dijkstra_path_length(graph2, 1, 2)) + self.assertEqual(nx.dijkstra_path_length(graph1, 2,3), nx.dijkstra_path_length(graph2, 2, 3)) + self.assertTrue(isinstance(graph1, nx.Graph)) + self.assertTrue(isinstance(graph2, nx.Graph)) + for edge in graph1.edges: + self.assertEqual(graph1.edges[edge]['weight'], graph2.edges[edge]['weight']) + + +if __name__ == "__main__": + unittest.main()