From 45904ea370694fac18cf0642e5696fdec345b9dd Mon Sep 17 00:00:00 2001 From: Moritz-Alexander-Kern Date: Thu, 16 Nov 2023 14:47:02 +0100 Subject: [PATCH 1/9] plot_patterns_hypergraph return axes --- viziphant/patterns_src/view.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/viziphant/patterns_src/view.py b/viziphant/patterns_src/view.py index 82e1a6a..31edfc5 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 @@ -230,7 +231,8 @@ def show(self, # 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") + axes = hv.render(plot, backend="matplotlib").axes[0] + return axes def draw_hyperedges(self, subset_style=VisualizationStyle.COLOR, From f61157662c96e473677ea91f86f81ae32614b688 Mon Sep 17 00:00:00 2001 From: Moritz-Alexander-Kern Date: Thu, 16 Nov 2023 14:59:23 +0100 Subject: [PATCH 2/9] remove bokeh --- viziphant/patterns_src/view.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/viziphant/patterns_src/view.py b/viziphant/patterns_src/view.py index 31edfc5..ff50c91 100644 --- a/viziphant/patterns_src/view.py +++ b/viziphant/patterns_src/view.py @@ -173,11 +173,6 @@ def create_polygon(*args, **kwargs): # 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 From 217c301a1a39fed9e047ea107940d5b7e5241465 Mon Sep 17 00:00:00 2001 From: Moritz-Alexander-Kern Date: Thu, 16 Nov 2023 15:26:19 +0100 Subject: [PATCH 3/9] added todos --- viziphant/patterns.py | 3 ++- viziphant/patterns_src/view.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/viziphant/patterns.py b/viziphant/patterns.py index 26a2afc..25c176d 100644 --- a/viziphant/patterns.py +++ b/viziphant/patterns.py @@ -399,6 +399,7 @@ def plot_patterns(spiketrains, patterns, circle_sizes=(3, 50, 70), axes.yaxis.set_label_coords(-0.01, 0.5) return axes +# TODO: add a parameter node_size def plot_patterns_hypergraph(patterns, num_neurons=None): """ Hypergraph visualization of spike patterns. @@ -461,7 +462,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) + viziphant.patterns.plot_patterns_hypergraph(patterns) plt.show() """ diff --git a/viziphant/patterns_src/view.py b/viziphant/patterns_src/view.py index ff50c91..a440d7a 100644 --- a/viziphant/patterns_src/view.py +++ b/viziphant/patterns_src/view.py @@ -53,8 +53,8 @@ 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 + # Size of the vertices TODO: add as parameter + self.node_radius = .2 # Selected title of the figure self.title = title @@ -225,8 +225,8 @@ def show(self, plot = self.dynamic_map * self.dynamic_map_edges # Set size of the plot to a square to avoid distortions self.plot = plot.redim.range(x=(-1, 11), y=(-1, 11)) - - axes = hv.render(plot, backend="matplotlib").axes[0] + # TODO: how to get axes? currently figure + axes = hv.render(plot, backend="matplotlib") return axes def draw_hyperedges(self, From ac82d333666e0da306886c48e33fc11a6d64c032 Mon Sep 17 00:00:00 2001 From: tm4185s Date: Thu, 30 Nov 2023 15:23:18 +0100 Subject: [PATCH 4/9] changed example --- viziphant/patterns.py | 1 - 1 file changed, 1 deletion(-) diff --git a/viziphant/patterns.py b/viziphant/patterns.py index 25c176d..fd3a880 100644 --- a/viziphant/patterns.py +++ b/viziphant/patterns.py @@ -463,7 +463,6 @@ def plot_patterns_hypergraph(patterns, num_neurons=None): patterns = cell_assembly_detection(bst, max_lag=2) viziphant.patterns.plot_patterns_hypergraph(patterns) - plt.show() """ # If only patterns of a single dataset are given, wrap them in a list to From 443e402d13a7878919e89e3468df11fcc64fa7dd Mon Sep 17 00:00:00 2001 From: tm4185s Date: Thu, 7 Dec 2023 14:52:26 +0100 Subject: [PATCH 5/9] added parameters for hypergraph function --- requirements/requirements.txt | 1 - viziphant/patterns.py | 42 ++++++++++++++++++++++++++++------ viziphant/patterns_src/view.py | 37 ++++++++++++++++++++++-------- 3 files changed, 62 insertions(+), 18 deletions(-) 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 fd3a880..2854b9a 100644 --- a/viziphant/patterns.py +++ b/viziphant/patterns.py @@ -399,8 +399,8 @@ def plot_patterns(spiketrains, patterns, circle_sizes=(3, 50, 70), axes.yaxis.set_label_coords(-0.01, 0.5) return axes -# TODO: add a parameter node_size -def plot_patterns_hypergraph(patterns, num_neurons=None): +def plot_patterns_hypergraph(patterns, node_size=3, pattern_size=None, num_neurons=None,\ + highlight_patterns=None, mark_neuron=None, node_color='white'): """ Hypergraph visualization of spike patterns. @@ -430,12 +430,19 @@ 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): tuple or int + 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 - + highlight_patterns (optional) : int + Highlight pattern which includes neuron x + node_color (optional) : String + change the color of the nodes Returns ------- A handle to a matplotlib figure containing the hypergraph. @@ -493,11 +500,33 @@ def plot_patterns_hypergraph(patterns, num_neurons=None): # Create one hypergraph per dataset hyperedges = [] + + # Create "range" of pattern_size + if (isinstance(pattern_size, int)): pattern_size=(pattern_size, pattern_size) + # 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: + hyperedges.append(pattern['neurons']) + # check if hyperedge(pattern) is greater or equal to min_pattern_size + elif len(pattern['neurons']) >= pattern_size[0] and len(pattern['neurons']) <= pattern_size[1] or highlight_patterns in pattern['neurons']: + hyperedges.append(pattern['neurons']) + + # check if neuron to highlight is in hyperedge + temp_hyperedges = [] + if highlight_patterns is not None: + if isinstance(highlight_patterns, int): + for edge in hyperedges: + if highlight_patterns in edge: + temp_hyperedges.append(edge) + hyperedges = temp_hyperedges + + # TODO: highlight_patterns as a list + + if (len(hyperedges) == 0): + raise Exception('Could not find any hyperedges that match the given parameters') + # Currently, all hyperedges receive the same weights weights = [weight] * len(hyperedges) @@ -507,8 +536,7 @@ def plot_patterns_hypergraph(patterns, num_neurons=None): weights=weights, repulse=repulsive) hypergraphs.append(hg) - - view = View(hypergraphs) + view = View(hypergraphs, node_size, mark_neuron, node_color) fig = view.show(subset_style=VisualizationStyle.COLOR, triangulation_style=VisualizationStyle.INVISIBLE) diff --git a/viziphant/patterns_src/view.py b/viziphant/patterns_src/view.py index a440d7a..b340600 100644 --- a/viziphant/patterns_src/view.py +++ b/viziphant/patterns_src/view.py @@ -35,7 +35,7 @@ class View: for the visualization of hypergraphs. """ - def __init__(self, hypergraphs, title=None): + def __init__(self, hypergraphs, node_size=3, mark_neuron=None, node_color='white', title=None): """ Constructs a View object that handles the visualization of the given hypergraphs. @@ -45,6 +45,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 + + mark_neuron (optional) : int + Neuron with given number will be highlighted + + node_color (optional) : String + change the color of the nodes """ # Hyperedge drawings @@ -53,12 +62,21 @@ def __init__(self, hypergraphs, title=None): # Which color of the color map to use next self.current_color = 1 - # Size of the vertices TODO: add as parameter + # 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 # Selected title of the figure self.title = title + # Marked node will be in a different color + self.mark_neuron = mark_neuron + # If no data was provided, fill in dummy data if hypergraphs: self.hypergraphs = hypergraphs @@ -107,7 +125,7 @@ def _setup_graph_visualization(self): # 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 @@ -124,8 +142,7 @@ def _setup_graph_visualization(self): # All in black cmap=['#ffffff', '#ffffff'] * 50, # Size of the nodes - node_size=self.node_radius)) - + node_size=self.node_size, node_color=self.node_color, show_legend=True)) return dynamic_map, pipe def _setup_hyperedge_drawing(self): @@ -226,10 +243,10 @@ def show(self, # Set size of the plot to a square to avoid distortions self.plot = plot.redim.range(x=(-1, 11), y=(-1, 11)) # TODO: how to get axes? currently figure - axes = hv.render(plot, backend="matplotlib") - return axes - - def draw_hyperedges(self, + fig = hv.render(plot, backend="matplotlib") + return fig + + def draw_hyperedges(self, highlight_neuron=None, subset_style=VisualizationStyle.COLOR, triangulation_style=VisualizationStyle.INVISIBLE): """ @@ -396,7 +413,7 @@ 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) From ece3072acf9c28504a66b606c9b7b723071ed898 Mon Sep 17 00:00:00 2001 From: tm4185s Date: Mon, 6 May 2024 08:24:26 +0200 Subject: [PATCH 6/9] Enh/add hypergraph tests added tests for different types of hypergraphs --- viziphant/test/test_hypergraph.py | 358 ++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 viziphant/test/test_hypergraph.py 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() From ea1bac29dc016874292e56ff58504361b0014660 Mon Sep 17 00:00:00 2001 From: tm4185s Date: Mon, 6 May 2024 08:32:23 +0200 Subject: [PATCH 7/9] added more parameters for hypergraph function and added fix for negative vertices --- viziphant/patterns.py | 42 +++++++++++----------------- viziphant/patterns_src/hypergraph.py | 16 ++++++++--- viziphant/patterns_src/view.py | 19 ++++++------- 3 files changed, 37 insertions(+), 40 deletions(-) diff --git a/viziphant/patterns.py b/viziphant/patterns.py index 2854b9a..926beee 100644 --- a/viziphant/patterns.py +++ b/viziphant/patterns.py @@ -399,8 +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, node_size=3, pattern_size=None, num_neurons=None,\ - highlight_patterns=None, mark_neuron=None, node_color='white'): +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. @@ -432,17 +432,21 @@ def plot_patterns_hypergraph(patterns, node_size=3, pattern_size=None, num_neuro pattern detectors. node_size (optional): int Change the size of the drawen nodes - pattern_size (optional): tuple or int + 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 - highlight_patterns (optional) : int + 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 ------- A handle to a matplotlib figure containing the hypergraph. @@ -500,33 +504,18 @@ def plot_patterns_hypergraph(patterns, node_size=3, pattern_size=None, num_neuro # Create one hypergraph per dataset hyperedges = [] - - # Create "range" of pattern_size - if (isinstance(pattern_size, int)): pattern_size=(pattern_size, pattern_size) - # Create one hyperedge from every pattern for pattern in patterns: # A hyperedge is the set of neurons of a pattern - if pattern_size is None: - hyperedges.append(pattern['neurons']) - # check if hyperedge(pattern) is greater or equal to min_pattern_size - elif len(pattern['neurons']) >= pattern_size[0] and len(pattern['neurons']) <= pattern_size[1] or highlight_patterns in pattern['neurons']: + if pattern_size is None or len(pattern['neurons']) in pattern_size: hyperedges.append(pattern['neurons']) - # check if neuron to highlight is in hyperedge - temp_hyperedges = [] - if highlight_patterns is not None: - if isinstance(highlight_patterns, int): - for edge in hyperedges: - if highlight_patterns in edge: - temp_hyperedges.append(edge) - hyperedges = temp_hyperedges - - # TODO: highlight_patterns as a list - - if (len(hyperedges) == 0): - raise Exception('Could not find any hyperedges that match the given parameters') + 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) @@ -536,7 +525,8 @@ def plot_patterns_hypergraph(patterns, node_size=3, pattern_size=None, num_neuro weights=weights, repulse=repulsive) hypergraphs.append(hg) - view = View(hypergraphs, node_size, mark_neuron, node_color) + view = View(hypergraphs=hypergraphs, node_size=node_size, + node_color=node_color, node_linewidth=node_linewidth) fig = view.show(subset_style=VisualizationStyle.COLOR, triangulation_style=VisualizationStyle.INVISIBLE) 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 b340600..f9e5358 100644 --- a/viziphant/patterns_src/view.py +++ b/viziphant/patterns_src/view.py @@ -34,8 +34,7 @@ class View: In summary, this class represents an interactive tool for the visualization of hypergraphs. """ - - def __init__(self, hypergraphs, node_size=3, mark_neuron=None, node_color='white', title=None): + def __init__(self, hypergraphs, node_size=3, node_color='white', node_linewidth=1, title=None): """ Constructs a View object that handles the visualization of the given hypergraphs. @@ -49,11 +48,11 @@ def __init__(self, hypergraphs, node_size=3, mark_neuron=None, node_color='white node_size (optional) : int Size of the nodes in the Hypergraphs - mark_neuron (optional) : int - Neuron with given number will be highlighted - node_color (optional) : String change the color of the nodes + + node_linewidth (optional) : int + change the line width of the nodes """ # Hyperedge drawings @@ -71,12 +70,12 @@ def __init__(self, hypergraphs, node_size=3, mark_neuron=None, node_color='white # 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 - # Marked node will be in a different color - self.mark_neuron = mark_neuron - # If no data was provided, fill in dummy data if hypergraphs: self.hypergraphs = hypergraphs @@ -125,7 +124,7 @@ def _setup_graph_visualization(self): # 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 @@ -142,7 +141,7 @@ def _setup_graph_visualization(self): # All in black cmap=['#ffffff', '#ffffff'] * 50, # Size of the nodes - node_size=self.node_size, node_color=self.node_color, show_legend=True)) + node_size=self.node_size, node_color=self.node_color, node_linewidth=self.node_linewidth, show_legend=True)) return dynamic_map, pipe def _setup_hyperedge_drawing(self): From 53a5eac15fe4b14df0650a6869b69fe35f37f83f Mon Sep 17 00:00:00 2001 From: tm4185s Date: Tue, 28 May 2024 12:31:39 +0200 Subject: [PATCH 8/9] Refactored VisualizationStyle class --- viziphant/patterns.py | 4 ++-- viziphant/patterns_src/view.py | 37 ++++++++++++++++++---------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/viziphant/patterns.py b/viziphant/patterns.py index 926beee..3ebc734 100644 --- a/viziphant/patterns.py +++ b/viziphant/patterns.py @@ -527,7 +527,7 @@ def plot_patterns_hypergraph(patterns, pattern_size=None, num_neurons=None,\ hypergraphs.append(hg) view = View(hypergraphs=hypergraphs, node_size=node_size, node_color=node_color, node_linewidth=node_linewidth) - fig = view.show(subset_style=VisualizationStyle.COLOR, - triangulation_style=VisualizationStyle.INVISIBLE) + fig = view.show(subset_style=VisualizationStyle.styles['color'], + triangulation_style=VisualizationStyle.styles['invisible']) return fig diff --git a/viziphant/patterns_src/view.py b/viziphant/patterns_src/view.py index f9e5358..9e0cb30 100644 --- a/viziphant/patterns_src/view.py +++ b/viziphant/patterns_src/view.py @@ -19,9 +19,12 @@ class VisualizationStyle: - INVISIBLE = 1 - NOCOLOR = 2 - COLOR = 3 + styles = { + 'invisible': 1, + 'nocolor': 2, + 'color': 3 + } + class View: @@ -219,8 +222,8 @@ def create_polygon(*args, **kwargs): 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. @@ -246,8 +249,8 @@ def show(self, return fig def draw_hyperedges(self, highlight_neuron=None, - subset_style=VisualizationStyle.COLOR, - triangulation_style=VisualizationStyle.INVISIBLE): + subset_style=VisualizationStyle.styles['color'], + triangulation_style=VisualizationStyle.styles['invisible']): """ Handler for drawing the hyperedges of the hypergraphs in the given style @@ -256,14 +259,14 @@ def draw_hyperedges(self, highlight_neuron=None, ---------- 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 @@ -280,11 +283,11 @@ def draw_hyperedges(self, highlight_neuron=None, # 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)) @@ -310,10 +313,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 From 77903ad2de74024380fa0ec65383368db6650fa4 Mon Sep 17 00:00:00 2001 From: tm4185s Date: Tue, 23 Jul 2024 11:38:13 +0200 Subject: [PATCH 9/9] removed dynamic_maps and refactored View class --- viziphant/patterns_src/view.py | 143 +++++---------------------------- 1 file changed, 21 insertions(+), 122 deletions(-) diff --git a/viziphant/patterns_src/view.py b/viziphant/patterns_src/view.py index 9e0cb30..d1e4859 100644 --- a/viziphant/patterns_src/view.py +++ b/viziphant/patterns_src/view.py @@ -37,7 +37,12 @@ class View: In summary, this class represents an interactive tool for the visualization of hypergraphs. """ - def __init__(self, hypergraphs, node_size=3, node_color='white', node_linewidth=1, 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. @@ -101,126 +106,9 @@ def __init__(self, hypergraphs, node_size=3, node_color='white', node_linewidth= # 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_size, node_color=self.node_color, node_linewidth=self.node_linewidth, show_legend=True)) - 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)] - 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.styles['color'], triangulation_style=VisualizationStyle.styles['invisible']): @@ -239,9 +127,21 @@ 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)) # TODO: how to get axes? currently figure @@ -381,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): """ @@ -416,8 +316,7 @@ def _update_nodes(self, data): 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