source: orange/Orange/OrangeWidgets/Unsupervised/OWNxAnalysis.py @ 10510:e65f85644182

Revision 10510:e65f85644182, 19.4 KB checked in by miha, 2 years ago (diff)

Fixed a bug in older versions of networkx.

Line 
1"""
2<name>Net Analysis</name>
3<description>Statistical analysis of network data.</description>
4<icon>icons/NetworkFromDistances.png</icon>
5<contact>Miha Stajdohar (miha.stajdohar(@at@)gmail.com)</contact> 
6<priority>6425</priority>
7"""
8from PyQt4.QtCore import QMutex
9import numpy
10import networkx as nx
11
12import Orange
13import OWGUI
14
15from OWWidget import *
16
17NODELEVEL = 0
18GRAPHLEVEL = 1
19
20class WorkerThread(QThread):
21    def __init__(self, receiver, name, label, type, algorithm):
22        QThread.__init__(self)
23        self.receiver = receiver
24        self.name = name
25        self.label = label
26        self.type = type
27        self.algorithm = algorithm
28       
29        self.stopped = 0
30        self.result = None
31        self.error = None
32        self.is_terminated = False
33         
34    def run(self):
35        try:
36            self.result = self.algorithm(self.receiver.graph)
37        except Exception as ex:
38            self.result = None
39            self.error = ex
40       
41class OWNxAnalysis(OWWidget):
42    settingsList=[
43        "auto_commit", "tab_index", "degree", "in_degree", "out_degree", "average_neighbor_degree",
44        "clustering", "triangles", "square_clustering", "number_of_cliques",
45        "degree_centrality", "in_degree_centrality", "out_degree_centrality", 
46        "closeness_centrality", "betweenness_centrality", 
47        "current_flow_closeness_centrality", "current_flow_betweenness_centrality",
48        "approximate_current_flow_betweenness_centrality", 
49        "eigenvector_centrality", "eigenvector_centrality_numpy", "load_centrality", 
50        "core_number", "eccentricity", "closeness_vitality", 
51       
52        "number_of_nodes", "number_of_edges", "average_degree", "density",
53        "degree_assortativity_coefficient", "degree_pearson_correlation_coefficient", 
54        "degree_pearson_correlation_coefficient",
55        "estrada_index", "graph_clique_number", "graph_number_of_cliques", 
56        "transitivity", "average_clustering", "number_connected_components", 
57        "number_strongly_connected_components", "number_weakly_connected_components", 
58        "number_attracting_components", "diameter", "radius", "average_shortest_path_length"
59    ]
60   
61    def __init__(self, parent=None, signalManager=None):
62        OWWidget.__init__(self, parent, signalManager, "Nx Analysis", noReport=True, wantMainArea=False)
63       
64        self.inputs = [("Network", Orange.network.Graph, self.set_graph), 
65                        ("Items", Orange.data.Table, self.set_items)]
66       
67        self.outputs = [("Network", Orange.network.Graph), 
68                        ("Items", Orange.data.Table)]
69
70        self.methods = [
71            ("number_of_nodes", True, "Number of nodes", GRAPHLEVEL, lambda G: G.number_of_nodes()),
72            ("number_of_edges", True, "Number of edges", GRAPHLEVEL, lambda G: G.number_of_edges()),
73            ("average_degree", True, "Average degree", GRAPHLEVEL, lambda G: numpy.average(G.degree().values())),
74            ("diameter", False, "Diameter", GRAPHLEVEL, nx.diameter),
75            ("radius", False, "Radius", GRAPHLEVEL, nx.radius),
76            ("average_shortest_path_length", False, "Average shortest path length", GRAPHLEVEL, nx.average_shortest_path_length),
77            ("density", True, "Density", GRAPHLEVEL, nx.density),
78            ("degree_assortativity_coefficient", False, \
79                "Degree assortativity coefficient", GRAPHLEVEL, \
80                    nx.degree_assortativity_coefficient if \
81                    hasattr(nx, "degree_assortativity_coefficient") else None),
82            # additional attr needed
83            #("attribute_assortativity_coefficient", False, "Attribute assortativity coefficient", GRAPHLEVEL, nx.attribute_assortativity_coefficient),
84            #("numeric_assortativity_coefficient", False, "Numeric assortativity coefficient", GRAPHLEVEL, nx.numeric_assortativity_coefficient),
85            ("degree_pearson_correlation_coefficient", False, \
86                "Degree pearson correlation coefficient", GRAPHLEVEL, \
87                nx.degree_pearson_correlation_coefficient if\
88                hasattr(nx, "degree_pearson_correlation_coefficient") else None),
89            ("estrada_index", False, "Estrada index", GRAPHLEVEL, \
90                nx.estrada_index if hasattr(nx, "estrada_index") else None),
91            ("graph_clique_number", False, "Graph clique number", GRAPHLEVEL, nx.graph_clique_number),
92            ("graph_number_of_cliques", False, "Graph number of cliques", GRAPHLEVEL, nx.graph_number_of_cliques),
93            ("transitivity", False, "Graph transitivity", GRAPHLEVEL, nx.transitivity),
94            ("average_clustering", False, "Average clustering coefficient", GRAPHLEVEL, nx.average_clustering),
95            ("number_connected_components", False, "Number of connected components", GRAPHLEVEL, nx.number_connected_components),
96            ("number_strongly_connected_components", False, "Number of strongly connected components", GRAPHLEVEL, nx.number_strongly_connected_components),
97            ("number_weakly_connected_components", False, "Number of weakly connected components", GRAPHLEVEL, nx.number_weakly_connected_components),
98            ("number_attracting_components", False, "Number of attracting components", GRAPHLEVEL, nx.number_attracting_components),
99            # TODO: input parameters
100            #("max_flow", False, "Maximum flow", GRAPHLEVEL, nx.max_flow),
101            #("min_cut", False, "Minimum cut", GRAPHLEVEL, nx.min_cut),
102            #("ford_fulkerson", False, "Maximum single-commodity flow (Ford-Fulkerson)", GRAPHLEVEL, nx.ford_fulkerson),
103            #("min_cost_flow_cost", False, "min_cost_flow_cost", GRAPHLEVEL, nx.min_cost_flow_cost),
104            # returns dict of dict
105            #("shortest_path_length", False, "Shortest path length", GRAPHLEVEL, nx.shortest_path_length),
106           
107            ("degree", False, "Degree", NODELEVEL, nx.degree),
108            ("in_degree", False, "In-degree", NODELEVEL, lambda G: G.in_degree()),
109            ("out_degree", False, "Out-degree", NODELEVEL, lambda G: G.out_degree()),
110            ("average_neighbor_degree", False, "Average neighbor degree", NODELEVEL, nx.average_neighbor_degree),
111            ("clustering", False, "Clustering coefficient", NODELEVEL, nx.clustering),
112            ("triangles", False, "Number of triangles", NODELEVEL, nx.triangles),
113            ("square_clustering", False, "Squares clustering coefficient", NODELEVEL, nx.square_clustering),
114            ("number_of_cliques", False, "Number of cliques", NODELEVEL, nx.number_of_cliques),
115            ("degree_centrality", False, "Degree centrality", NODELEVEL, nx.degree_centrality),
116            ("in_degree_centrality", False, "In-egree centrality", NODELEVEL, nx.in_degree_centrality),
117            ("out_degree_centrality", False, "Out-degree centrality", NODELEVEL, nx.out_degree_centrality),
118            ("closeness_centrality", False, "Closeness centrality", NODELEVEL, nx.closeness_centrality),
119            ("betweenness_centrality", False, "Betweenness centrality", NODELEVEL, nx.betweenness_centrality),
120            ("current_flow_closeness_centrality", False, "Information centrality", NODELEVEL, nx.current_flow_closeness_centrality),
121            ("current_flow_betweenness_centrality", False, "Random-walk betweenness centrality", NODELEVEL, nx.current_flow_betweenness_centrality),
122            ("approximate_current_flow_betweenness_centrality", False, \
123                "Approx. random-walk betweenness centrality", NODELEVEL, \
124                nx.approximate_current_flow_betweenness_centrality if \
125                hasattr(nx, "approximate_current_flow_betweenness_centrality") \
126                    else None),
127            ("eigenvector_centrality", False, "Eigenvector centrality", NODELEVEL, nx.eigenvector_centrality),
128            ("eigenvector_centrality_numpy", False, "Eigenvector centrality (NumPy)", NODELEVEL, nx.eigenvector_centrality_numpy),
129            ("load_centrality", False, "Load centrality", NODELEVEL, nx.load_centrality),
130            ("core_number", False, "Core number", NODELEVEL, nx.core_number),
131            ("eccentricity", False, "Eccentricity", NODELEVEL, nx.eccentricity),
132            ("closeness_vitality", False, "Closeness vitality", NODELEVEL, nx.closeness_vitality),                   
133        ]
134
135        self.methods = [method for method in self.methods if method[-1] is not None]
136       
137        self.auto_commit = False
138        self.tab_index = 0
139        self.mutex = QMutex()
140       
141        self.graph = None
142        self.items = None          # items set by Items signal
143        self.items_graph = None    # items set by graph.items by Network signal
144        self.items_analysis = None # items to output and merge with analysis result
145       
146        self.job_queue = []
147        self.job_working = []
148        self.analfeatures = [] 
149        self.analdata = {}
150       
151        for method in self.methods:
152            setattr(self, method[0], method[1])
153            setattr(self, "lbl_" + method[0], "")
154   
155        self.loadSettings()
156       
157        self.tabs = OWGUI.tabWidget(self.controlArea)
158        self.tabs.setMinimumWidth(450)
159        self.graphIndices = OWGUI.createTabPage(self.tabs, "Graph-level indices")   
160        self.nodeIndices = OWGUI.createTabPage(self.tabs, "Node-level indices")
161        self.tabs.setCurrentIndex(self.tab_index)
162        self.connect(self.tabs, SIGNAL("currentChanged(int)"), lambda index: setattr(self, 'tab_index', index))
163       
164        for name, default, label, type, algorithm in self.methods:
165            if type == NODELEVEL:
166                box = OWGUI.widgetBox(self.nodeIndices, orientation="horizontal")
167            elif type == GRAPHLEVEL:
168                box = OWGUI.widgetBox(self.graphIndices, orientation="horizontal")
169           
170            OWGUI.checkBox(box, self, name, label=label, callback=lambda n=name: self.method_clicked(n))
171            box.layout().addStretch(1)
172            lbl = OWGUI.label(box, self, "%(lbl_" + name + ")s")
173            setattr(self, "tool_" + name, lbl)
174           
175        self.graphIndices.layout().addStretch(1)
176        self.nodeIndices.layout().addStretch(1)
177       
178        OWGUI.checkBox(self.controlArea, self, "auto_commit", label="Commit automatically")
179       
180        hb = OWGUI.widgetBox(self.controlArea, None, orientation='horizontal')
181        self.btnCommit = OWGUI.button(hb, self, "Commit", callback=self.analyze, toggleButton=1)
182        self.btnStopC = OWGUI.button(hb, self, "Stop current", callback=lambda current=True: self.stop_job(current))
183        self.btnStopA = OWGUI.button(hb, self, "Stop all", callback=lambda current=False: self.stop_job(current))
184        self.btnStopC.setEnabled(False)
185        self.btnStopA.setEnabled(False)
186       
187        self.reportButton = OWGUI.button(hb, self, "&Report", self.reportAndFinish, debuggingEnabled=0)
188        self.reportButton.setAutoDefault(0)
189       
190    def set_graph(self, graph):
191        if graph is None:
192            return
193       
194        self.stop_job(current=False)
195       
196        self.mutex.lock()
197       
198        self.graph = graph
199        self.items_graph = graph.items()
200        self.items_analysis = graph.items()
201       
202        if self.items is not None:
203            self.items_analysis =  self.items
204       
205        self.clear_results()
206        self.clear_labels()
207        # only clear computed statistics on new graph
208        self.analdata.clear()
209       
210        self.mutex.unlock()
211       
212        if self.auto_commit:
213            self.btnCommit.setChecked(True)
214            self.analyze()
215       
216       
217    def set_items(self, items):
218        self.mutex.lock()
219       
220        if items is None and self.items_graph is not None:
221            self.items_analysis = self.items_graph
222           
223        elif items is not None:
224            self.items_analysis = items
225       
226        self.items = items
227       
228        self.mutex.unlock()
229       
230    def analyze(self):
231        if self.graph is None or not self.btnCommit.isChecked():
232            return
233       
234        if len(self.job_queue) > 0 or len(self.job_working) > 0:
235            return
236       
237        self.btnStopC.setEnabled(True)
238        self.btnStopA.setEnabled(True)
239        self.clear_labels()
240        qApp.processEvents()
241       
242        self.clear_results()
243       
244        for method in self.methods:
245            self.add_job(method)
246           
247        if len(self.job_queue) > 0:         
248            self.start_job()
249        else:
250            self.send_data()
251   
252    def add_job(self, method):
253        name, default, label, type, algorithm = method
254       
255        is_method_enabled = getattr(self, name)
256           
257        if not is_method_enabled:
258            return
259       
260        #if type == NODELEVEL:
261        job = WorkerThread(self, name, label, type, algorithm)
262        self.connect(job, SIGNAL("terminated()"), lambda j=job: self.job_terminated(j))
263        self.connect(job, SIGNAL("finished()"), lambda j=job: self.job_finished(j))
264        self.job_queue.insert(0, job)
265        setattr(self, "lbl_" + job.name, "   waiting")
266   
267    def start_job(self):
268        max_jobs = QThread.idealThreadCount() - 1
269       
270        self.mutex.lock()
271        if len(self.job_queue) > 0 and len(self.job_working) < max_jobs:
272            job = self.job_queue.pop()
273            setattr(self, "lbl_" + job.name, "   started")
274           
275            # if data for this job already computed
276            if job.name in self.analdata:
277                if job.type == NODELEVEL:
278                    self.analfeatures.append((job.name, \
279                                Orange.feature.Continuous(job.label)))
280                    setattr(self, "lbl_" + job.name, "  finished")
281                   
282                elif job.type == GRAPHLEVEL:
283                    setattr(self, "lbl_" + job.name,("%.4f" % \
284                            self.analdata[job.name]).rstrip('0').rstrip('.'))                     
285               
286                job.quit()
287                self.send_data()
288            else:
289                self.job_working.append(job)
290                job.start()
291        self.mutex.unlock()
292       
293        if len(self.job_queue) > 0 and len(self.job_working) < max_jobs:
294            self.start_job()
295           
296    def job_terminated(self, job):
297        self.mutex.lock()
298        job.is_terminated = True
299        self.mutex.unlock()
300       
301    def job_finished(self, job):
302        self.mutex.lock()
303        if job.is_terminated:
304            setattr(self, "lbl_" + job.name, "terminated")
305        else:
306            setattr(self, "lbl_" + job.name, "  finished")
307       
308            if job.error is not None:
309                setattr(self, "lbl_" + job.name, "     error")
310                tooltop = getattr(self, "tool_" + job.name)
311                tooltop.setToolTip(QString(job.error.message))
312               
313            elif job.result is not None:
314                if job.type == NODELEVEL:
315                    self.analfeatures.append((job.name, Orange.feature.Continuous(job.label)))
316                    self.analdata[job.name] = [job.result[node] for node in sorted(job.result.iterkeys())]
317                   
318                elif job.type == GRAPHLEVEL:
319                    self.analdata[job.name] = job.result
320                    setattr(self, "lbl_" + job.name, ("%.4f" % job.result).rstrip('0').rstrip('.'))
321       
322        if job in self.job_working:
323            self.job_working.remove(job)
324
325        self.send_data()
326        self.mutex.unlock()
327       
328        if len(self.job_queue) > 0:
329            self.start_job()
330           
331    def stop_job(self, current=True, name=None):
332        self.mutex.lock()
333       
334        if name is not None:
335            for i in range(len(self.job_queue) - 1, -1, -1 ):
336                job = self.job_queue[i]
337                if name == job.name:
338                   
339                    job.is_terminated = True
340                    job.quit()
341                    job.wait()
342                    self.job_queue.remove(job)
343                    setattr(self, "lbl_" + name, "terminated")                           
344                   
345            for job in self.job_working:
346                if name == job.name:
347                    job.is_terminated = True
348                    job.terminate()
349        else:
350            if not current:
351                while len(self.job_queue) > 0:
352                    job = self.job_queue.pop()
353                    job.is_terminated = True
354                    job.quit()
355                    job.wait()
356                    setattr(self, "lbl_" + job.name, "terminated")
357   
358            for job in self.job_working:
359                job.is_terminated = True
360                job.terminate()
361
362        self.mutex.unlock()
363       
364    def send_data(self):
365        if len(self.job_queue) <= 0 and len(self.job_working) <= 0:
366            self.btnCommit.setChecked(False)
367            self.btnStopC.setEnabled(False)
368            self.btnStopA.setEnabled(False)
369           
370            if self.analdata is not None and len(self.analdata) > 0 and \
371                                                    len(self.analfeatures) > 0:
372                vars = [] 
373                analdata = []
374                for name, var in self.analfeatures:
375                    analdata.append(self.analdata[name])
376                    vars.append(var)
377                   
378                analitems  = Orange.data.Table(Orange.data.Domain(vars, False), [list(t) for t in zip(*analdata)])
379                self.graph.set_items(Orange.data.Table([analitems, self.items_analysis]))
380           
381            self.send("Network", self.graph)
382            self.send("Items", self.graph.items())
383           
384            self.clear_results()
385           
386    def method_clicked(self, name):
387        if self.auto_commit:
388            self.mutex.lock()
389            if len(self.job_queue) <= 0 and len(self.job_working) <= 0:
390                self.btnCommit.setChecked(True)
391                self.mutex.unlock()
392                self.analyze()
393            else:
394                is_method_enabled = getattr(self, name)
395                if is_method_enabled:
396                    for method in self.methods:
397                        if name == method[0]:
398                            self.add_job(method)
399                    self.mutex.unlock()
400                else:
401                    self.mutex.unlock()
402                    self.stop_job(name=name)
403           
404    def clear_results(self):
405        del self.job_queue[:]
406        del self.job_working[:]
407        del self.analfeatures[:]
408       
409    def clear_labels(self):
410        for method in self.methods:
411            setattr(self, "lbl_" + method[0], "")
412           
413    def sendReport(self):
414        report = []
415       
416        for name, default, label, type, algorithm in self.methods:
417            if type == GRAPHLEVEL:
418                value = getattr(self, "lbl_" + name)
419                value = str(value).strip().lower()
420                if value != "" and value != "error"  and value != "waiting" \
421                            and value != "terminated" and value != "finished":
422                    report.append((label, value))
423       
424        self.reportSettings("Graph statistics", report)
425           
426       
427if __name__ == "__main__":   
428    a=QApplication(sys.argv)
429    ow=OWNxAnalysis()
430    ow.show()
431    def setNetwork(signal, data, id=None):
432        if signal == 'Network':
433            ow.set_graph(data)
434        #if signal == 'Items':
435        #    ow.set_items(data)
436       
437    import OWNxFile
438    owFile = OWNxFile.OWNxFile()
439    owFile.send = setNetwork
440    owFile.show()
441    owFile.selectNetFile(0)
442   
443    a.exec_()
444    ow.saveSettings()
445    owFile.saveSettings()
Note: See TracBrowser for help on using the repository browser.