leidenalg-0.11.0/0000775000175000017510000000000015101156755013054 5ustar nileshnileshleidenalg-0.11.0/tests/0000775000175000017510000000000015101156755014216 5ustar nileshnileshleidenalg-0.11.0/tests/test_VertexPartition.py0000664000175000017510000002043115101156755020776 0ustar nileshnileshimport unittest import igraph as ig import leidenalg import random from copy import deepcopy from ddt import ddt, data, unpack #%% def name_object(obj, name): obj.__name__ = name return obj graphs = [ ########################################################################### # Zachary karate network name_object(ig.Graph.Famous('Zachary'), 'Zachary'), ########################################################################### # ER Networks # Undirected no loop name_object(ig.Graph.Erdos_Renyi(100, p=1./100, directed=False, loops=False), 'ER_k1_undirected_no_loops'), name_object(ig.Graph.Erdos_Renyi(100, p=5./100, directed=False, loops=False), 'ER_k5_undirected_no_loops'), # Directed no loop name_object(ig.Graph.Erdos_Renyi(100, p=1./100, directed=True, loops=False), 'ER_k1_directed_no_loops'), name_object(ig.Graph.Erdos_Renyi(100, p=5./100, directed=True, loops=False), 'ER_k5_directed_no_loops'), # Undirected loops name_object(ig.Graph.Erdos_Renyi(100, p=1./100, directed=False, loops=True), 'ER_k1_undirected_loops'), name_object(ig.Graph.Erdos_Renyi(100, p=5./100, directed=False, loops=True), 'ER_k5_undirected_loops'), # Directed loops name_object(ig.Graph.Erdos_Renyi(100, p=1./100, directed=True, loops=True), 'ER_k1_directed_loops'), name_object(ig.Graph.Erdos_Renyi(100, p=5./100, directed=True, loops=True), 'ER_k5_directed_loops'), ########################################################################### # Tree name_object(ig.Graph.Tree(100, 3, mode='undirected'), 'Tree_undirected'), name_object(ig.Graph.Tree(100, 3, mode='out'), 'Tree_directed_out'), name_object(ig.Graph.Tree(100, 3, mode='in'), 'Tree_directed_in'), ########################################################################### # Lattice name_object(ig.Graph.Lattice([100], nei=3, directed=False, mutual=True, circular=True), 'Lattice_undirected'), name_object(ig.Graph.Lattice([100], nei=3, directed=True, mutual=False, circular=True), 'Lattice_directed') ] bipartite_graph = name_object( ig.Graph.Bipartite([0, 0, 0, 0, 1, 1, 1, 1], [[0, 4], [0, 5], [0, 6], [1, 4], [1, 5], [2, 6], [2, 7], [3, 6], [3, 7], [3, 5]]), 'bipartite_example') def make_weighted(G): m = G.ecount() G.es['weight'] = [random.random() for i in range(G.ecount())] G.__name__ += '_weighted' return G graphs += [make_weighted(H) for H in graphs] class BaseTest: @ddt class MutableVertexPartitionTest(unittest.TestCase): def setUp(self): self.optimiser = leidenalg.Optimiser() @data(*graphs) def test_move_nodes(self, graph): if 'weight' in graph.es.attributes() and self.partition_type == leidenalg.SignificanceVertexPartition: raise unittest.SkipTest('Significance doesn\'t handle weighted graphs') if 'weight' in graph.es.attributes(): partition = self.partition_type(graph, weights='weight') else: partition = self.partition_type(graph) for v in range(graph.vcount()): if graph.degree(v) >= 1: u = graph.neighbors(v)[0] diff = partition.diff_move(v, partition.membership[u]) q1 = partition.quality() partition.move_node(v, partition.membership[u]) q2 = partition.quality() self.assertAlmostEqual( q2 - q1, diff, places=5, msg="Difference in quality ({0}) not equal to calculated difference ({1})".format( q2 - q1, diff)) @data(*graphs) def test_aggregate_partition(self, graph): if 'weight' in graph.es.attributes() and self.partition_type != leidenalg.SignificanceVertexPartition: partition = self.partition_type(graph, weights='weight') else: partition = self.partition_type(graph) self.optimiser.move_nodes(partition) aggregate_partition = partition.aggregate_partition() self.assertAlmostEqual( partition.quality(), aggregate_partition.quality(), places=5, msg='Quality not equal for aggregate partition.') self.optimiser.move_nodes(aggregate_partition) partition.from_coarse_partition(aggregate_partition) self.assertAlmostEqual( partition.quality(), aggregate_partition.quality(), places=5, msg='Quality not equal from coarser partition.') @data(*graphs) def test_total_weight_in_all_comms(self, graph): if 'weight' in graph.es.attributes() and self.partition_type != leidenalg.SignificanceVertexPartition: partition = self.partition_type(graph, weights='weight') else: partition = self.partition_type(graph) self.optimiser.optimise_partition(partition) s = sum([partition.total_weight_in_comm(c) for c,_ in enumerate(partition)]) self.assertAlmostEqual( s, partition.total_weight_in_all_comms(), places=5, msg='Total weight in all communities ({0}) not equal to the sum of the weight in all communities ({1}).'.format( s, partition.total_weight_in_all_comms()) ) @data(*graphs) def test_copy(self, graph): if 'weight' in graph.es.attributes() and self.partition_type != leidenalg.SignificanceVertexPartition: partition = self.partition_type(graph, weights='weight') else: partition = self.partition_type(graph) self.optimiser.optimise_partition(partition) partition2 = deepcopy(partition) self.assertAlmostEqual( partition.quality(), partition2.quality(), places=5, msg='Quality of deepcopy ({0}) not equal to quality of original partition ({1}).'.format( partition.quality(), partition2.quality()) ) if (partition2.membership[0] == 0): partition2.move_node(0, 1) else: partition2.move_node(0, 0) self.assertNotEqual( partition.membership[0], partition2.membership[0], msg='Moving node 0 in the deepcopy to community {0} results in community membership {1} for node 0 also in original partition.'.format( partition.membership[0], partition2.membership[0]) ) class ModularityVertexPartitionTest(BaseTest.MutableVertexPartitionTest): def setUp(self): super(ModularityVertexPartitionTest, self).setUp() self.partition_type = leidenalg.ModularityVertexPartition class RBERVertexPartitionTest(BaseTest.MutableVertexPartitionTest): def setUp(self): super(RBERVertexPartitionTest, self).setUp() self.partition_type = leidenalg.RBERVertexPartition class RBConfigurationVertexPartitionTest(BaseTest.MutableVertexPartitionTest): def setUp(self): super(RBConfigurationVertexPartitionTest, self).setUp() self.partition_type = leidenalg.RBConfigurationVertexPartition class CPMVertexPartitionTest(BaseTest.MutableVertexPartitionTest): def setUp(self): super(CPMVertexPartitionTest, self).setUp() self.partition_type = leidenalg.CPMVertexPartition def test_Bipartite(self): graph = bipartite_graph partition, partition_0, partition_1 = \ leidenalg.CPMVertexPartition.Bipartite(graph, resolution_parameter_01=0.2) self.optimiser.optimise_partition_multiplex( [partition, partition_0, partition_1], layer_weights=[1, -1, -1]) self.assertEqual(len(partition), 1) class SurpriseVertexPartitionTest(BaseTest.MutableVertexPartitionTest): def setUp(self): super(SurpriseVertexPartitionTest, self).setUp() self.partition_type = leidenalg.SurpriseVertexPartition class SignificanceVertexPartitionTest(BaseTest.MutableVertexPartitionTest): def setUp(self): super(SignificanceVertexPartitionTest, self).setUp() self.partition_type = leidenalg.SignificanceVertexPartition #%% if __name__ == '__main__': #%% unittest.main(verbosity=3) suite = unittest.TestLoader().discover('.') unittest.TextTestRunner(verbosity=1).run(suite) leidenalg-0.11.0/tests/test_Optimiser.py0000664000175000017510000002001615101156755017601 0ustar nileshnileshimport unittest import igraph as ig import leidenalg from functools import reduce class OptimiserTest(unittest.TestCase): def setUp(self): self.optimiser = leidenalg.Optimiser() def test_move_nodes(self): G = ig.Graph.Full(100) partition = leidenalg.CPMVertexPartition(G, resolution_parameter=0.5) self.optimiser.move_nodes(partition, consider_comms=leidenalg.ALL_NEIGH_COMMS) self.assertListEqual( partition.sizes(), [100], msg="CPMVertexPartition(resolution_parameter=0.5) of complete graph after move nodes incorrect.") def test_move_nodes_with_max_comm_size(self): G = ig.Graph.Full(100) partition = leidenalg.CPMVertexPartition(G, resolution_parameter=0.5) self.optimiser.max_comm_size = 17 self.optimiser.move_nodes(partition, consider_comms=leidenalg.ALL_NEIGH_COMMS) self.assertListEqual( partition.sizes(), [17, 17, 17, 17, 17, 15], msg="CPMVertexPartition(resolution_parameter=0.5) of complete graph after move nodes (max_comm_size=17) incorrect.") def test_move_nodes_with_fixed(self): # One edge plus singleton, but the two connected nodes are fixed G = ig.Graph([(0, 2)]) is_membership_fixed = [True, False, True] partition = leidenalg.CPMVertexPartition( G, resolution_parameter=0.1) self.optimiser.move_nodes(partition, is_membership_fixed=is_membership_fixed, consider_comms=leidenalg.ALL_NEIGH_COMMS) self.assertListEqual( partition.sizes(), [1, 1, 1], msg="CPMVertexPartition(resolution_parameter=0.1) of one edge plus singleton after move nodes with fixed nodes is incorrect.") def test_merge_nodes(self): G = ig.Graph.Full(100) partition = leidenalg.CPMVertexPartition(G, resolution_parameter=0.5) self.optimiser.merge_nodes(partition, consider_comms=leidenalg.ALL_NEIGH_COMMS) self.assertListEqual( partition.sizes(), [100], msg="CPMVertexPartition(resolution_parameter=0.5) of complete graph after merge nodes incorrect.") self.assertEqual( partition.total_weight_in_all_comms(), G.ecount(), msg="total_weight_in_all_comms not equal to ecount of graph.") def test_merge_nodes_with_max_comm_size(self): G = ig.Graph.Full(100) partition = leidenalg.CPMVertexPartition(G, resolution_parameter=0.5) self.optimiser.max_comm_size = 17 self.optimiser.merge_nodes(partition, consider_comms=leidenalg.ALL_NEIGH_COMMS) self.assertListEqual( partition.sizes(), [17, 17, 17, 17, 17, 15], msg="CPMVertexPartition(resolution_parameter=0.5) of complete graph after merge nodes (max_comm_size=17) incorrect.") def test_diff_move_node_optimality(self): G = ig.Graph.Erdos_Renyi(100, p=5./100, directed=False, loops=False) partition = leidenalg.CPMVertexPartition(G, resolution_parameter=0.1) while 0 < self.optimiser.move_nodes(partition, consider_comms=leidenalg.ALL_NEIGH_COMMS): pass for v in G.vs: neigh_comms = set(partition.membership[u.index] for u in v.neighbors()) for c in neigh_comms: self.assertLessEqual( partition.diff_move(v.index, c), 1e-10, # Allow for a small difference up to rounding error. msg="Was able to move a node to a better community, violating node optimality.") def test_optimiser(self): G = reduce(ig.Graph.disjoint_union, (ig.Graph.Tree(10, 3, mode=ig.TREE_UNDIRECTED) for i in range(10))) partition = leidenalg.CPMVertexPartition(G, resolution_parameter=0) self.optimiser.consider_comms=leidenalg.ALL_NEIGH_COMMS self.optimiser.optimise_partition(partition) self.assertListEqual( partition.sizes(), 10*[10], msg="After optimising partition failed to find different components with CPMVertexPartition(resolution_parameter=0)") def test_optimiser_with_max_comm_size(self): G = ig.Graph.Full(100) partition = leidenalg.CPMVertexPartition(G, resolution_parameter=0) self.optimiser.consider_comms=leidenalg.ALL_NEIGH_COMMS self.optimiser.max_comm_size = 10 self.optimiser.optimise_partition(partition) self.assertListEqual( partition.sizes(), 10*[10], msg="After optimising partition (max_comm_size=10) failed to find different components with CPMVertexPartition(resolution_parameter=0)") def test_optimiser_split_with_max_comm_size(self): G = ig.Graph.Full(100) partition = leidenalg.CPMVertexPartition(G, resolution_parameter=0.5) self.optimiser.merge_nodes(partition, consider_comms=leidenalg.ALL_NEIGH_COMMS) self.assertListEqual( partition.sizes(), [100], msg="CPMVertexPartition(resolution_parameter=0.5) of complete graph after merge nodes incorrect.") self.optimiser.max_comm_size = 10 self.optimiser.optimise_partition(partition) self.assertListEqual( partition.sizes(), 10*[10], msg="After optimising partition (max_comm_size=10) failed to find different components with CPMVertexPartition(resolution_parameter=0.5)") def test_optimiser_with_is_membership_fixed(self): G = ig.Graph.Full(3) partition = leidenalg.CPMVertexPartition( G, resolution_parameter=0.01, initial_membership=[2, 1, 0]) # Equivalent to setting initial membership #partition.set_membership([2, 1, 2]) is_membership_fixed = [True, False, False] original_quality = partition.quality() diff = self.optimiser.optimise_partition(partition, is_membership_fixed=is_membership_fixed) self.assertAlmostEqual(partition.quality() - original_quality, diff, places=10, msg="Optimisation with fixed nodes returns inconsistent quality") self.assertListEqual( partition.membership, [2, 2, 2], msg="After optimising partition with fixed nodes failed to recover initial fixed memberships" ) def test_optimiser_is_membership_fixed_large_labels(self): G = ig.Graph.Erdos_Renyi(n=100, p=5./100, directed=True, loops=True) membership = list(range(G.vcount())) partition = leidenalg.RBConfigurationVertexPartition(G, initial_membership=membership) # large enough to force nonconsecutive labels in the final partition fixed_node_idx = 90 is_membership_fixed = [False] * G.vcount() is_membership_fixed[fixed_node_idx] = True original_quality = partition.quality() diff = self.optimiser.optimise_partition(partition, is_membership_fixed=is_membership_fixed) self.assertLess(len(set(partition.membership)), len(partition), msg="Optimisation with fixed nodes yielded too many communities") self.assertAlmostEqual(partition.quality() - original_quality, diff, places=10, msg="Optimisation with fixed nodes returned inconsistent quality") self.assertEqual(partition.membership[fixed_node_idx], fixed_node_idx, msg="Optimisation with fixed nodes failed to keep the associated community labels fixed") def test_neg_weight_bipartite(self): G = ig.Graph.Full_Bipartite(50, 50) G.es['weight'] = -0.1 partition = leidenalg.CPMVertexPartition(G, resolution_parameter=-0.1, weights='weight') self.optimiser.consider_comms=leidenalg.ALL_COMMS self.optimiser.optimise_partition(partition) self.assertListEqual( partition.sizes(), 2*[50], msg="After optimising partition failed to find bipartite structure with CPMVertexPartition(resolution_parameter=-0.1)") def test_resolution_profile(self): G = ig.Graph.Famous('Zachary') profile = self.optimiser.resolution_profile(G, leidenalg.CPMVertexPartition, resolution_range=(0,1)) self.assertListEqual( profile[0].sizes(), [G.vcount()], msg="Resolution profile incorrect: at resolution 0, not equal to a single community for CPM.") self.assertListEqual( profile[-1].sizes(), [1]*G.vcount(), msg="Resolution profile incorrect: at resolution 1, not equal to a singleton partition for CPM.") #%% if __name__ == '__main__': #%% unittest.main(verbosity=3) suite = unittest.TestLoader().discover('.') unittest.TextTestRunner(verbosity=1).run(suite) leidenalg-0.11.0/tests/__init__.py0000664000175000017510000000000015101156755016315 0ustar nileshnileshleidenalg-0.11.0/src/0000775000175000017510000000000015101156755013643 5ustar nileshnileshleidenalg-0.11.0/src/leidenalg/0000775000175000017510000000000015101156755015567 5ustar nileshnileshleidenalg-0.11.0/src/leidenalg/python_partition_interface.cpp0000664000175000017510000011001515101156755023723 0ustar nileshnilesh#include "python_partition_interface.h" Graph* create_graph_from_py(PyObject* py_obj_graph, PyObject* py_node_sizes) { return create_graph_from_py(py_obj_graph, py_node_sizes, NULL, true, false); } Graph* create_graph_from_py(PyObject* py_obj_graph, PyObject* py_node_sizes, PyObject* py_weights) { return create_graph_from_py(py_obj_graph, py_node_sizes, py_weights, true, false); } Graph* create_graph_from_py(PyObject* py_obj_graph, PyObject* py_node_sizes, PyObject* py_weights, bool check_positive_weight, bool correct_self_loops) { #ifdef DEBUG cerr << "create_graph_from_py" << endl; #endif igraph_t* py_graph = (igraph_t*) PyCapsule_GetPointer(py_obj_graph, NULL); #ifdef DEBUG cerr << "Got igraph_t " << py_graph << endl; #endif // If necessary create a weighted graph Graph* graph = NULL; #ifdef DEBUG cerr << "Creating graph."<< endl; #endif size_t n = igraph_vcount(py_graph); size_t m = igraph_ecount(py_graph); vector node_sizes; vector weights; if (py_node_sizes != NULL && py_node_sizes != Py_None) { #ifdef DEBUG cerr << "Reading node_sizes." << endl; #endif size_t nb_node_size = PyList_Size(py_node_sizes); if (nb_node_size != n) { throw Exception("Node size vector not the same size as the number of nodes."); } node_sizes.resize(n); for (size_t v = 0; v < n; v++) { PyObject* py_item = PyList_GetItem(py_node_sizes, v); if (PyNumber_Check(py_item)) { double e = PyFloat_AsDouble(py_item); node_sizes[v] = e; } else { throw Exception("Expected numerical values for node sizes vector."); } } } if (py_weights != NULL && py_weights != Py_None) { #ifdef DEBUG cerr << "Reading weights." << endl; #endif size_t nb_weights = PyList_Size(py_weights); if (nb_weights != m) throw Exception("Weight vector not the same size as the number of edges."); weights.resize(m); for (size_t e = 0; e < m; e++) { PyObject* py_item = PyList_GetItem(py_weights, e); if (PyNumber_Check(py_item)) { weights[e] = PyFloat_AsDouble(py_item); } else { throw Exception("Expected floating point value for weight vector."); } if (check_positive_weight) if (weights[e] < 0 ) throw Exception("Cannot accept negative weights."); if (isnan(weights[e])) throw Exception("Cannot accept NaN weights."); if (!isfinite(weights[e])) throw Exception("Cannot accept infinite weights."); } } if (node_sizes.size() == n) { if (weights.size() == m) graph = new Graph(py_graph, weights, node_sizes, correct_self_loops); else graph = Graph::GraphFromNodeSizes(py_graph, node_sizes, correct_self_loops); } else { if (weights.size() == m) graph = Graph::GraphFromEdgeWeights(py_graph, weights, correct_self_loops); else graph = new Graph(py_graph, correct_self_loops); } #ifdef DEBUG cerr << "Created graph " << graph << endl; cerr << "Number of nodes " << graph->vcount() << endl; cerr << "Number of edges " << graph->ecount() << endl; cerr << "Total weight " << graph->total_weight() << endl; #endif return graph; } vector create_size_t_vector(PyObject* py_list) { size_t n = PyList_Size(py_list); vector result(n); for (size_t i = 0; i < n; i++) { PyObject* py_item = PyList_GetItem(py_list, i); if (PyNumber_Check(py_item) && PyIndex_Check(py_item)) { size_t e = PyLong_AsSize_t(PyNumber_Long(py_item)); if (e >= n) throw Exception("Value cannot exceed length of list."); else result[i] = e; } else throw Exception("Value cannot exceed length of list."); } return result; } PyObject* capsule_MutableVertexPartition(MutableVertexPartition* partition) { PyObject* py_partition = PyCapsule_New(partition, "leidenalg.VertexPartition.MutableVertexPartition", del_MutableVertexPartition); return py_partition; } MutableVertexPartition* decapsule_MutableVertexPartition(PyObject* py_partition) { MutableVertexPartition* partition = (MutableVertexPartition*) PyCapsule_GetPointer(py_partition, "leidenalg.VertexPartition.MutableVertexPartition"); return partition; } void del_MutableVertexPartition(PyObject* py_partition) { MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); delete partition; } #ifdef __cplusplus extern "C" { #endif PyObject* _new_ModularityVertexPartition(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_obj_graph = NULL; PyObject* py_initial_membership = NULL; PyObject* py_weights = NULL; static const char* kwlist[] = {"graph", "initial_membership", "weights", NULL}; if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|OO", (char**) kwlist, &py_obj_graph, &py_initial_membership, &py_weights)) return NULL; try { Graph* graph = create_graph_from_py(py_obj_graph, NULL, py_weights); ModularityVertexPartition* partition = NULL; // If necessary create an initial partition if (py_initial_membership != NULL && py_initial_membership != Py_None) { vector initial_membership = create_size_t_vector(py_initial_membership); partition = new ModularityVertexPartition(graph, initial_membership); } else partition = new ModularityVertexPartition(graph); // Do *NOT* forget to remove the graph upon deletion partition->destructor_delete_graph = true; PyObject* py_partition = capsule_MutableVertexPartition(partition); #ifdef DEBUG cerr << "Created capsule partition at address " << py_partition << endl; #endif return py_partition; } catch (std::exception& e ) { string s = "Could not construct partition: " + string(e.what()); PyErr_SetString(PyExc_BaseException, s.c_str()); return NULL; } } PyObject* _new_SignificanceVertexPartition(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_obj_graph = NULL; PyObject* py_initial_membership = NULL; PyObject* py_node_sizes = NULL; static const char* kwlist[] = {"graph", "initial_membership", "node_sizes", NULL}; if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|OO", (char**) kwlist, &py_obj_graph, &py_initial_membership, &py_node_sizes)) return NULL; try { Graph* graph = create_graph_from_py(py_obj_graph, py_node_sizes); SignificanceVertexPartition* partition = NULL; // If necessary create an initial partition if (py_initial_membership != NULL && py_initial_membership != Py_None) { vector initial_membership = create_size_t_vector(py_initial_membership); partition = new SignificanceVertexPartition(graph, initial_membership); } else partition = new SignificanceVertexPartition(graph); // Do *NOT* forget to remove the graph upon deletion partition->destructor_delete_graph = true; PyObject* py_partition = capsule_MutableVertexPartition(partition); #ifdef DEBUG cerr << "Created capsule partition at address " << py_partition << endl; #endif return py_partition; } catch (std::exception const & e ) { string s = "Could not construct partition: " + string(e.what()); PyErr_SetString(PyExc_BaseException, s.c_str()); return NULL; } } PyObject* _new_SurpriseVertexPartition(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_obj_graph = NULL; PyObject* py_initial_membership = NULL; PyObject* py_weights = NULL; PyObject* py_node_sizes = NULL; static const char* kwlist[] = {"graph", "initial_membership", "weights", "node_sizes", NULL}; if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|OOO", (char**) kwlist, &py_obj_graph, &py_initial_membership, &py_weights, &py_node_sizes)) return NULL; try { Graph* graph = create_graph_from_py(py_obj_graph, py_node_sizes, py_weights); SurpriseVertexPartition* partition = NULL; // If necessary create an initial partition if (py_initial_membership != NULL && py_initial_membership != Py_None) { vector initial_membership = create_size_t_vector(py_initial_membership); partition = new SurpriseVertexPartition(graph, initial_membership); } else partition = new SurpriseVertexPartition(graph); // Do *NOT* forget to remove the graph upon deletion partition->destructor_delete_graph = true; PyObject* py_partition = capsule_MutableVertexPartition(partition); #ifdef DEBUG cerr << "Created capsule partition at address " << py_partition << endl; #endif return py_partition; } catch (std::exception const & e ) { string s = "Could not construct partition: " + string(e.what()); PyErr_SetString(PyExc_BaseException, s.c_str()); return NULL; } } PyObject* _new_CPMVertexPartition(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_obj_graph = NULL; PyObject* py_initial_membership = NULL; PyObject* py_weights = NULL; PyObject* py_node_sizes = NULL; double resolution_parameter = 1.0; int correct_self_loops = false; static const char* kwlist[] = {"graph", "initial_membership", "weights", "node_sizes", "resolution_parameter", "correct_self_loops", NULL}; if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|OOOdp", (char**) kwlist, &py_obj_graph, &py_initial_membership, &py_weights, &py_node_sizes, &resolution_parameter, &correct_self_loops)) return NULL; try { Graph* graph = create_graph_from_py(py_obj_graph, py_node_sizes, py_weights, false, correct_self_loops); CPMVertexPartition* partition = NULL; // If necessary create an initial partition if (py_initial_membership != NULL && py_initial_membership != Py_None) { vector initial_membership = create_size_t_vector(py_initial_membership); partition = new CPMVertexPartition(graph, initial_membership, resolution_parameter); } else partition = new CPMVertexPartition(graph, resolution_parameter); // Do *NOT* forget to remove the graph upon deletion partition->destructor_delete_graph = true; PyObject* py_partition = capsule_MutableVertexPartition(partition); #ifdef DEBUG cerr << "Created capsule partition at address " << py_partition << endl; #endif return py_partition; } catch (std::exception const & e ) { string s = "Could not construct partition: " + string(e.what()); PyErr_SetString(PyExc_BaseException, s.c_str()); return NULL; } } PyObject* _new_RBERVertexPartition(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_obj_graph = NULL; PyObject* py_initial_membership = NULL; PyObject* py_weights = NULL; PyObject* py_node_sizes = NULL; double resolution_parameter = 1.0; static const char* kwlist[] = {"graph", "initial_membership", "weights", "node_sizes", "resolution_parameter", NULL}; if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|OOOd", (char**) kwlist, &py_obj_graph, &py_initial_membership, &py_weights, &py_node_sizes, &resolution_parameter)) return NULL; try { Graph* graph = create_graph_from_py(py_obj_graph, py_node_sizes, py_weights); RBERVertexPartition* partition = NULL; // If necessary create an initial partition if (py_initial_membership != NULL && py_initial_membership != Py_None) { vector initial_membership = create_size_t_vector(py_initial_membership); partition = new RBERVertexPartition(graph, initial_membership, resolution_parameter); } else partition = new RBERVertexPartition(graph, resolution_parameter); // Do *NOT* forget to remove the graph upon deletion partition->destructor_delete_graph = true; PyObject* py_partition = capsule_MutableVertexPartition(partition); #ifdef DEBUG cerr << "Created capsule partition at address " << py_partition << endl; #endif return py_partition; } catch (std::exception const & e ) { string s = "Could not construct partition: " + string(e.what()); PyErr_SetString(PyExc_BaseException, s.c_str()); return NULL; } } PyObject* _new_RBConfigurationVertexPartition(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_obj_graph = NULL; PyObject* py_initial_membership = NULL; PyObject* py_weights = NULL; double resolution_parameter = 1.0; static const char* kwlist[] = {"graph", "initial_membership", "weights", "resolution_parameter", NULL}; if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|OOd", (char**) kwlist, &py_obj_graph, &py_initial_membership, &py_weights, &resolution_parameter)) return NULL; try { Graph* graph = create_graph_from_py(py_obj_graph, NULL, py_weights); RBConfigurationVertexPartition* partition = NULL; // If necessary create an initial partition if (py_initial_membership != NULL && py_initial_membership != Py_None) { vector initial_membership = create_size_t_vector(py_initial_membership); partition = new RBConfigurationVertexPartition(graph, initial_membership, resolution_parameter); } else partition = new RBConfigurationVertexPartition(graph, resolution_parameter); // Do *NOT* forget to remove the graph upon deletion partition->destructor_delete_graph = true; PyObject* py_partition = capsule_MutableVertexPartition(partition); #ifdef DEBUG cerr << "Created capsule partition at address " << py_partition << endl; #endif return py_partition; } catch (std::exception const & e ) { string s = "Could not construct partition: " + string(e.what()); PyErr_SetString(PyExc_BaseException, s.c_str()); return NULL; } } PyObject* _MutableVertexPartition_get_py_igraph(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; static const char* kwlist[] = {"partition", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_partition)) return NULL; #ifdef DEBUG cerr << "get_py_igraph();" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif Graph* graph = partition->get_graph(); size_t n = graph->vcount(); size_t m = graph->ecount(); PyObject* edges = PyList_New(m); for (size_t e = 0; e < m; e++) { vector edge = graph->edge(e); PyList_SetItem(edges, e, Py_BuildValue("(nn)", edge[0], edge[1])); } PyObject* weights = PyList_New(m); for (size_t e = 0; e < m; e++) { PyObject* item = PyFloat_FromDouble(graph->edge_weight(e)); PyList_SetItem(weights, e, item); } PyObject* node_sizes = PyList_New(n); for (size_t v = 0; v < n; v++) { PyObject* item = PyLong_FromSize_t(graph->node_size(v)); PyList_SetItem(node_sizes, v, item); } return Py_BuildValue("lOOOO", n, graph->is_directed() ? Py_True : Py_False, edges, weights, node_sizes); } PyObject* _MutableVertexPartition_from_coarse_partition(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; PyObject* py_membership = NULL; PyObject* py_coarse_node = NULL; static const char* kwlist[] = {"partition", "membership", "coarse_node", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif // TODO : Instead of simply returning NULL, we should also set an error. if (!PyArg_ParseTupleAndKeywords(args, keywds, "OO|O", (char**) kwlist, &py_partition, &py_membership, &py_coarse_node)) return NULL; #ifdef DEBUG cerr << "from_coarse_partition();" << endl; #endif vector membership; try { membership = create_size_t_vector(py_membership); } catch (std::exception& e ) { string s = "Could not create membership vector: " + string(e.what()); PyErr_SetString(PyExc_BaseException, s.c_str()); return NULL; } #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif if (py_coarse_node != NULL && py_coarse_node != Py_None) { vector coarse_node; try { coarse_node = create_size_t_vector(py_coarse_node); } catch (std::exception& e ) { string s = "Could not create coarse node vector: " + string(e.what()); PyErr_SetString(PyExc_BaseException, s.c_str()); return NULL; } partition->from_coarse_partition(membership, coarse_node); } else partition->from_coarse_partition(membership); Py_INCREF(Py_None); return Py_None; } PyObject* _MutableVertexPartition_renumber_communities(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; static const char* kwlist[] = {"partition", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_partition)) return NULL; #ifdef DEBUG cerr << "renumber_communities();" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif partition->renumber_communities(); Py_INCREF(Py_None); return Py_None; } PyObject* _MutableVertexPartition_diff_move(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; size_t v; size_t new_comm; static const char* kwlist[] = {"partition", "v", "new_comm", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "Onn", (char**) kwlist, &py_partition, &v, &new_comm)) return NULL; #ifdef DEBUG cerr << "diff_move(" << v << ", " << new_comm << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif double diff = partition->diff_move(v, new_comm); return PyFloat_FromDouble(diff); } PyObject* _MutableVertexPartition_move_node(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; size_t v; size_t new_comm; static const char* kwlist[] = {"partition", "v", "new_comm", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "Onn", (char**) kwlist, &py_partition, &v, &new_comm)) return NULL; #ifdef DEBUG cerr << "move_node(" << v << ", " << new_comm << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif if (new_comm >= partition->get_graph()->vcount()) { PyErr_SetString(PyExc_TypeError, "Community membership cannot exceed number of nodes."); return NULL; } else if (new_comm < 0) { PyErr_SetString(PyExc_TypeError, "Community membership cannot be negative"); return NULL; } partition->move_node(v, new_comm); Py_INCREF(Py_None); return Py_None; } PyObject* _MutableVertexPartition_quality(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; static const char* kwlist[] = {"partition", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_partition)) return NULL; #ifdef DEBUG cerr << "quality();" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif double q = partition->quality(); return PyFloat_FromDouble(q); } PyObject* _MutableVertexPartition_aggregate_partition(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; static const char* kwlist[] = {"partition", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_partition)) return NULL; #ifdef DEBUG cerr << "aggregate_partition();" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif // First collapse graph (i.e. community graph) Graph* collapsed_graph = partition->get_graph()->collapse_graph(partition); // Create collapsed partition (i.e. default partition of each node in its own community). MutableVertexPartition* collapsed_partition = partition->create(collapsed_graph); collapsed_partition->destructor_delete_graph = true; PyObject* py_collapsed_partition = capsule_MutableVertexPartition(collapsed_partition); return py_collapsed_partition; } PyObject* _MutableVertexPartition_total_weight_in_comm(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; size_t comm; static const char* kwlist[] = {"partition", "comm", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "On", (char**) kwlist, &py_partition, &comm)) return NULL; #ifdef DEBUG cerr << "total_weight_in_comm(" << comm << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif if (comm >= partition->n_communities()) { PyErr_SetString(PyExc_IndexError, "Try to index beyond the number of communities."); return NULL; } double w = partition->total_weight_in_comm(comm); return PyFloat_FromDouble(w); } PyObject* _MutableVertexPartition_total_weight_from_comm(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; size_t comm; static const char* kwlist[] = {"partition", "comm", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "On", (char**) kwlist, &py_partition, &comm)) return NULL; #ifdef DEBUG cerr << "total_weight_from_comm(" << comm << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); if (comm >= partition->n_communities()) { PyErr_SetString(PyExc_IndexError, "Try to index beyond the number of communities."); return NULL; } #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif double w = partition->total_weight_from_comm(comm); return PyFloat_FromDouble(w); } PyObject* _MutableVertexPartition_total_weight_to_comm(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; size_t comm; static const char* kwlist[] = {"partition", "comm", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "On", (char**) kwlist, &py_partition, &comm)) return NULL; #ifdef DEBUG cerr << "total_weight_to_comm(" << comm << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); if (comm >= partition->n_communities()) { PyErr_SetString(PyExc_IndexError, "Try to index beyond the number of communities."); return NULL; } #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif double w = partition->total_weight_to_comm(comm); return PyFloat_FromDouble(w); } PyObject* _MutableVertexPartition_total_weight_in_all_comms(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; static const char* kwlist[] = {"partition", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_partition)) return NULL; #ifdef DEBUG cerr << "total_weight_in_all_comms();" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif double w = partition->total_weight_in_all_comms(); return PyFloat_FromDouble(w); } PyObject* _MutableVertexPartition_total_possible_edges_in_all_comms(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; static const char* kwlist[] = {"partition", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_partition)) return NULL; #ifdef DEBUG cerr << "total_possible_edges_in_all_comms();" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif size_t e = partition->total_possible_edges_in_all_comms(); return PyLong_FromSize_t(e); } PyObject* _MutableVertexPartition_weight_to_comm(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; size_t v; size_t comm; static const char* kwlist[] = {"partition", "v", "comm", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "Onn", (char**) kwlist, &py_partition, &v, &comm)) return NULL; #ifdef DEBUG cerr << "weight_to_comm(" << v << ", " << comm << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); if (comm >= partition->n_communities()) { PyErr_SetString(PyExc_IndexError, "Try to index beyond the number of communities."); return NULL; } if (v >= partition->get_graph()->vcount()) { PyErr_SetString(PyExc_IndexError, "Try to index beyond the number of nodes."); return NULL; } #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif double diff = partition->weight_to_comm(v, comm); return PyFloat_FromDouble(diff); } PyObject* _MutableVertexPartition_weight_from_comm(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; size_t v; size_t comm; static const char* kwlist[] = {"partition", "v", "comm", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "Onn", (char**) kwlist, &py_partition, &v, &comm)) return NULL; #ifdef DEBUG cerr << "weight_to_comm(" << v << ", " << comm << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); if (comm >= partition->n_communities()) { PyErr_SetString(PyExc_IndexError, "Try to index beyond the number of communities."); return NULL; } if (v >= partition->get_graph()->vcount()) { PyErr_SetString(PyExc_IndexError, "Try to index beyond the number of nodes."); return NULL; } #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif double diff = partition->weight_to_comm(v, comm); return PyFloat_FromDouble(diff); } PyObject* _MutableVertexPartition_get_membership(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; static const char* kwlist[] = {"partition", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_partition)) return NULL; #ifdef DEBUG cerr << "get_membership();" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif size_t n = partition->get_graph()->vcount(); PyObject* py_membership = PyList_New(n); for (size_t v = 0; v < n; v++) { PyObject* item = PyLong_FromSize_t(partition->membership(v)); PyList_SetItem(py_membership, v, item); } return py_membership; } PyObject* _MutableVertexPartition_set_membership(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; PyObject* py_membership = NULL; static const char* kwlist[] = {"partition", "membership", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "OO", (char**) kwlist, &py_partition, &py_membership)) return NULL; #ifdef DEBUG cerr << "set_membership();" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif try { partition->set_membership(create_size_t_vector(py_membership)); } catch (std::exception& e ) { string s = "Could not set membership: " + string(e.what()); PyErr_SetString(PyExc_BaseException, s.c_str()); return NULL; } #ifdef DEBUG cerr << "Exiting set_membership();" << endl; #endif Py_INCREF(Py_None); return Py_None; } PyObject* _ResolutionParameterVertexPartition_get_resolution(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; static const char* kwlist[] = {"partition", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_partition)) return NULL; #ifdef DEBUG cerr << "get_resolution();" << endl; #endif #ifdef DEBUG cerr << "Capsule ResolutionParameterVertexPartition at address " << py_partition << endl; #endif ResolutionParameterVertexPartition* partition = (ResolutionParameterVertexPartition*)decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using ResolutionParameterVertexPartition at address " << partition << endl; #endif return PyFloat_FromDouble(partition->resolution_parameter); } PyObject* _ResolutionParameterVertexPartition_set_resolution(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; double resolution_parameter = 1.0; static const char* kwlist[] = {"partition", "resolution_parameter", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "Od", (char**) kwlist, &py_partition, &resolution_parameter)) return NULL; #ifdef DEBUG cerr << "set_resolution(" << resolution_parameter << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule ResolutionParameterVertexPartition at address " << py_partition << endl; #endif ResolutionParameterVertexPartition* partition = (ResolutionParameterVertexPartition*)decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using ResolutionParameterVertexPartition at address " << partition << endl; #endif partition->resolution_parameter = resolution_parameter; Py_INCREF(Py_None); return Py_None; } PyObject* _ResolutionParameterVertexPartition_quality(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_partition = NULL; PyObject* py_res = NULL; double resolution_parameter = 0.0; static const char* kwlist[] = {"partition", "resolution_parameter", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|O", (char**) kwlist, &py_partition, &py_res)) return NULL; #ifdef DEBUG cerr << "quality();" << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif ResolutionParameterVertexPartition* partition = (ResolutionParameterVertexPartition*)decapsule_MutableVertexPartition(py_partition); if (py_res != NULL && py_res != Py_None) { if (PyNumber_Check(py_res)) { resolution_parameter = PyFloat_AsDouble(py_res); } else { PyErr_SetString(PyExc_TypeError, "Expected floating point value for resolution parameter."); return NULL; } if (isnan(resolution_parameter)) { PyErr_SetString(PyExc_TypeError, "Cannot accept NaN resolution parameter."); return NULL; } } else resolution_parameter = partition->resolution_parameter; #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif double q = partition->quality(resolution_parameter); return PyFloat_FromDouble(q); } #ifdef __cplusplus } #endif leidenalg-0.11.0/src/leidenalg/python_optimiser_interface.cpp0000664000175000017510000007065615101156755023745 0ustar nileshnilesh#include "python_optimiser_interface.h" PyObject* capsule_Optimiser(Optimiser* optimiser) { PyObject* py_optimiser = PyCapsule_New(optimiser, "leidenalg.Optimiser", del_Optimiser); return py_optimiser; } Optimiser* decapsule_Optimiser(PyObject* py_optimiser) { Optimiser* optimiser = (Optimiser*) PyCapsule_GetPointer(py_optimiser, "leidenalg.Optimiser"); return optimiser; } void del_Optimiser(PyObject* py_optimiser) { Optimiser* optimiser = decapsule_Optimiser(py_optimiser); delete optimiser; } #ifdef __cplusplus extern "C" { #endif PyObject* _new_Optimiser(PyObject *self, PyObject *args) { if (args != NULL) { PyErr_BadArgument(); return NULL; } Optimiser* optimiser = new Optimiser(); PyObject* py_optimiser = capsule_Optimiser(optimiser); return py_optimiser; } PyObject* _Optimiser_optimise_partition(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; PyObject* py_partition = NULL; PyObject* py_is_membership_fixed = NULL; static const char* kwlist[] = {"optimiser", "partition", "is_membership_fixed", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "OO|O", (char**) kwlist, &py_optimiser, &py_partition, &py_is_membership_fixed)) return NULL; #ifdef DEBUG cerr << "optimise_partition(" << py_partition << ", is_membership_fixed=" << py_is_membership_fixed << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif size_t n = partition->get_graph()->vcount(); vector is_membership_fixed(n, false); if (py_is_membership_fixed != NULL && py_is_membership_fixed != Py_None) { #ifdef DEBUG cerr << "Reading is_membership_fixed." << endl; #endif size_t nb_is_membership_fixed = PyList_Size(py_is_membership_fixed); if (nb_is_membership_fixed != n) { PyErr_SetString(PyExc_ValueError, "Membership fixed vector not the same size as the number of nodes."); return NULL; } for (size_t v = 0; v < n; v++) { PyObject* py_item = PyList_GetItem(py_is_membership_fixed, v); is_membership_fixed[v] = PyObject_IsTrue(py_item); } } double q = 0.0; try { q = optimiser->optimise_partition(partition, is_membership_fixed); } catch (std::exception& e) { PyErr_SetString(PyExc_ValueError, e.what()); return NULL; } return PyFloat_FromDouble(q); } PyObject* _Optimiser_optimise_partition_multiplex(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; PyObject* py_partitions = NULL; PyObject* py_layer_weights = NULL; PyObject* py_is_membership_fixed = NULL; static const char* kwlist[] = {"optimiser", "partitions", "layer_weights", "is_membership_fixed", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "OOO|O", (char**) kwlist, &py_optimiser, &py_partitions, &py_layer_weights, &py_is_membership_fixed)) return NULL; size_t nb_partitions = (size_t)PyList_Size(py_partitions); if (nb_partitions != (size_t)PyList_Size(py_layer_weights)) { PyErr_SetString(PyExc_ValueError, "Number of layer weights does not equal the number of partitions"); return NULL; } #ifdef DEBUG cerr << "Parsing " << nb_partitions << " partitions." << endl; #endif // This is all done per layer. vector partitions(nb_partitions); vector layer_weights(nb_partitions, 1.0); for (size_t layer = 0; layer < nb_partitions; layer++) { PyObject* py_partition = PyList_GetItem(py_partitions, layer); #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif PyObject* layer_weight = PyList_GetItem(py_layer_weights, layer); partitions[layer] = partition; if (PyNumber_Check(layer_weight)) { layer_weights[layer] = PyFloat_AsDouble(layer_weight); } else { PyErr_SetString(PyExc_TypeError, "Expected floating value for layer weight."); return NULL; } if (isnan(layer_weights[layer])) { PyErr_SetString(PyExc_TypeError, "Cannot accept NaN weights."); return NULL; } } if (nb_partitions == 0) return NULL; size_t n = partitions[0]->get_graph()->vcount(); vector is_membership_fixed(n, false); if (py_is_membership_fixed != NULL && py_is_membership_fixed != Py_None) { #ifdef DEBUG cerr << "Reading is_membership_fixed." << endl; #endif size_t nb_is_membership_fixed = PyList_Size(py_is_membership_fixed); if (nb_is_membership_fixed != n) { PyErr_SetString(PyExc_TypeError, "Membership fixed vector not the same size as the number of nodes."); return NULL; } for (size_t v = 0; v < n; v++) { PyObject* py_item = PyList_GetItem(py_is_membership_fixed, v); is_membership_fixed[v] = PyObject_IsTrue(py_item); } } #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif double q = 0.0; try { q = optimiser->optimise_partition(partitions, layer_weights, is_membership_fixed); } catch (std::exception& e) { PyErr_SetString(PyExc_ValueError, e.what()); return NULL; } return PyFloat_FromDouble(q); } PyObject* _Optimiser_move_nodes(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; PyObject* py_partition = NULL; PyObject* py_is_membership_fixed = NULL; int consider_comms = -1; static const char* kwlist[] = {"optimiser", "partition", "is_membership_fixed", "consider_comms", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "OO|Oi", (char**) kwlist, &py_optimiser, &py_partition, &py_is_membership_fixed, &consider_comms)) return NULL; #ifdef DEBUG cerr << "optimise_partition(" << py_partition << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif size_t n = partition->get_graph()->vcount(); vector is_membership_fixed(n, false); if (py_is_membership_fixed != NULL && py_is_membership_fixed != Py_None) { #ifdef DEBUG cerr << "Reading is_membership_fixed." << endl; #endif size_t nb_is_membership_fixed = PyList_Size(py_is_membership_fixed); if (nb_is_membership_fixed != n) { PyErr_SetString(PyExc_TypeError, "Membership fixed vector not the same size as the number of nodes."); return NULL; } for (size_t v = 0; v < n; v++) { PyObject* py_item = PyList_GetItem(py_is_membership_fixed, v); is_membership_fixed[v] = PyObject_IsTrue(py_item); } } if (consider_comms < 0) consider_comms = optimiser->consider_comms; double q = 0.0; try { q = optimiser->move_nodes(partition, is_membership_fixed, consider_comms, true); } catch (std::exception& e) { PyErr_SetString(PyExc_ValueError, e.what()); return NULL; } return PyFloat_FromDouble(q); } PyObject* _Optimiser_merge_nodes(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; PyObject* py_partition = NULL; PyObject* py_is_membership_fixed = NULL; int consider_comms = -1; static const char* kwlist[] = {"optimiser", "partition", "is_membership_fixed", "consider_comms", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "OO|Oi", (char**) kwlist, &py_optimiser, &py_partition, &py_is_membership_fixed, &consider_comms)) return NULL; #ifdef DEBUG cerr << "optimise_partition(" << py_partition << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif size_t n = partition->get_graph()->vcount(); vector is_membership_fixed(n, false); if (py_is_membership_fixed != NULL && py_is_membership_fixed != Py_None) { #ifdef DEBUG cerr << "Reading is_membership_fixed." << endl; #endif size_t nb_is_membership_fixed = PyList_Size(py_is_membership_fixed); if (nb_is_membership_fixed != n) { PyErr_SetString(PyExc_TypeError, "Membership fixed vector not the same size as the number of nodes."); return NULL; } for (size_t v = 0; v < n; v++) { PyObject* py_item = PyList_GetItem(py_is_membership_fixed, v); is_membership_fixed[v] = PyObject_IsTrue(py_item); } } if (consider_comms < 0) consider_comms = optimiser->consider_comms; double q = 0.0; try { q = optimiser->merge_nodes(partition, is_membership_fixed, consider_comms, true); } catch (std::exception& e) { PyErr_SetString(PyExc_ValueError, e.what()); return NULL; } return PyFloat_FromDouble(q); } PyObject* _Optimiser_move_nodes_constrained(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; PyObject* py_partition = NULL; PyObject* py_constrained_partition = NULL; int consider_comms = -1; static const char* kwlist[] = {"optimiser", "partition", "constrained_partition", "consider_comms", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "OOO|i", (char**) kwlist, &py_optimiser, &py_partition, &py_constrained_partition, &consider_comms)) return NULL; #ifdef DEBUG cerr << "optimise_partition(" << py_partition << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif #ifdef DEBUG cerr << "Capsule constrained partition at address " << py_constrained_partition << endl; #endif MutableVertexPartition* constrained_partition = decapsule_MutableVertexPartition(py_constrained_partition); #ifdef DEBUG cerr << "Using constrained partition at address " << constrained_partition << endl; #endif if (consider_comms < 0) consider_comms = optimiser->refine_consider_comms; double q = 0.0; try { q = optimiser->move_nodes_constrained(partition, consider_comms, constrained_partition); } catch (std::exception& e) { PyErr_SetString(PyExc_ValueError, e.what()); return NULL; } return PyFloat_FromDouble(q); } PyObject* _Optimiser_merge_nodes_constrained(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; PyObject* py_partition = NULL; PyObject* py_constrained_partition = NULL; int consider_comms = -1; static const char* kwlist[] = {"optimiser", "partition", "constrained_partition", "consider_comms", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "OOO|i", (char**) kwlist, &py_optimiser, &py_partition, &py_constrained_partition, &consider_comms)) return NULL; #ifdef DEBUG cerr << "optimise_partition(" << py_partition << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif #ifdef DEBUG cerr << "Capsule partition at address " << py_partition << endl; #endif MutableVertexPartition* partition = decapsule_MutableVertexPartition(py_partition); #ifdef DEBUG cerr << "Using partition at address " << partition << endl; #endif #ifdef DEBUG cerr << "Capsule constrained partition at address " << py_partition << endl; #endif MutableVertexPartition* constrained_partition = decapsule_MutableVertexPartition(py_constrained_partition); #ifdef DEBUG cerr << "Using constrained partition at address " << partition << endl; #endif if (consider_comms < 0) consider_comms = optimiser->refine_consider_comms; double q = 0.0; try { q = optimiser->merge_nodes_constrained(partition, consider_comms, constrained_partition); } catch (std::exception& e) { PyErr_SetString(PyExc_ValueError, e.what()); return NULL; } return PyFloat_FromDouble(q); } PyObject* _Optimiser_set_consider_comms(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; int consider_comms = Optimiser::ALL_NEIGH_COMMS; static const char* kwlist[] = {"optimiser", "consider_comms", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oi", (char**) kwlist, &py_optimiser, &consider_comms)) return NULL; #ifdef DEBUG cerr << "set_consider_comms(" << consider_comms << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif optimiser->consider_comms = consider_comms; Py_INCREF(Py_None); return Py_None; } PyObject* _Optimiser_get_consider_comms(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; static const char* kwlist[] = {"optimiser", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_optimiser)) return NULL; #ifdef DEBUG cerr << "get_consider_comms();" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif return PyLong_FromLong(optimiser->consider_comms); } PyObject* _Optimiser_set_refine_consider_comms(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; int refine_consider_comms = Optimiser::ALL_NEIGH_COMMS; static const char* kwlist[] = {"optimiser", "refine_consider_comms", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oi", (char**) kwlist, &py_optimiser, &refine_consider_comms)) return NULL; #ifdef DEBUG cerr << "set_refine_consider_comms(" << refine_consider_comms << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif optimiser->refine_consider_comms = refine_consider_comms; Py_INCREF(Py_None); return Py_None; } PyObject* _Optimiser_get_refine_consider_comms(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; static const char* kwlist[] = {"optimiser", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_optimiser)) return NULL; #ifdef DEBUG cerr << "get_refine_consider_comms();" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif return PyLong_FromLong(optimiser->refine_consider_comms); } PyObject* _Optimiser_set_optimise_routine(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; int optimise_routine = Optimiser::ALL_NEIGH_COMMS; static const char* kwlist[] = {"optimiser", "optimise_routine", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oi", (char**) kwlist, &py_optimiser, &optimise_routine)) return NULL; #ifdef DEBUG cerr << "set_optimise_routine(" << optimise_routine << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif optimiser->optimise_routine = optimise_routine; Py_INCREF(Py_None); return Py_None; } PyObject* _Optimiser_get_optimise_routine(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; static const char* kwlist[] = {"optimiser", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_optimiser)) return NULL; #ifdef DEBUG cerr << "get_optimise_routine();" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif return PyLong_FromLong(optimiser->optimise_routine); } PyObject* _Optimiser_set_refine_routine(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; int refine_routine = Optimiser::ALL_NEIGH_COMMS; static const char* kwlist[] = {"optimiser", "refine_routine", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oi", (char**) kwlist, &py_optimiser, &refine_routine)) return NULL; #ifdef DEBUG cerr << "set_refine_routine(" << refine_routine << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif optimiser->refine_routine = refine_routine; Py_INCREF(Py_None); return Py_None; } PyObject* _Optimiser_get_refine_routine(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; static const char* kwlist[] = {"optimiser", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_optimiser)) return NULL; #ifdef DEBUG cerr << "get_refine_routine();" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif return PyLong_FromLong(optimiser->refine_routine); } PyObject* _Optimiser_set_consider_empty_community(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; int consider_empty_community = true; static const char* kwlist[] = {"optimiser", "consider_empty_community", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oi", (char**) kwlist, &py_optimiser, &consider_empty_community)) return NULL; #ifdef DEBUG cerr << "set_consider_empty_community(" << consider_empty_community << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif #ifdef DEBUG cerr << "Setting consider_empty_community to " << consider_empty_community << endl; #endif optimiser->consider_empty_community = consider_empty_community; #ifdef DEBUG cerr << "Set consider_empty_community to " << optimiser->consider_empty_community << endl; #endif Py_INCREF(Py_None); return Py_None; } PyObject* _Optimiser_get_consider_empty_community(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; static const char* kwlist[] = {"optimiser", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_optimiser)) return NULL; #ifdef DEBUG cerr << "get_consider_empty_community();" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; cerr << "Returning " << optimiser->consider_empty_community << endl; #endif return PyBool_FromLong(optimiser->consider_empty_community); } PyObject* _Optimiser_set_refine_partition(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; int refine_partition = false; static const char* kwlist[] = {"optimiser", "refine_partition", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oi", (char**) kwlist, &py_optimiser, &refine_partition)) return NULL; #ifdef DEBUG cerr << "set_refine_partition(" << refine_partition << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif optimiser->refine_partition = refine_partition; Py_INCREF(Py_None); return Py_None; } PyObject* _Optimiser_get_refine_partition(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; static const char* kwlist[] = {"optimiser", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_optimiser)) return NULL; #ifdef DEBUG cerr << "get_refine_partition();" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif return PyBool_FromLong(optimiser->refine_partition); } PyObject* _Optimiser_set_max_comm_size(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; size_t max_comm_size = 0; static const char* kwlist[] = {"optimiser", "max_comm_size", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "On", (char**) kwlist, &py_optimiser, &max_comm_size)) return NULL; #ifdef DEBUG cerr << "set_max_comm_size(" << max_comm_size << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif optimiser->max_comm_size = max_comm_size; Py_INCREF(Py_None); return Py_None; } PyObject* _Optimiser_get_max_comm_size(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; static const char* kwlist[] = {"optimiser", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "O", (char**) kwlist, &py_optimiser)) return NULL; #ifdef DEBUG cerr << "get_max_comm_size();" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif return PyLong_FromSize_t(optimiser->max_comm_size); } PyObject* _Optimiser_set_rng_seed(PyObject *self, PyObject *args, PyObject *keywds) { PyObject* py_optimiser = NULL; int seed = 0; static const char* kwlist[] = {"optimiser", "seed", NULL}; #ifdef DEBUG cerr << "Parsing arguments..." << endl; #endif if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oi", (char**) kwlist, &py_optimiser, &seed)) return NULL; #ifdef DEBUG cerr << "set_rng_seed(" << seed << ");" << endl; #endif #ifdef DEBUG cerr << "Capsule optimiser at address " << py_optimiser << endl; #endif Optimiser* optimiser = decapsule_Optimiser(py_optimiser); #ifdef DEBUG cerr << "Using optimiser at address " << optimiser << endl; #endif #ifdef DEBUG cerr << "Setting seed to " << seed << endl; #endif optimiser->set_rng_seed(seed); Py_INCREF(Py_None); return Py_None; } #ifdef __cplusplus } #endif leidenalg-0.11.0/src/leidenalg/pynterface.cpp0000664000175000017510000000003015101156755020424 0ustar nileshnilesh#include "pynterface.h" leidenalg-0.11.0/src/leidenalg/functions.py0000664000175000017510000004744315101156755020165 0ustar nileshnileshimport sys import igraph as _ig from . import _c_leiden from ._c_leiden import ALL_COMMS from ._c_leiden import ALL_NEIGH_COMMS from ._c_leiden import RAND_COMM from ._c_leiden import RAND_NEIGH_COMM from ._c_leiden import MOVE_NODES from ._c_leiden import MERGE_NODES from collections import Counter def _get_py_capsule(graph): return graph.__graph_as_capsule() from .VertexPartition import * from .Optimiser import * def find_partition(graph, partition_type, initial_membership=None, weights=None, n_iterations=2, max_comm_size=0, seed=None, **kwargs): """ Detect communities using the default settings. This function detects communities given the specified method in the ``partition_type``. This should be type derived from :class:`VertexPartition.MutableVertexPartition`, e.g. :class:`ModularityVertexPartition` or :class:`CPMVertexPartition`. Optionally an initial membership and edge weights can be provided. Remaining ``**kwargs`` are passed on to the constructor of the ``partition_type``, including for example a ``resolution_parameter``. Parameters ---------- graph : :class:`ig.Graph` The graph for which to detect communities. partition_type : type of :class:` The type of partition to use for optimisation. initial_membership : list of int Initial membership for the partition. If :obj:`None` then defaults to a singleton partition. weights : list of double, or edge attribute Weights of edges. Can be either an iterable or an edge attribute. n_iterations : int Number of iterations to run the Leiden algorithm. By default, 2 iterations are run. If the number of iterations is negative, the Leiden algorithm is run until an iteration in which there was no improvement. max_comm_size : non-negative int Maximal total size of nodes in a community. If zero (the default), then communities can be of any size. seed : int Seed for the random number generator. By default uses a random seed if nothing is specified. **kwargs Remaining keyword arguments, passed on to constructor of ``partition_type``. Returns ------- partition The optimised partition. See Also -------- :func:`Optimiser.optimise_partition` Examples -------- >>> G = ig.Graph.Famous('Zachary') >>> partition = la.find_partition(G, la.ModularityVertexPartition) """ if not weights is None: kwargs['weights'] = weights partition = partition_type(graph, initial_membership=initial_membership, **kwargs) optimiser = Optimiser() optimiser.max_comm_size = max_comm_size if (not seed is None): optimiser.set_rng_seed(seed) optimiser.optimise_partition(partition, n_iterations) return partition def find_partition_multiplex(graphs, partition_type, layer_weights=None, n_iterations=2, max_comm_size=0, seed=None, **kwargs): """ Detect communities for multiplex graphs. Each graph should be defined on the same set of vertices, only the edges may differ for different graphs. See :func:`Optimiser.optimise_partition_multiplex` for a more detailed explanation. Parameters ---------- graphs : list of :class:`ig.Graph` List of :class:`ig.Graph` graphs to optimise. partition_type : type of :class:`MutableVertexPartition` The type of partition to use for optimisation (identical for all graphs). layer_weights : list of double List containing weights of each layer. If None (the default), then all layers are weighted with 1. n_iterations : int Number of iterations to run the Leiden algorithm. By default, 2 iterations are run. If the number of iterations is negative, the Leiden algorithm is run until an iteration in which there was no improvement. max_comm_size : non-negative int Maximal total size of nodes in a community. If zero (the default), then communities can be of any size. seed : int Seed for the random number generator. By default uses a random seed if nothing is specified. **kwargs Remaining keyword arguments, passed on to constructor of ``partition_type``. Returns ------- list of int membership of nodes. float Improvement in quality of combined partitions, see :func:`Optimiser.optimise_partition_multiplex`. Notes ----- We don't return a partition in this case because a partition is always defined on a single graph. We therefore simply return the membership (which is the same for all layers). See Also -------- :func:`Optimiser.optimise_partition_multiplex` :func:`slices_to_layers` Examples -------- >>> n = 100 >>> G_1 = ig.Graph.Lattice([n], 1) >>> G_2 = ig.Graph.Lattice([n], 1) >>> membership, improvement = la.find_partition_multiplex([G_1, G_2], ... la.ModularityVertexPartition) """ if layer_weights is None: n_layers = len(graphs) layer_weights = [1]*n_layers partitions = [] for graph in graphs: partitions.append(partition_type(graph, **kwargs)) optimiser = Optimiser() optimiser.max_comm_size = max_comm_size; if (not seed is None): optimiser.set_rng_seed(seed) improvement = optimiser.optimise_partition_multiplex(partitions, layer_weights, n_iterations) return partitions[0].membership, improvement def find_partition_temporal(graphs, partition_type, interslice_weight=1, slice_attr='slice', vertex_id_attr='id', edge_type_attr='type', weight_attr='weight', n_iterations=2, max_comm_size=0, seed=None, **kwargs): """ Detect communities for temporal graphs. Each graph is considered to represent a time slice and does not necessarily need to be defined on the same set of vertices. Nodes in two consecutive slices are identified on the basis of the ``vertex_id_attr``, i.e. if two nodes in two consecutive slices have an identical value of the ``vertex_id_attr`` they are coupled. The ``vertex_id_attr`` should hence be unique in each slice. The nodes are then coupled with a weight of ``interslice_weight`` which is set in the edge attribute ``weight_attr``. No weight is set if the ``interslice_weight`` is None (i.e. corresponding in practice with a weight of 1). See :func:`time_slices_to_layers` for a more detailed explanation. Parameters ---------- graphs : list of :class:`ig.Graph` List of :class:`leidenalg.VertexPartition` layers to optimise. partition_type : type of :class:`VertexPartition.MutableVertexPartition` The type of partition to use for optimisation (identical for all graphs). interslice_weight : float The weight of the coupling between two consecutive time slices. slice_attr : string The vertex attribute to use for indicating the slice of a node. vertex_id_attr : string The vertex to use to identify nodes. edge_type_attr : string The edge attribute to use for indicating the type of link (`interslice` or `intraslice`). weight_attr : string The edge attribute used to indicate the weight. n_iterations : int Number of iterations to run the Leiden algorithm. By default, 2 iterations are run. If the number of iterations is negative, the Leiden algorithm is run until an iteration in which there was no improvement. max_comm_size : non-negative int Maximal total size of nodes in a community. If zero (the default), then communities can be of any size. seed : int Seed for the random number generator. By default uses a random seed if nothing is specified. **kwargs Remaining keyword arguments, passed on to constructor of ``partition_type``. Returns ------- list of membership list containing for each slice the membership vector. float Improvement in quality of combined partitions, see :func:`Optimiser.optimise_partition_multiplex`. See Also -------- :func:`time_slices_to_layers` :func:`slices_to_layers` Examples -------- >>> n = 100 >>> G_1 = ig.Graph.Lattice([n], 1) >>> G_1.vs['id'] = range(n) >>> G_2 = ig.Graph.Lattice([n], 1) >>> G_2.vs['id'] = range(n) >>> membership, improvement = la.find_partition_temporal([G_1, G_2], ... la.ModularityVertexPartition, ... interslice_weight=1) """ # Create layers G_layers, G_interslice, G = time_slices_to_layers(graphs, interslice_weight, slice_attr=slice_attr, vertex_id_attr=vertex_id_attr, edge_type_attr=edge_type_attr, weight_attr=weight_attr) # Optimise partitions arg_dict = {} if 'node_sizes' in partition_type.__init__.__code__.co_varnames: arg_dict['node_sizes'] = 'node_size' if 'weights' in partition_type.__init__.__code__.co_varnames: arg_dict['weights'] = 'weight' arg_dict.update(kwargs) partitions = [] for H in G_layers: arg_dict['graph'] = H partitions.append(partition_type(**arg_dict)) # We can always take the same interslice partition, as this should have no # cost in the optimisation. partition_interslice = CPMVertexPartition(G_interslice, resolution_parameter=0, node_sizes='node_size', weights=weight_attr) optimiser = Optimiser() optimiser.max_comm_size = max_comm_size if (not seed is None): optimiser.set_rng_seed(seed) improvement = optimiser.optimise_partition_multiplex(partitions + [partition_interslice], n_iterations=n_iterations) # Transform results back into original form. membership = {(v[slice_attr], v[vertex_id_attr]): m for v, m in zip(G.vs, partitions[0].membership)} membership_time_slices = [] for slice_idx, H in enumerate(graphs): membership_slice = [membership[(slice_idx, v[vertex_id_attr])] for v in H.vs] membership_time_slices.append(list(membership_slice)) return membership_time_slices, improvement #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # These are helper functions to create a proper # disjoint union in python. The igraph implementation # currently does not keep the attributes when creating # a disjoint_union. def get_attrs_or_nones(seq, attr_name): try: return seq[attr_name] except KeyError: return [None] * len(seq) def disjoint_union_attrs(graphs): G = _ig.Graph.disjoint_union(graphs[0], graphs[1:]) vertex_attributes = set(sum([H.vertex_attributes() for H in graphs], [])) edge_attributes = set(sum([H.edge_attributes() for H in graphs], [])) for attr in vertex_attributes: attr_value = sum([get_attrs_or_nones(H.vs, attr) for H in graphs], []) G.vs[attr] = attr_value for attr in edge_attributes: attr_value = sum([get_attrs_or_nones(H.es, attr) for H in graphs], []) G.es[attr] = attr_value return G #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # Conversion to layer graphs def time_slices_to_layers(graphs, interslice_weight=1, slice_attr='slice', vertex_id_attr='id', edge_type_attr='type', weight_attr='weight'): """ Convert time slices to layer graphs. Each graph is considered to represent a time slice. This function simply connects all the consecutive slices (i.e. the slice graph) with an ``interslice_weight``. The further conversion is then delegated to :func:`slices_to_layers`, which also provides further details. See Also -------- :func:`find_partition_temporal` :func:`slices_to_layers` """ G_slices = _ig.Graph.Tree(len(graphs), 1, mode=_ig.TREE_UNDIRECTED) G_slices.es[weight_attr] = interslice_weight G_slices.vs[slice_attr] = graphs return slices_to_layers(G_slices, slice_attr, vertex_id_attr, edge_type_attr, weight_attr) def slices_to_layers(G_coupling, slice_attr='slice', vertex_id_attr='id', edge_type_attr='type', weight_attr='weight'): """ Convert a coupling graph of slices to layers of graphs. This function converts a graph of slices to layers so that they can be used with this package. This function assumes that the slices are represented by nodes in ``G_coupling``, and stored in the attribute ``slice_attr``. In other words, ``G_coupling.vs[slice_attr]`` should contain :class:`ig.Graph` s . The slices will be converted to layers, and nodes in different slices will be coupled if the two slices are connected in ``G_coupling``. Nodes in two connected slices are identified on the basis of the ``vertex_id_attr``, i.e. if two nodes in two connected slices have an identical value of the ``vertex_id_attr`` they will be coupled. The ``vertex_id_attr`` should hence be unique in each slice. Each node in the resulting layer graphs will contain two vertex attributes with the name of ``slice_attr`` and ``vertex_id_attr`` that refer respectively to the slice and id of the node. The weight of the coupling is determined by the weight of this link in ``G_coupling``, as determined by the ``weight_attr``. Parameters ---------- G_coupling : :class:`ig.Graph` The graph connecting the different slices. slice_attr : string The vertex attribute which contains the slices. vertex_id_attr : string The vertex attribute which is used to identify whether two nodes in two slices represent the same node, and hence, should be coupled. edge_type_attr : string The edge attribute to use for indicating the type of link (``interslice`` or ``intraslice``). weight_attr : string The edge attribute used to indicate the (coupling) weight. Returns ------- G_layers : list of :class:`ig.Graph` A list of slices converted to layers. G_interslice : :class:`ig.Graph` The interslice coupling layer. G : :class:`ig.Graph` The complete graph containing all layers and interslice couplings. Notes ----- The distinction between slices and layers is not easy to grasp. Slices in this context refer to graphs that somehow represents different aspects of a network. The simplest example is probably slices that represents time: there are different snapshots network across time, and each snapshot is considered a slice. Some nodes may drop out of the network over time, while others enter the network. Edges may change over time, or the weight of the links may change over time. This is just the simplest example of a slice, and there may be different, more complex possibilities. Below an example with three time slices: .. image:: figures/slices.png Now in order to optimise partitions across these different slices, we represent them slightly differently, namely as layers. The idea of layers is that all graphs always are defined on the same set of nodes, and that only the links differ for different layers. We thus create new nodes as combinations of original nodes and slices. For example, if node 1 existed in both slice 1 and in slice 2, we will thus create two nodes to build the layers: a node 1-1 and a node 1-2. Additionally, if the slices are connected in the slice graph, the two nodes would also be connected, so there would be a linke between node 1-1 and 1-2. Different slices will then correspond to different layers: each layer only contains the link for that particular slice. In addition, for methods such as :class:`CPMVertexPartition`, so-called ``node_sizes`` are required, and for them to properly function, they should be set to 1 only for nodes of a layer that represent nodes of the corresponding slice and 0 for the other nodes (which is handled appropriately in this function, and stored in the vertex attribute ``node_size``). Additionally, ``node_sizes`` should be set to 0 for the interslice coupling layer. We thus obtain equally many layers as we have slices, and we need one more layer for representing the interslice couplings. For the example provided above, we thus obtain the following: .. image:: figures/layers_separate.png The idea of doing community detection with slices is further detailed in [1]. References ---------- .. [1] Mucha, P. J., Richardson, T., Macon, K., Porter, M. A., & Onnela, J.-P. (2010). Community structure in time-dependent, multiscale, and multiplex networks. Science, 328(5980), 876-8. `10.1126/science.1184819 `_ See Also -------- :func:`find_partition_temporal` :func:`time_slices_to_layers` """ if not slice_attr in G_coupling.vertex_attributes(): raise ValueError("Could not find the vertex attribute {0} in the coupling graph.".format(slice_attr)) if not weight_attr in G_coupling.edge_attributes(): raise ValueError("Could not find the edge attribute {0} in the coupling graph.".format(weight_attr)) # Create disjoint union of the time graphs for v_slice in G_coupling.vs: H = v_slice[slice_attr] H.vs[slice_attr] = v_slice.index if not vertex_id_attr in H.vertex_attributes(): raise ValueError("Could not find the vertex attribute {0} to identify nodes in different slices.".format(vertex_id_attr )) if not weight_attr in H.edge_attributes(): H.es[weight_attr] = 1 G = disjoint_union_attrs(G_coupling.vs[slice_attr]) G.es[edge_type_attr] = 'intraslice' for v_slice in G_coupling.vs: for u_slice in v_slice.neighbors(mode=_ig.OUT): if v_slice.index < u_slice.index or G_coupling.is_directed(): nodes_v = G.vs.select(lambda v: v[slice_attr]==v_slice.index)[vertex_id_attr] if len(set(nodes_v)) != len(nodes_v): err = '\n'.join( ['\t{0} {1} times'.format(item, count) for item, count in Counter(nodes_v).items() if count > 1] ) raise ValueError('No unique IDs for slice {0}, require unique IDs:\n{1}'.format(v_slice.index, err)) nodes_u = G.vs.select(lambda v: v[slice_attr]==u_slice.index)[vertex_id_attr] if len(set(nodes_u)) != len(nodes_u): err = '\n'.join( ['\t{0} {1} times'.format(item, count) for item, count in Counter(nodes_u).items() if count > 1] ) raise ValueError('No unique IDs for slice {0}, require unique IDs:\n{1}'.format(u_slice.index, err)) common_nodes = set(nodes_v).intersection(set(nodes_u)) nodes_v = sorted([v for v in G.vs if v[slice_attr] == v_slice.index and v[vertex_id_attr] in common_nodes], key=lambda v: v[vertex_id_attr]) nodes_u = sorted([v for v in G.vs if v[slice_attr] == u_slice.index and v[vertex_id_attr] in common_nodes], key=lambda v: v[vertex_id_attr]) edges = zip(nodes_v, nodes_u) e_start = G.ecount() G.add_edges(edges) e_end = G.ecount() e_idx = range(e_start, e_end) interslice_weight = G_coupling.es[G_coupling.get_eid(v_slice, u_slice)][weight_attr] if not interslice_weight is None: G.es[e_idx][weight_attr] = interslice_weight G.es[e_idx][edge_type_attr] = 'interslice' # Convert aggregate graph to individual layers for each time slice. G_layers = [None]*G_coupling.vcount() for v_slice in G_coupling.vs: H = G.subgraph_edges(G.es.select(_within=[v.index for v in G.vs if v[slice_attr] == v_slice.index]), delete_vertices=False) H.vs['node_size'] = [1 if v[slice_attr] == v_slice.index else 0 for v in H.vs] G_layers[v_slice.index] = H # Create one graph for the interslice links. G_interslice = G.subgraph_edges(G.es.select(type_eq='interslice'), delete_vertices=False) G_interslice.vs['node_size'] = 0 return G_layers, G_interslice, G leidenalg-0.11.0/src/leidenalg/__init__.py0000664000175000017510000000451015101156755017700 0ustar nileshnilesh# -*- coding: utf-8 -*- r""" This package implements the Leiden algorithm in ``C++`` and exposes it to python. It relies on ``(python-)igraph`` for it to function. Besides the relative flexibility of the implementation, it also scales well, and can be run on graphs of millions of nodes (as long as they can fit in memory). Each method is represented by a different class, all of whom derive from :class:`~leidenalg.VertexPartition.MutableVertexPartition`. In addition, multiplex graphs are supported as layers, which also supports multislice representations. Examples -------- The simplest example just finds a partition using modularity >>> G = ig.Graph.Tree(100, 3) >>> partition = la.find_partition(G, la.ModularityVertexPartition) Alternatively, one can access the different optimisation routines individually and construct partitions oneself. These partitions can then be optimised by constructing an :class:`Optimiser` object and running :func:`~Optimiser.optimise_partition`. >>> G = ig.Graph.Tree(100, 3) >>> partition = la.CPMVertexPartition(G, resolution_parameter = 0.1) >>> optimiser = la.Optimiser() >>> diff = optimiser.optimise_partition(partition) The :class:`Optimiser` class contains also the different subroutines that are used internally by :func:`~Optimiser.optimise_partition`. In addition, through the Optimiser class there are various options available for changing some of the optimisation procedure which can affect both speed and quality, which are not immediately available in :func:`leidenalg.find_partition`. """ from .functions import ALL_COMMS from .functions import ALL_NEIGH_COMMS from .functions import RAND_COMM from .functions import RAND_NEIGH_COMM from .functions import MOVE_NODES from .functions import MERGE_NODES from .functions import find_partition from .functions import find_partition_multiplex from .functions import find_partition_temporal from .functions import slices_to_layers from .functions import time_slices_to_layers from .Optimiser import Optimiser from .VertexPartition import ModularityVertexPartition from .VertexPartition import SurpriseVertexPartition from .VertexPartition import SignificanceVertexPartition from .VertexPartition import RBERVertexPartition from .VertexPartition import RBConfigurationVertexPartition from .VertexPartition import CPMVertexPartition from .version import * leidenalg-0.11.0/src/leidenalg/VertexPartition.py0000664000175000017510000012202515101156755021312 0ustar nileshnileshimport igraph as _ig from . import _c_leiden from .functions import _get_py_capsule class MutableVertexPartition(_ig.VertexClustering): """ Contains a partition of a graph, derives from :class:`ig.VertexClustering`. Please see the `documentation `_ of :class:`ig.VertexClustering` for more details about its functionality. This class contains the basic implementation for optimising a partition. Specifically, it implements all the administration necessary to keep track of the partition from various points of view. Internally, it keeps track of the number of internal edges (or total weight), the size of the communities, the total incoming degree (or weight) for a community, et cetera. In order to keep the administration up-to-date, all changes in a partition should be done through :func:`~VertexPartition.MutableVertexPartition.move_node` or :func:`~VertexPartition.MutableVertexPartition.set_membership`. The first moves a node from one community to another, and updates the administration. The latter simply updates the membership vector and updates the administration. The basic idea is that :func:`~VertexPartition.MutableVertexPartition.diff_move` computes the difference in the quality function if we would call :func:`~VertexPartition.MutableVertexPartition.move_node` for the same move. These functions are overridden in any derived classes to provide an actual implementation. These functions are used by :class:`Optimiser` to optimise the partition. .. warning:: This base class should never be used in practice, since only derived classes provide an actual implementation. """ # Init def __init__(self, graph, initial_membership=None): """ Parameters ---------- graph The :class:`ig.Graph` on which this partition is defined. membership The membership vector of this partition. ``Membership[i] = c`` implies that node ``i`` is in community ``c``. If ``None``, it is initialised with a singleton partition community, i.e. ``membership[i] = i``. """ if initial_membership is not None: initial_membership = list(initial_membership) super(MutableVertexPartition, self).__init__(graph, initial_membership) @classmethod def _FromCPartition(cls, partition): n, directed, edges, weights, node_sizes = _c_leiden._MutableVertexPartition_get_py_igraph(partition) graph = _ig.Graph(n=n, directed=directed, edges=edges, edge_attrs={'weight': weights}, vertex_attrs={'node_size': node_sizes}) new_partition = cls(graph) new_partition._partition = partition new_partition._update_internal_membership() return new_partition @classmethod def FromPartition(cls, partition, **kwargs): """ Create a new partition from an existing partition. Parameters ---------- partition The :class:`~VertexPartition.MutableVertexPartition` to replicate. **kwargs Any remaining keyword arguments will be passed on to the constructor of the new partition. Notes ----- This may for example come in handy when determining the quality of a partition using a different method. Suppose that we already have a partition ``p`` and that we want to determine the Significance of that partition. We can then simply use >>> p = la.find_partition(ig.Graph.Famous('Zachary'), ... la.ModularityVertexPartition) >>> sig = la.SignificanceVertexPartition.FromPartition(p).quality() """ new_partition = cls(partition.graph, partition.membership, **kwargs) return new_partition def _update_internal_membership(self): self._membership = _c_leiden._MutableVertexPartition_get_membership(self._partition) # Reset the length of the object, i.e. the number of communities if len(self._membership)>0: self._len = max(m for m in self._membership if m is not None)+1 else: self._len = 0 def set_membership(self, membership): """ Set membership. """ _c_leiden._MutableVertexPartition_set_membership(self._partition, list(membership)) self._update_internal_membership() # Calculate improvement *if* we move this node def diff_move(self,v,new_comm): """ Calculate the difference in the quality function if node ``v`` is moved to community ``new_comm``. Parameters ---------- v The node to move. new_comm The community to move to. Returns ------- float Difference in quality function. Notes ----- The difference returned by diff_move should be equivalent to first determining the quality of the partition, then calling move_node, and then determining again the quality of the partition and looking at the difference. In other words >>> partition = la.find_partition(ig.Graph.Famous('Zachary'), ... la.ModularityVertexPartition) >>> diff = partition.diff_move(v=0, new_comm=0) >>> q1 = partition.quality() >>> partition.move_node(v=0, new_comm=0) >>> q2 = partition.quality() >>> round(diff, 10) == round(q2 - q1, 10) True .. warning:: Only derived classes provide actual implementations, the base class provides no implementation for this function. """ return _c_leiden._MutableVertexPartition_diff_move(self._partition, v, new_comm) def aggregate_partition(self, membership_partition=None): """ Aggregate the graph according to the current partition and provide a default partition for it. The aggregated graph can then be found as a parameter of the partition ``partition.graph``. Notes ----- This function contrasts to the function ``cluster_graph`` in igraph itself, which also provides the aggregate graph, but we may require setting the appropriate ``resolution_parameter``, ``weights`` and ``node_sizes``. In particular, this function also ensures that the quality defined on the aggregate partition is identical to the quality defined on the original partition. Examples -------- >>> G = ig.Graph.Famous('Zachary') >>> partition = la.find_partition(G, la.ModularityVertexPartition) >>> aggregate_partition = partition.aggregate_partition(partition) >>> aggregate_graph = aggregate_partition.graph >>> aggregate_partition.quality() == partition.quality() True """ partition_agg = self._FromCPartition(_c_leiden._MutableVertexPartition_aggregate_partition(self._partition)) if (not membership_partition is None): membership = partition_agg.membership for v in self.graph.vs: membership[self.membership[v.index]] = membership_partition.membership[v.index] partition_agg.set_membership(membership) return partition_agg def move_node(self,v,new_comm): """ Move node ``v`` to community ``new_comm``. Parameters ---------- v Node to move. new_comm Community to move to. Examples -------- >>> G = ig.Graph.Famous('Zachary') >>> partition = la.ModularityVertexPartition(G) >>> partition.move_node(0, 1) """ _c_leiden._MutableVertexPartition_move_node(self._partition, v, new_comm) # Make sure this move is also reflected in the membership vector of the python object self._membership[v] = new_comm self._modularity_dirty = True def from_coarse_partition(self, partition, coarse_node=None): """ Update current partition according to coarser partition. Parameters ---------- partition : :class:`~VertexPartition.MutableVertexPartition` The coarser partition used to update the current partition. coarse_node : list of int The coarser node which represent the current node in the partition. Notes ----- This function is to be used to determine the correct partition for an aggregated graph. In particular, suppose we move nodes and then get an aggregate graph. >>> diff = optimiser.move_nodes(partition) >>> aggregate_partition = partition.aggregate_partition() Now we also move nodes in the aggregate partition >>> diff = optimiser.move_nodes(aggregate_partition) Now we improved the quality function of ``aggregate_partition``, but this is not yet reflected in the original ``partition``. We can thus call >>> partition.from_coarse_partition(aggregate_partition) so that ``partition`` now reflects the changes made to ``aggregate_partition``. The ``coarse_node`` can be used it the ``aggregate_partition`` is not defined based on the membership of this partition. In particular the membership of this partition is defined as follows: >>> for v in G.vs: ... partition.membership[v] = aggregate_partition.membership[coarse_node[v]] # doctest: +SKIP If ``coarse_node`` is :obj:`None` it is assumed the coarse node was defined based on the membership of the current partition, so that >>> for v in G.vs: ... partition.membership[v] = aggregate_partition.membership[partition.membership[v]] # doctest: +SKIP This can be useful when the aggregate partition is defined on a more refined partition. """ # Read the coarser partition _c_leiden._MutableVertexPartition_from_coarse_partition(self._partition, partition.membership, coarse_node) self._update_internal_membership() def renumber_communities(self): """ Renumber the communities so that they are numbered in decreasing size. Notes ----- The sort is not necessarily stable. """ _c_leiden._MutableVertexPartition_renumber_communities(self._partition) self._update_internal_membership() def quality(self): """ The current quality of the partition. """ return _c_leiden._MutableVertexPartition_quality(self._partition) def total_weight_in_comm(self, comm): """ The total weight (i.e. number of edges) within a community. Parameters ---------- comm Community See Also -------- :func:`~VertexPartition.MutableVertexPartition.total_weight_to_comm` :func:`~VertexPartition.MutableVertexPartition.total_weight_from_comm` :func:`~VertexPartition.MutableVertexPartition.total_weight_in_all_comms` """ return _c_leiden._MutableVertexPartition_total_weight_in_comm(self._partition, comm) def total_weight_from_comm(self, comm): """ The total weight (i.e. number of edges) from a community. Parameters ---------- comm Community Notes ----- This includes all edges, also the ones that are internal to a community. Sometimes this is also referred to as the community (out)degree. See Also -------- :func:`~VertexPartition.MutableVertexPartition.total_weight_to_comm` :func:`~VertexPartition.MutableVertexPartition.total_weight_in_comm` :func:`~VertexPartition.MutableVertexPartition.total_weight_in_all_comms` """ return _c_leiden._MutableVertexPartition_total_weight_from_comm(self._partition, comm) def total_weight_to_comm(self, comm): """ The total weight (i.e. number of edges) to a community. Parameters ---------- comm Community Notes ----- This includes all edges, also the ones that are internal to a community. Sometimes this is also referred to as the community (in)degree. See Also -------- :func:`~VertexPartition.MutableVertexPartition.total_weight_from_comm` :func:`~VertexPartition.MutableVertexPartition.total_weight_in_comm` :func:`~VertexPartition.MutableVertexPartition.total_weight_in_all_comms` """ return _c_leiden._MutableVertexPartition_total_weight_to_comm(self._partition, comm) def total_weight_in_all_comms(self): """ The total weight (i.e. number of edges) within all communities. Notes ----- This should be equal to simply the sum of ``total_weight_in_comm`` for all communities. See Also -------- :func:`~VertexPartition.MutableVertexPartition.total_weight_to_comm` :func:`~VertexPartition.MutableVertexPartition.total_weight_from_comm` :func:`~VertexPartition.MutableVertexPartition.total_weight_in_comm` """ return _c_leiden._MutableVertexPartition_total_weight_in_all_comms(self._partition) def total_possible_edges_in_all_comms(self): """ The total possible number of edges in all communities. Notes ----- If we denote by :math:`n_c` the number of nodes in community :math:`c`, this is simply .. math :: \\sum_c \\binom{n_c}{2} """ return _c_leiden._MutableVertexPartition_total_possible_edges_in_all_comms(self._partition) def weight_to_comm(self, v, comm): """ The total number of edges (or sum of weights) from node ``v`` to community ``comm``. See Also -------- :func:`~VertexPartition.MutableVertexPartition.weight_from_comm` """ return _c_leiden._MutableVertexPartition_weight_to_comm(self._partition, v, comm) def weight_from_comm(self, v, comm): """ The total number of edges (or sum of weights) to node ``v`` from community ``comm``. See Also -------- :func:`~VertexPartition.MutableVertexPartition.weight_to_comm` """ return _c_leiden._MutableVertexPartition_weight_from_comm(self._partition, v, comm) class ModularityVertexPartition(MutableVertexPartition): r""" Implements modularity. This quality function is well-defined only for positive edge weights. Notes ----- The quality function is .. math:: Q = \\frac{1}{2m} \\sum_{ij} \\left(A_{ij} - \\frac{k_i k_j}{2m} \\right)\\delta(\\sigma_i, \\sigma_j) where :math:`A` is the adjacency matrix, :math:`k_i` is the (weighted) degree of node :math:`i`, :math:`m` is the total number of edges (or total edge weight), :math:`\\sigma_i` denotes the community of node :math:`i` and :math:`\\delta(\\sigma_i, \\sigma_j) = 1` if :math:`\\sigma_i = \\sigma_j` and `0` otherwise. This can alternatively be formulated as a sum over communities: .. math:: Q = \\frac{1}{2m} \\sum_{c} \\left(m_c - \\frac{K_c^2}{4m} \\right) where :math:`m_c` is the number of internal edges (or total internal edge weight) of community :math:`c` and :math:`K_c = \\sum_{i \\mid \\sigma_i = c} k_i` is the total (weighted) degree of nodes in community :math:`c`. Note that for directed graphs a slightly different formulation is used, as proposed by Leicht and Newman [2]: .. math:: Q = \\frac{1}{m} \\sum_{ij} \\left(A_{ij} - \\frac{k_i^\\mathrm{out} k_j^\\mathrm{in}}{m} \\right)\\delta(\\sigma_i, \\sigma_j), where :math:`k_i^\\mathrm{out}` and :math:`k_i^\\mathrm{in}` refers to respectively the outdegree and indegree of node :math:`i`, and :math:`A_{ij}` refers to an edge from :math:`i` to :math:`j`. References ---------- .. [1] Newman, M. E. J., & Girvan, M. (2004). Finding and evaluating community structure in networks. Physical Review E, 69(2), 026113. `10.1103/PhysRevE.69.026113 `_ .. [2] Leicht, E. A., & Newman, M. E. J. (2008). Community Structure in Directed Networks. Physical Review Letters, 100(11), 118703. `10.1103/PhysRevLett.100.118703 `_ """ def __init__(self, graph, initial_membership=None, weights=None): """ Parameters ---------- graph : :class:`ig.Graph` Graph to define the partition on. initial_membership : list of int Initial membership for the partition. If :obj:`None` then defaults to a singleton partition. weights : list of double, or edge attribute Weights of edges. Can be either an iterable or an edge attribute. """ if initial_membership is not None: initial_membership = list(initial_membership) super(ModularityVertexPartition, self).__init__(graph, initial_membership) pygraph_t = _get_py_capsule(graph) if weights is not None: if isinstance(weights, str): weights = graph.es[weights] else: # Make sure it is a list weights = list(weights) self._partition = _c_leiden._new_ModularityVertexPartition(pygraph_t, initial_membership, weights) self._update_internal_membership() def __deepcopy__(self, memo): n, directed, edges, weights, node_sizes = _c_leiden._MutableVertexPartition_get_py_igraph(self._partition) new_partition = ModularityVertexPartition(self.graph, self.membership, weights) return new_partition class SurpriseVertexPartition(MutableVertexPartition): """ Implements (asymptotic) Surprise. This quality function is well-defined only for positive edge weights. Notes ----- The quality function is .. math:: Q = m D(q \\parallel \\langle q \\rangle) where :math:`m` is the number of edges, .. math:: q = \\frac{\\sum_c m_c}{m}, is the fraction of internal edges, .. math:: \\langle q \\rangle = \\frac{\\sum_c \\binom{n_c}{2}}{\\binom{n}{2}} is the expected fraction of internal edges, and finally .. math:: D(x \\parallel y) = x \\ln \\frac{x}{y} + (1 - x) \\ln \\frac{1 - x}{1 - y} is the binary Kullback-Leibler divergence. For directed graphs we can multiplying the binomials by 2, and this leaves :math:`\\langle q \\rangle` unchanged, so that we can simply use the same formulation. For weighted graphs we can simply count the total internal weight instead of the total number of edges for :math:`q`, while :math:`\\langle q \\rangle` remains unchanged. References ---------- .. [1] Traag, V. A., Aldecoa, R., & Delvenne, J.-C. (2015). Detecting communities using asymptotical surprise. Physical Review E, 92(2), 022816. `10.1103/PhysRevE.92.022816 `_ """ def __init__(self, graph, initial_membership=None, weights=None, node_sizes=None): """ Parameters ---------- graph : :class:`ig.Graph` Graph to define the partition on. initial_membership : list of int Initial membership for the partition. If :obj:`None` then defaults to a singleton partition. weights : list of double, or edge attribute Weights of edges. Can be either an iterable or an edge attribute. node_sizes : list of int, or vertex attribute The quality function takes into account the size of a community, which is defined as the sum over the sizes of each individual node. By default, the node sizes are set to 1, meaning that the size of a community equals the number of nodes of a community. If a node already represents an aggregation, this could be reflect in its node size. """ if initial_membership is not None: initial_membership = list(initial_membership) super(SurpriseVertexPartition, self).__init__(graph, initial_membership) pygraph_t = _get_py_capsule(graph) if weights is not None: if isinstance(weights, str): weights = graph.es[weights] else: # Make sure it is a list weights = list(weights) if node_sizes is not None: if isinstance(node_sizes, str): node_sizes = graph.vs[node_sizes] else: # Make sure it is a list node_sizes = list(node_sizes) self._partition = _c_leiden._new_SurpriseVertexPartition(pygraph_t, initial_membership, weights, node_sizes) self._update_internal_membership() def __deepcopy__(self, memo): n, directed, edges, weights, node_sizes = _c_leiden._MutableVertexPartition_get_py_igraph(self._partition) new_partition = SurpriseVertexPartition(self.graph, self.membership, weights, node_sizes) return new_partition class SignificanceVertexPartition(MutableVertexPartition): """ Implements Significance. This quality function is well-defined only for unweighted graphs. Notes ----- The quality function is .. math:: Q = \\sum_c \\binom{n_c}{2} D(p_c \\parallel p) where :math:`n_c` is the number of nodes in community :math:`c`, .. math:: p_c = \\frac{m_c}{\\binom{n_c}{2}}, is the density of community :math:`c`, .. math:: p = \\frac{m}{\\binom{n}{2}} is the overall density of the graph, and finally .. math:: D(x \\parallel y) = x \\ln \\frac{x}{y} + (1 - x) \\ln \\frac{1 - x}{1 - y} is the binary Kullback-Leibler divergence. For directed graphs simply multiply the binomials by 2. The expected Significance in Erdos-Renyi graphs behaves roughly as :math:`\\frac{1}{2} n \\ln n` for both directed and undirected graphs in this formulation. .. warning:: This method is not suitable for weighted graphs. References ---------- .. [1] Traag, V. A., Krings, G., & Van Dooren, P. (2013). Significant scales in community structure. Scientific Reports, 3, 2930. `10.1038/srep02930 `_ """ def __init__(self, graph, initial_membership=None, node_sizes=None): """ Parameters ---------- graph : :class:`ig.Graph` Graph to define the partition on. initial_membership : list of int Initial membership for the partition. If :obj:`None` then defaults to a singleton partition. node_sizes : list of int, or vertex attribute The quality function takes into account the size of a community, which is defined as the sum over the sizes of each individual node. By default, the node sizes are set to 1, meaning that the size of a community equals the number of nodes of a community. If a node already represents an aggregation, this could be reflect in its node size. """ if initial_membership is not None: initial_membership = list(initial_membership) super(SignificanceVertexPartition, self).__init__(graph, initial_membership) pygraph_t = _get_py_capsule(graph) if node_sizes is not None: if isinstance(node_sizes, str): node_sizes = graph.vs[node_sizes] else: # Make sure it is a list node_sizes = list(node_sizes) self._partition = _c_leiden._new_SignificanceVertexPartition(pygraph_t, initial_membership, node_sizes) self._update_internal_membership() def __deepcopy__(self, memo): n, directed, edges, weights, node_sizes = _c_leiden._MutableVertexPartition_get_py_igraph(self._partition) new_partition = SignificanceVertexPartition(self.graph, self.membership, node_sizes) return new_partition class LinearResolutionParameterVertexPartition(MutableVertexPartition): """ Some quality functions have a linear resolution parameter, for which the basis is implemented here. With a linear resolution parameter, we mean that the objective function has the form: .. math:: Q = E - \\gamma F where :math:`\\gamma` is some resolution parameter and :math:`E` and :math:`F` arbitrary other functions of the partition. One thing that can be easily done on these type of quality functions, is bisectioning on the gamma function (also assuming that :math:`E` is a stepwise decreasing monotonic function, e.g. as for :class:`CPMVertexPartition`). """ def __init__(self, graph, initial_membership=None): if initial_membership is not None: initial_membership = list(initial_membership) super(LinearResolutionParameterVertexPartition, self).__init__(graph, initial_membership) #########################################################3 # resolution parameter @property def resolution_parameter(self): """ Resolution parameter. """ return _c_leiden._ResolutionParameterVertexPartition_get_resolution(self._partition) @resolution_parameter.setter def resolution_parameter(self, value): return _c_leiden._ResolutionParameterVertexPartition_set_resolution(self._partition, value) def bisect_value(self): """ Give the value on which we can perform bisectioning. If p1 and p2 are two different optimal partitions for two different resolution parameters g1 and g2, then if p1.bisect_value() == p2.bisect_value() the two partitions should be optimal for both g1 and g2. """ return self.total_weight_in_all_comms() def quality(self, resolution_parameter=None): return _c_leiden._ResolutionParameterVertexPartition_quality(self._partition, resolution_parameter) class RBERVertexPartition(LinearResolutionParameterVertexPartition): """ Implements Reichardt and Bornholdt’s Potts model with an Erdős-Rényi null model. This quality function is well-defined only for positive edge weights. This quality function uses a linear resolution parameter. Notes ----- The quality function is .. math:: Q = \\sum_{ij} \\left(A_{ij} - \\gamma p \\right)\\delta(\\sigma_i, \\sigma_j) where :math:`A` is the adjacency matrix, .. math:: p = \\frac{m}{\\binom{n}{2}} is the overall density of the graph, :math:`\\sigma_i` denotes the community of node :math:`i`, :math:`\\delta(\\sigma_i, \\sigma_j) = 1` if :math:`\\sigma_i = \\sigma_j` and `0` otherwise, and, finally :math:`\\gamma` is a resolution parameter. This can alternatively be formulated as a sum over communities: .. math:: Q = \\sum_{c} \\left[m_c - \\gamma p \\binom{n_c}{2} \\right] where :math:`m_c` is the number of internal edges of community :math:`c` and :math:`n_c` the number of nodes in community :math:`c`. References ---------- .. [1] Reichardt, J., & Bornholdt, S. (2006). Statistical mechanics of community detection. Physical Review E, 74(1), 016110. `10.1103/PhysRevE.74.016110 `_ """ def __init__(self, graph, initial_membership=None, weights=None, node_sizes=None, resolution_parameter=1.0): """ Parameters ---------- graph : :class:`ig.Graph` Graph to define the partition on. initial_membership : list of int Initial membership for the partition. If :obj:`None` then defaults to a singleton partition. weights : list of double, or edge attribute Weights of edges. Can be either an iterable or an edge attribute. node_sizes : list of int, or vertex attribute The quality function takes into account the size of a community, which is defined as the sum over the sizes of each individual node. By default, the node sizes are set to 1, meaning that the size of a community equals the number of nodes of a community. If a node already represents an aggregation, this could be reflect in its node size. resolution_parameter : double Resolution parameter. """ if initial_membership is not None: initial_membership = list(initial_membership) super(RBERVertexPartition, self).__init__(graph, initial_membership) pygraph_t = _get_py_capsule(graph) if weights is not None: if isinstance(weights, str): weights = graph.es[weights] else: # Make sure it is a list weights = list(weights) if node_sizes is not None: if isinstance(node_sizes, str): node_sizes = graph.vs[node_sizes] else: # Make sure it is a list node_sizes = list(node_sizes) self._partition = _c_leiden._new_RBERVertexPartition(pygraph_t, initial_membership, weights, node_sizes, resolution_parameter) self._update_internal_membership() def __deepcopy__(self, memo): n, directed, edges, weights, node_sizes = _c_leiden._MutableVertexPartition_get_py_igraph(self._partition) new_partition = RBERVertexPartition(self.graph, self.membership, weights, node_sizes, self.resolution_parameter) return new_partition class RBConfigurationVertexPartition(LinearResolutionParameterVertexPartition): r""" Implements Reichardt and Bornholdt's Potts model with a configuration null model. This quality function is well-defined only for positive edge weights. This quality function uses a linear resolution parameter. Notes ----- The quality function is .. math:: Q = \\sum_{ij} \\left(A_{ij} - \\gamma \\frac{k_i k_j}{2m} \\right)\\delta(\\sigma_i, \\sigma_j) where :math:`A` is the adjacency matrix, :math:`k_i` is the (weighted) degree of node :math:`i`, :math:`m` is the total number of edges (or total edge weight), :math:`\\sigma_i` denotes the community of node :math:`i` and :math:`\\delta(\\sigma_i, \\sigma_j) = 1` if :math:`\\sigma_i = \\sigma_j` and `0` otherwise. This can alternatively be formulated as a sum over communities: .. math:: Q = \\sum_{c} \\left(m_c - \\gamma \\frac{K_c^2}{4m} \\right) where :math:`m_c` is the number of internal edges (or total internal edge weight) of community :math:`c` and :math:`K_c = \\sum_{i \\mid \\sigma_i = c} k_i` is the total (weighted) degree of nodes in community :math:`c`. Note that for directed graphs a slightly different formulation is used, as proposed by Leicht and Newman [2]: .. math:: Q = \\sum_{ij} \\left(A_{ij} - \\gamma \\frac{k_i^\\mathrm{out} k_j^\\mathrm{in}}{m} \\right)\\delta(\\sigma_i, \\sigma_j), where :math:`k_i^\\mathrm{out}` and :math:`k_i^\\mathrm{in}` refers to respectively the outdegree and indegree of node :math:`i`, and :math:`A_{ij}` refers to an edge from :math:`i` to :math:`j`. Note that this is the same as :class:`ModularityVertexPartition` when setting :math:`\\gamma=1` and normalising by :math:`2m`, or :math:`m` for directed graphs. References ---------- .. [1] Reichardt, J., & Bornholdt, S. (2006). Statistical mechanics of community detection. Physical Review E, 74(1), 016110. `10.1103/PhysRevE.74.016110 `_ .. [2] Leicht, E. A., & Newman, M. E. J. (2008). Community Structure in Directed Networks. Physical Review Letters, 100(11), 118703. `10.1103/PhysRevLett.100.118703 `_ """ def __init__(self, graph, initial_membership=None, weights=None, resolution_parameter=1.0): """ Parameters ---------- graph : :class:`ig.Graph` Graph to define the partition on. initial_membership : list of int Initial membership for the partition. If :obj:`None` then defaults to a singleton partition. weights : list of double, or edge attribute Weights of edges. Can be either an iterable or an edge attribute. resolution_parameter : double Resolution parameter. """ if initial_membership is not None: initial_membership = list(initial_membership) super(RBConfigurationVertexPartition, self).__init__(graph, initial_membership) pygraph_t = _get_py_capsule(graph) if weights is not None: if isinstance(weights, str): weights = graph.es[weights] else: # Make sure it is a list weights = list(weights) self._partition = _c_leiden._new_RBConfigurationVertexPartition(pygraph_t, initial_membership, weights, resolution_parameter) self._update_internal_membership() def __deepcopy__(self, memo): n, directed, edges, weights, node_sizes = _c_leiden._MutableVertexPartition_get_py_igraph(self._partition) new_partition = RBConfigurationVertexPartition(self.graph, self.membership, weights, self.resolution_parameter) return new_partition class CPMVertexPartition(LinearResolutionParameterVertexPartition): """ Implements the Constant Potts Model (CPM). This quality function is well-defined for both positive and negative edge weights. This quality function uses a linear resolution parameter. Notes ----- The Constant Potts Model (CPM) quality function is .. math:: Q = \\sum_{ij} \\left(A_{ij} - \\gamma \\right)\\delta(\\sigma_i, \\sigma_j) where :math:`A` is the adjacency matrix, :math:`\\sigma_i` denotes the community of node :math:`i`, :math:`\\delta(\\sigma_i, \\sigma_j) = 1` if :math:`\\sigma_i = \\sigma_j` and `0` otherwise, and, finally :math:`\\gamma` is a resolution parameter. This can alternatively be formulated as a sum over communities: .. math:: Q = \\sum_{c} \\left[m_c - \\gamma \\binom{n_c}{2} \\right] where :math:`m_c` is the number of internal edges of community :math:`c` and :math:`n_c` the number of nodes in community :math:`c`. The resolution parameter :math:`\\gamma` for this functions has a particularly simple interpretation. The internal density of communities .. math:: p_c = \\frac{m_c}{\\binom{n_c}{2}} \\geq \\gamma is higher than :math:`\\gamma`, while the external density .. math:: p_{cd} = \\frac{m_{cd}}{n_c n_d} \\leq \\gamma is lower than :math:`\\gamma`. In other words, choosing a particular :math:`\\gamma` corresponds to choosing to find communities of a particular density, and as such defines communities. Finally, the definition of a community is in a sense independent of the actual graph, which is not the case for any of the other methods (see the reference for more detail). References ---------- .. [1] Traag, V. A., Van Dooren, P., & Nesterov, Y. (2011). Narrow scope for resolution-limit-free community detection. Physical Review E, 84(1), 016114. `10.1103/PhysRevE.84.016114 `_ """ def __init__(self, graph, initial_membership=None, weights=None, node_sizes=None, resolution_parameter=1.0, correct_self_loops=None): """ Parameters ---------- graph : :class:`ig.Graph` Graph to define the partition on. initial_membership : list of int Initial membership for the partition. If :obj:`None` then defaults to a singleton partition. weights : list of double, or edge attribute Weights of edges. Can be either an iterable or an edge attribute. node_sizes : list of int, or vertex attribute The quality function takes into account the size of a community, which is defined as the sum over the sizes of each individual node. By default, the node sizes are set to 1, meaning that the size of a community equals the number of nodes of a community. If a node already represents an aggregation, this could be reflect in its node size. resolution_parameter : double Resolution parameter. """ if initial_membership is not None: initial_membership = list(initial_membership) super(CPMVertexPartition, self).__init__(graph, initial_membership) pygraph_t = _get_py_capsule(graph) if weights is not None: if isinstance(weights, str): weights = graph.es[weights] else: # Make sure it is a list weights = list(weights) if node_sizes is not None: if isinstance(node_sizes, str): node_sizes = graph.vs[node_sizes] else: # Make sure it is a list node_sizes = list(node_sizes) if correct_self_loops is None: correct_self_loops = any(graph.is_loop()) self._partition = _c_leiden._new_CPMVertexPartition(pygraph_t, initial_membership, weights, node_sizes, resolution_parameter, correct_self_loops) self._update_internal_membership() def __deepcopy__(self, memo): n, directed, edges, weights, node_sizes = _c_leiden._MutableVertexPartition_get_py_igraph(self._partition) new_partition = CPMVertexPartition(self.graph, self.membership, weights, node_sizes, self.resolution_parameter) return new_partition @classmethod def Bipartite(cls, graph, resolution_parameter_01, resolution_parameter_0 = 0, resolution_parameter_1 = 0, degree_as_node_size=False, types='type', **kwargs): """ Create three layers for bipartite partitions. This creates three layers for bipartite partition necessary for detecting communities in bipartite networks. These three layers should be passed to :func:`Optimiser.optimise_partition_multiplex` with ``layer_weights=[1,-1,-1]``. See `Notes <#notes-bipartite>`_ for more details. Parameters ---------- graph : :class:`ig.Graph` Graph to define the bipartite partitions on. resolution_parameter_01 : double Resolution parameter for in between two classes. resolution_parameter_0 : double Resolution parameter for class 0. resolution_parameter_1 : double Resolution parameter for class 1. degree_as_node_size : boolean If ``True`` use degree as node size instead of 1, to mimic modularity, see `Notes <#notes-bipartite>`_. types : vertex attribute or list Indicator of the class for each vertex. If not 0, 1, it is automatically converted. **kwargs Additional arguments passed on to default constructor of :class:`CPMVertexPartition`. Returns ------- :class:`ig.CPMVertexPartition` partition containing the bipartite graph and correct node sizes. :class:`ig.CPMVertexPartition` partition for type 0, containing the correct node sizes for type 0. :class:`ig.CPMVertexPartition` partition for type 1, containing the correct node sizes for type 1. .. _notes-bipartite: Notes ----- For bipartite networks, we would like to be able to set three different resolution parameters: one for within each class :math:`\\gamma_0, \\gamma_1`, and one for the links between classes, :math:`\\gamma_{01}`. Then the formulation would be .. math:: Q = \\sum_{ij} [A_{ij} - (\\gamma_0\\delta(s_i,0) + \\gamma_1\\delta(s_i,1)) \\delta(s_i,s_j) - \\gamma_{01}(1 - \\delta(s_i, s_j)) ]\\delta(\\sigma_i, \\sigma_j) In terms of communities this is .. math:: Q = \\sum_c (e_c - \\gamma_{01} 2 n_c(0) n_c(1) - \\gamma_0 n^2_c(0) - \\gamma_1 n^2_c(1)) where :math:`n_c(0)` is the number of nodes in community :math:`c` of class 0 (and similarly for 1) and :math:`e_c` is the number of edges within community :math:`c`. We denote by :math:`n_c = n_c(0) + n_c(1)` the total number of nodes in community :math:`c`. We achieve this by creating three layers : (1) all nodes have ``node_size = 1`` and all relevant links; (2) only nodes of class 0 have ``node_size = 1`` and no links; (3) only nodes of class 1 have ``node_size = 1`` and no links. If we add the first with resolution parameter :math:`\\gamma_{01}`, and the others with resolution parameters :math:`\\gamma_{01} - \\gamma_0` and :math:`\\gamma_{01} - \\gamma_1`, but the latter two with a layer weight of -1 while the first layer has layer weight 1, we obtain the following: .. math:: Q &= \\sum_c (e_c - \\gamma_{01} n_c^2) -\\sum_c (- (\\gamma_{01} - \\gamma_0) n_c(0)^2) -\\sum_c (- (\\gamma_{01} - \\gamma_1) n_c(1)^2) \\\\ &= \\sum_c [e_c - \\gamma_{01} 2 n_c(0) n_c(1) - \\gamma_{01} n_c(0)^2 - \\gamma_{01} n_c(1)^2) + ( \\gamma_{01} - \\gamma_0) n_c(0)^2 + ( \\gamma_{01} - \\gamma_1) n_c(1)^2 ] \\\\ &= \\sum_c [e_c - \\gamma_{01} 2 n_c(0) n_c(1) - \\gamma_{0} n_c(0)^2 - \\gamma_{1} n_c(1)^2] Although the derivation above is using :math:`n_c^2`, implicitly assuming a direct graph with self-loops, similar derivations can be made for undirected graphs using :math:`\\binom{n_c}{2}`, but the notation is then somewhat more convoluted. If we set node sizes equal to the degree, we get something similar to modularity, except that the resolution parameter should still be divided by :math:`2m`. In particular, in general (i.e. not specifically for bipartite graph) if ``node_sizes=G.degree()`` we then obtain .. math:: Q = \\sum_{ij} A_{ij} - \\gamma k_i k_j In the case of bipartite graphs something similar is obtained, but then correctly adapted (as long as the resolution parameter is also appropriately rescaled). .. note:: This function is not suited for directed graphs in the case of using the degree as node sizes. """ if types is not None: if isinstance(types, str): types = graph.vs[types] else: # Make sure it is a list types = list(types) if set(types) != set([0, 1]): new_type = _ig.UniqueIdGenerator() types = [new_type[t] for t in types] if set(types) != set([0, 1]): raise ValueError("More than one type specified.") if degree_as_node_size: if (graph.is_directed()): raise ValueError("This method is not suitable for directed graphs " + "when using degree as node sizes.") node_sizes = graph.degree() else: node_sizes = [1]*graph.vcount() partition_01 = cls(graph, node_sizes=node_sizes, resolution_parameter=resolution_parameter_01, **kwargs) H_0 = graph.subgraph_edges([], delete_vertices=False) partition_0 = cls(H_0, weights=None, node_sizes=[s if t == 0 else 0 for v, s, t in zip(graph.vs,node_sizes,types)], resolution_parameter=resolution_parameter_01 - resolution_parameter_0) H_1 = graph.subgraph_edges([], delete_vertices=False) partition_1 = cls(H_1, weights=None, node_sizes=[s if t == 1 else 0 for v, s, t in zip(graph.vs,node_sizes,types)], resolution_parameter=resolution_parameter_01 - resolution_parameter_1) return partition_01, partition_0, partition_1 leidenalg-0.11.0/src/leidenalg/Optimiser.py0000664000175000017510000007400515101156755020122 0ustar nileshnileshfrom . import _c_leiden from .VertexPartition import LinearResolutionParameterVertexPartition from collections import namedtuple from math import log, sqrt class Optimiser(object): r""" Class for doing community detection using the Leiden algorithm. The Leiden algorithm [1] derives from the Louvain algorithm [2]. The Louvain algorithm has an elegant formulation. It consists of two phases: (1) move nodes between communities; (2) aggregate the graph. It then repeats these phases on the aggregate graph. The Leiden algorithm consists of three phases: (1) move nodes; (2) refine communities; (3) aggregate the graph based on the refinement. The Louvain algorithm can lead to arbitrarily badly connected communities, whereas the Leiden algorithm guarantees communities are well-connected. In fact, it converges towards a partition in which all subsets of all communities are locally optimally assigned. Finally, the Leiden algorithm is also much faster, because it relies on a fast local move routine. There is one, rather technical, difference with the original Leiden algorithm. This implementation provides a general optimisation routine for any quality function. There is one aspect of the original Leiden algorithm that cannot be translated well in this framework: when merging subcommunities in the refinement procedure, it does not consider whether they are sufficiently well connected to the rest of the community. This implementation therefore does not guarantee subpartition :math:`\\gamma`-density. However, all other guarantees still hold: After each iteration 1. :math:`\\gamma`-separation 2. :math:`\\gamma`-connectivity After a stable iteration 3. Node optimality 4. Some subsets are locally optimally assigned Asymptotically 5. Uniform :math:`\\gamma`-density 6. Subset optimality The optimiser class provides a number of different methods for optimising a given partition. The overall optimisation procedure :func:`optimise_partition` calls either :func:`move_nodes` or :func:`merge_nodes` (which is controlled by :attr:`optimise_routine`) then aggregates the graph and repeats the same procedure. Possible, indicated by :attr:`refine_partition` the partition is refined before aggregating, meaning that subsets of communities are considered for moving around. Which routine is used for the refinement is indicated by :attr:`refine_routine`. For calculating the actual improvement of moving a node (corresponding a subset of nodes in the aggregate graph), the code relies on :func:`~VertexPartition.MutableVertexPartition.diff_move` which provides different values for different methods (e.g. modularity or CPM). The default settings are consistent with the Leiden algorithm. By not doing the refinement, you essentially get the Louvain algorithm with a fast local move. Finally, the Optimiser class provides a routine to construct a :func:`resolution_profile` on a resolution parameter. References ---------- .. [1] Traag, V.A., Waltman. L., Van Eck, N.-J. (2018). From Louvain to Leiden: guaranteeing well-connected communities. `arXiv:1810.08473 `_ .. [2] Blondel, V. D., Guillaume, J.-L., Lambiotte, R., & Lefebvre, E. (2008). Fast unfolding of communities in large networks. Journal of Statistical Mechanics: Theory and Experiment, 10008(10), 6. `10.1088/1742-5468/2008/10/P10008 `_ """ def __init__(self): """ Create a new Optimiser object """ self._optimiser = _c_leiden._new_Optimiser() #########################################################3 # consider_comms @property def consider_comms(self): """ Determine how alternative communities are considered for moving a node for *optimising* a partition. Nodes will only move to alternative communities that improve the given quality function. The default is :attr:`leidenalg.ALL_NEIGH_COMMS`. Notes ------- This attribute should be set to one of the following values * :attr:`leidenalg.ALL_NEIGH_COMMS` Consider all neighbouring communities for moving. * :attr:`leidenalg.ALL_COMMS` Consider all communities for moving. This is especially useful in the case of negative links, in which case it may be better to move a node to a non-neighbouring community. * :attr:`leidenalg.RAND_NEIGH_COMM` Consider a random neighbour community for moving. The probability to choose a community is proportional to the number of neighbours a node has in that community. * :attr:`leidenalg.RAND_COMM` Consider a random community for moving. The probability to choose a community is proportional to the number of nodes in that community. """ return _c_leiden._Optimiser_get_consider_comms(self._optimiser) @consider_comms.setter def consider_comms(self, value): _c_leiden._Optimiser_set_consider_comms(self._optimiser, value) #########################################################3 # refine consider_comms @property def refine_consider_comms(self): """ Determine how alternative communities are considered for moving a node when *refining* a partition. Nodes will only move to alternative communities that improve the given quality function. The default is :attr:`leidenalg.ALL_NEIGH_COMMS`. Notes ------- This attribute should be set to one of the following values * :attr:`leidenalg.ALL_NEIGH_COMMS` Consider all neighbouring communities for moving. * :attr:`leidenalg.ALL_COMMS` Consider all communities for moving. This is especially useful in the case of negative links, in which case it may be better to move a node to a non-neighbouring community. * :attr:`leidenalg.RAND_NEIGH_COMM` Consider a random neighbour community for moving. The probability to choose a community is proportional to the number of neighbours a node has in that community. * :attr:`leidenalg.RAND_COMM` Consider a random community for moving. The probability to choose a community is proportional to the number of nodes in that community. """ return _c_leiden._Optimiser_get_refine_consider_comms(self._optimiser) @refine_consider_comms.setter def refine_consider_comms(self, value): _c_leiden._Optimiser_set_refine_consider_comms(self._optimiser, value) #########################################################3 # optimise routine @property def optimise_routine(self): """ Determine the routine to use for *optimising* a partition. The default is :attr:`leidenalg.MOVE_NODES`. Notes ------- This attribute should be set to one of the following values * :attr:`leidenalg.MOVE_NODES` Use :func:`move_nodes`. * :attr:`leidenalg.MERGE_NODES` Use :func:`merge_nodes`. """ return _c_leiden._Optimiser_get_optimise_routine(self._optimiser) @optimise_routine.setter def optimise_routine(self, value): _c_leiden._Optimiser_set_optimise_routine(self._optimiser, value) #########################################################3 # optimise routine @property def refine_routine(self): """ Determine the routine to use for *refining* a partition. The default is :attr:`leidenalg.MERGE_NODES`. Notes ------- This attribute should be set to one of the following values * :attr:`leidenalg.MOVE_NODES` Use :func:`move_nodes_constrained`. * :attr:`leidenalg.MERGE_NODES` Use :func:`merge_nodes_constrained`. """ return _c_leiden._Optimiser_get_refine_routine(self._optimiser) @refine_routine.setter def refine_routine(self, value): _c_leiden._Optimiser_set_refine_routine(self._optimiser, value) #########################################################3 # refine_partition @property def refine_partition(self): """ boolean: if ``True`` refine partition before aggregation. """ return _c_leiden._Optimiser_get_refine_partition(self._optimiser) @refine_partition.setter def refine_partition(self, value): _c_leiden._Optimiser_set_refine_partition(self._optimiser, value) #########################################################3 # consider_empty_community @property def consider_empty_community(self): """ boolean: if ``True`` consider also moving nodes to an empty community (default). """ return _c_leiden._Optimiser_get_consider_empty_community(self._optimiser) @consider_empty_community.setter def consider_empty_community(self, value): _c_leiden._Optimiser_set_consider_empty_community(self._optimiser, value) #########################################################3 # max_comm_size @property def max_comm_size(self): """ Constrain the maximal community size. By default (zero), communities can be of any size. If this is set to a positive integer value, then communities will be constrained to be at most this total size. """ return _c_leiden._Optimiser_get_max_comm_size(self._optimiser) @max_comm_size.setter def max_comm_size(self, value): if value < 0: raise ValueError("negative max_comm_size: %s" % value) _c_leiden._Optimiser_set_max_comm_size(self._optimiser, value) ########################################################## # Set rng seed def set_rng_seed(self, value): """ Set the random seed for the random number generator. Parameters ---------- value The integer seed used in the random number generator """ _c_leiden._Optimiser_set_rng_seed(self._optimiser, value) def optimise_partition(self, partition, n_iterations=2, is_membership_fixed=None): """ Optimise the given partition. Parameters ---------- partition The :class:`~VertexPartition.MutableVertexPartition` to optimise. n_iterations : int Number of iterations to run the Leiden algorithm. By default, 2 iterations are run. If the number of iterations is negative, the Leiden algorithm is run until an iteration in which there was no improvement. is_membership_fixed: list of bools or None Boolean list of nodes that are not allowed to change community. The length of this list must be equal to the number of nodes. By default (None) all nodes can change community during the optimization. Returns ------- float Improvement in quality function. Examples -------- >>> G = ig.Graph.Famous('Zachary') >>> optimiser = la.Optimiser() >>> partition = la.ModularityVertexPartition(G) >>> diff = optimiser.optimise_partition(partition) or, fixing the membership of some nodes: >>> is_membership_fixed = [False for v in G.vs] >>> is_membership_fixed[4] = True >>> is_membership_fixed[6] = True >>> diff = optimiser.optimise_partition(partition, is_membership_fixed=is_membership_fixed) """ itr = 0 diff = 0 continue_iteration = itr < n_iterations or n_iterations < 0 if is_membership_fixed is not None: # Make sure it is a list is_membership_fixed = list(is_membership_fixed) while continue_iteration: diff_inc = _c_leiden._Optimiser_optimise_partition( self._optimiser, partition._partition, is_membership_fixed=is_membership_fixed, ) diff += diff_inc itr += 1 if n_iterations < 0: continue_iteration = (diff_inc > 0) else: continue_iteration = itr < n_iterations partition._update_internal_membership() return diff def optimise_partition_multiplex(self, partitions, layer_weights=None, n_iterations=2, is_membership_fixed=None): r""" Optimise the given partitions simultaneously. Parameters ---------- partitions List of :class:`~VertexPartition.MutableVertexPartition` layers to optimise. layer_weights List of weights of layers. is_membership_fixed: list of bools or None Boolean list of nodes that are not allowed to change community. The length of this list must be equal to the number of nodes. By default (None) all nodes can change community during the optimization. n_iterations : int Number of iterations to run the Leiden algorithm. By default, 2 iterations are run. If the number of iterations is negative, the Leiden algorithm is run until an iteration in which there was no improvement. Returns ------- float Improvement in quality of combined partitions, see `Notes <#notes-multiplex>`_. .. _notes-multiplex: Notes ----- This method assumes that the partitions are defined for graphs with the same vertices. The connections between the vertices may be different, but the vertices themselves should be identical. In other words, all vertices should have identical indices in all graphs (i.e. node `i` is assumed to be the same node in all graphs). The quality of the overall partition is simply the sum of the individual qualities for the various partitions, weighted by the layer_weight. If we denote by :math:`Q_k` the quality of layer :math:`k` and the weight by :math:`\\lambda_k`, the overall quality is then .. math:: Q = \\sum_k \\lambda_k Q_k. This is particularly useful for graphs containing negative links. When separating the graph in two graphs, the one containing only the positive links, and the other only the negative link, by supplying a negative weight to the latter layer, we try to find relatively many positive links within a community and relatively many negative links between communities. Note that in this case it may be better to assign a node to a community to which it is not connected so that :attr:`consider_comms` may be better set to :attr:`leidenalg.ALL_COMMS`. Besides multiplex graphs where each node is assumed to have a single community, it is also useful in the case of for example multiple time slices, or in situations where nodes can have different communities in different slices. The package includes some special helper functions for using :func:`optimise_partition_multiplex` in such cases, where there is a conversion required from (time) slices to layers suitable for use in this function. See Also -------- :func:`slices_to_layers` :func:`time_slices_to_layers` :func:`find_partition_multiplex` :func:`find_partition_temporal` Examples -------- >>> G_pos = ig.Graph.SBM(100, pref_matrix=[[0.5, 0.1], [0.1, 0.5]], block_sizes=[50, 50]) >>> G_neg = ig.Graph.SBM(100, pref_matrix=[[0.1, 0.5], [0.5, 0.1]], block_sizes=[50, 50]) >>> optimiser = la.Optimiser() >>> partition_pos = la.ModularityVertexPartition(G_pos) >>> partition_neg = la.ModularityVertexPartition(G_neg) >>> diff = optimiser.optimise_partition_multiplex( ... partitions=[partition_pos, partition_neg], ... layer_weights=[1,-1]) """ if not layer_weights: layer_weights = [1]*len(partitions) itr = 0 diff = 0 continue_iteration = itr < n_iterations or n_iterations < 0 while continue_iteration: diff_inc = _c_leiden._Optimiser_optimise_partition_multiplex( self._optimiser, [partition._partition for partition in partitions], layer_weights, is_membership_fixed) diff += diff_inc itr += 1 if n_iterations < 0: continue_iteration = (diff_inc > 0) else: continue_iteration = itr < n_iterations for partition in partitions: partition._update_internal_membership() return diff def move_nodes(self, partition, is_membership_fixed=None, consider_comms=None): """ Move nodes to alternative communities for *optimising* the partition. Parameters ---------- partition The partition for which to move nodes. is_membership_fixed: list of bools or None Boolean list of nodes that are not allowed to change community. The length of this list must be equal to the number of nodes. By default (None) all nodes can change community during the optimization. consider_comms If ``None`` uses :attr:`consider_comms`, but can be set to something else. Returns ------- float Improvement in quality function. Notes ----- When moving nodes, the function loops over nodes and considers moving the node to an alternative community. Which community depends on ``consider_comms``. The function terminates when no more nodes can be moved to an alternative community. See Also -------- :func:`Optimiser.move_nodes_constrained` :func:`Optimiser.merge_nodes` Examples -------- >>> G = ig.Graph.Famous('Zachary') >>> optimiser = la.Optimiser() >>> partition = la.ModularityVertexPartition(G) >>> diff = optimiser.move_nodes(partition) """ if (consider_comms is None): consider_comms = self.consider_comms diff = _c_leiden._Optimiser_move_nodes( self._optimiser, partition._partition, is_membership_fixed, consider_comms) partition._update_internal_membership() return diff def move_nodes_constrained(self, partition, constrained_partition, consider_comms=None): """ Move nodes to alternative communities for *refining* the partition. Parameters ---------- partition The partition for which to move nodes. constrained_partition The partition within which we may move nodes. consider_comms If ``None`` uses :attr:`refine_consider_comms`, but can be set to something else. Returns ------- float Improvement in quality function. Notes ----- The idea is constrain the movement of nodes to alternative communities to another partition. In other words, if there is a partition ``P`` which we want to refine, we can then initialize a new singleton partition, and move nodes in that partition constrained to ``P``. See Also -------- :func:`Optimiser.move_nodes` :func:`Optimiser.merge_nodes_constrained` Examples -------- >>> G = ig.Graph.Famous('Zachary') >>> optimiser = la.Optimiser() >>> partition = la.ModularityVertexPartition(G) >>> diff = optimiser.optimise_partition(partition) >>> refine_partition = la.ModularityVertexPartition(G) >>> diff = optimiser.move_nodes_constrained(refine_partition, partition) """ if (consider_comms is None): consider_comms = self.refine_consider_comms diff = _c_leiden._Optimiser_move_nodes_constrained(self._optimiser, partition._partition, constrained_partition._partition, consider_comms) partition._update_internal_membership() return diff def merge_nodes(self, partition, is_membership_fixed=None, consider_comms=None): """ Merge nodes for *optimising* the partition. Parameters ---------- partition The partition for which to merge nodes. is_membership_fixed: list of bools or None Boolean list of nodes that are not allowed to change community. The length of this list must be equal to the number of nodes. By default (None) all nodes can change community during the optimization. consider_comms If ``None`` uses :attr:`consider_comms`, but can be set to something else. Returns ------- float Improvement in quality function. Notes ----- This function loop over all nodes once and tries to merge them with another community. Merging in this case implies that a node will never be removed from a community, only merged with other communities. See Also -------- :func:`Optimiser.move_nodes` :func:`Optimiser.merge_nodes_constrained` Examples -------- >>> G = ig.Graph.Famous('Zachary') >>> optimiser = la.Optimiser() >>> partition = la.ModularityVertexPartition(G) >>> diff = optimiser.merge_nodes(partition) """ if (consider_comms is None): consider_comms = self.consider_comms diff = _c_leiden._Optimiser_merge_nodes( self._optimiser, partition._partition, is_membership_fixed, consider_comms) partition._update_internal_membership() return diff def merge_nodes_constrained(self, partition, constrained_partition, consider_comms=None): """ Merge nodes for *refining* the partition. Parameters ---------- partition The partition for which to merge nodes. constrained_partition The partition within which we may merge nodes. consider_comms If ``None`` uses :attr:`refine_consider_comms`, but can be set to something else. Returns ------- float Improvement in quality function. Notes ----- The idea is constrain the merging of nodes to another partition. In other words, if there is a partition ``P`` which we want to refine, we can then initialize a new singleton partition, and move nodes in that partition constrained to ``P``. See Also -------- :func:`Optimiser.move_nodes_constrained` :func:`Optimiser.merge_nodes` Examples -------- >>> G = ig.Graph.Famous('Zachary') >>> optimiser = la.Optimiser() >>> partition = la.ModularityVertexPartition(G) >>> diff = optimiser.optimise_partition(partition) >>> refine_partition = la.ModularityVertexPartition(G) >>> diff = optimiser.move_nodes_constrained(refine_partition, partition) """ if (consider_comms is None): consider_comms = self.refine_consider_comms diff = _c_leiden._Optimiser_merge_nodes_constrained(self._optimiser, partition._partition, constrained_partition._partition, consider_comms) partition._update_internal_membership() return diff def resolution_profile(self, graph, partition_type, resolution_range, weights=None, bisect_func=lambda p: p.bisect_value(), min_diff_bisect_value=1, min_diff_resolution=1e-3, linear_bisection=False, number_iterations=1, **kwargs ): """ Use bisectioning on the resolution parameter in order to construct a resolution profile. Parameters ---------- graph The graph for which to construct a resolution profile. partition_type The type of :class:`~VertexPartition.MutableVertexPartition` used to find a partition (must support resolution parameters obviously). resolution_range The range of resolution values that we would like to scan. weights If provided, indicates the edge attribute to use as a weight. Returns ------- list of :class:`~VertexPartition.MutableVertexPartition` A list of partitions for different resolutions. Other Parameters ---------------- bisect_func The function used for bisectioning. For the methods currently implemented, this should usually not be altered. min_diff_bisect_value The difference in the value returned by the bisect_func below which the bisectioning stops (i.e. by default, a difference of a single edge does not trigger further bisectioning). min_diff_resolution The difference in resolution below which the bisectioning stops. For positive differences, the logarithmic difference is used by default, i.e. ``diff = log(res_1) - log(res_2) = log(res_1/res_2)``, for which ``diff > min_diff_resolution`` to continue bisectioning. Set the linear_bisection to true in order to use only linear bisectioning (in the case of negative resolution parameters for example, which can happen with negative weights). linear_bisection Whether the bisectioning will be done on a linear or on a logarithmic basis (if possible). number_iterations Indicates the number of iterations of the algorithm to run. If negative (or zero) the algorithm is run until a stable iteration. Examples -------- >>> G = ig.Graph.Famous('Zachary') >>> optimiser = la.Optimiser() >>> profile = optimiser.resolution_profile(G, la.CPMVertexPartition, ... resolution_range=(0,1)) """ # Helper function for cleaning values to be a stepwise function def clean_stepwise(bisect_values): # Check best partition for each resolution parameter for res, bisect in bisect_values.items(): best_bisect = bisect best_quality = bisect.partition.quality(res) for res2, bisect2 in bisect_values.items(): if bisect2.partition.quality(res) > best_quality: best_bisect = bisect2 best_quality = bisect2.partition.quality(res) if best_bisect != bisect: bisect_values[res] = best_bisect # We only need to keep the changes in the bisection values bisect_list = sorted([(res, part.bisect_value) for res, part in bisect_values.items()], key=lambda x: x[0]) for (res1, v1), (res2, v2) \ in zip(bisect_list, bisect_list[1:]): # If two consecutive bisection values are the same, remove the second # resolution parameter if v1 == v2: del bisect_values[res2] for res, bisect in bisect_values.items(): bisect.partition.resolution_parameter = res # We assume here that the bisection values are # monotonically decreasing with increasing resolution # parameter values. def ensure_monotonicity(bisect_values, new_res): # First check if this partition improves on any other partition for res, bisect_part in bisect_values.items(): if bisect_values[new_res].partition.quality(res) > bisect_part.partition.quality(res): bisect_values[res] = bisect_values[new_res] # Then check what is best partition for the new_res current_quality = bisect_values[new_res].partition.quality(new_res) best_res = new_res for res, bisect_part in bisect_values.items(): if bisect_part.partition.quality(new_res) > current_quality: best_res = new_res bisect_values[new_res] = bisect_values[best_res] def find_partition(self, graph, partition_type, weights=None, **kwargs): partition = partition_type(graph, weights=weights, **kwargs) n_itr = 0 while self.optimise_partition(partition) > 0 and \ (n_itr < number_iterations or number_iterations <= 0): n_itr += 1 return partition assert issubclass(partition_type, LinearResolutionParameterVertexPartition), "Bisectioning only works on partitions with a linear resolution parameter." # Start actual bisectioning bisect_values = {} stack_res_range = [] # Push first range onto the stack stack_res_range.append(resolution_range) # Make sure the bisection values are calculated # The namedtuple we will use in the bisection function BisectPartition = namedtuple('BisectPartition', ['partition', 'bisect_value']) partition = find_partition(self, graph=graph, partition_type=partition_type, weights=weights,resolution_parameter=resolution_range[0], **kwargs) bisect_values[resolution_range[0]] = BisectPartition(partition=partition, bisect_value=bisect_func(partition)) partition = find_partition(self, graph=graph, partition_type=partition_type, weights=weights, resolution_parameter=resolution_range[1], **kwargs) bisect_values[resolution_range[1]] = BisectPartition(partition=partition, bisect_value=bisect_func(partition)) # While stack of ranges not yet empty try: from tqdm import tqdm progress = tqdm(total=float('inf')) except: progress = None while stack_res_range: # Get the current range from the stack current_range = stack_res_range.pop() # Get the difference in bisection values diff_bisect_value = abs(bisect_values[current_range[0]].bisect_value - bisect_values[current_range[1]].bisect_value) # Get the difference in resolution parameter (in log space if 0 is not in # the interval (assuming only non-negative resolution parameters). if current_range[0] > 0 and current_range[1] > 0 and not linear_bisection: diff_resolution = log(current_range[1]/current_range[0]) else: diff_resolution = abs(current_range[1] - current_range[0]) # Check if we still want to scan a smaller interval # If we would like to bisect this interval if diff_bisect_value > min_diff_bisect_value and \ diff_resolution > min_diff_resolution: # Determine new resolution value if current_range[0] > 0 and current_range[1] > 0 and not linear_bisection: new_res = sqrt(current_range[1]*current_range[0]) else: new_res = sum(current_range)/2.0 # Bisect left (push on stack) stack_res_range.append((current_range[0], new_res)) # Bisect right (push on stack) stack_res_range.append((new_res, current_range[1])) # If we haven't scanned this resolution value yet, # do so now if not new_res in bisect_values: partition = find_partition(self, graph, partition_type=partition_type, weights=weights, resolution_parameter=new_res, **kwargs) bisect_values[new_res] = BisectPartition(partition=partition, bisect_value=bisect_func(partition)) if progress is not None: progress.update(1) progress.set_postfix(resolution_parameter=new_res, refresh=False) # Because of stochastic differences in different runs, the monotonicity # of the bisection values might be violated, so check for any # inconsistencies ensure_monotonicity(bisect_values, new_res) if progress is not None: progress.close() # Ensure we only keep those resolution values for which # the bisection values actually changed, instead of all of them clean_stepwise(bisect_values) # Use an ordered dict so that when iterating over it, the results appear in # increasing order based on the resolution value. return sorted((bisect.partition for res, bisect in bisect_values.items()), key=lambda x: x.resolution_parameter) leidenalg-0.11.0/setup.py0000664000175000017510000000333315101156755014570 0ustar nileshnilesh#!usr/bin/env python import os import platform import sys import glob ########################################################################### # Check Python's version info and exit early if it is too old if sys.version_info < (3, 7): print("This module requires Python >= 3.7") sys.exit(0) ########################################################################### from setuptools import setup, Extension try: from wheel.bdist_wheel import bdist_wheel except ImportError: bdist_wheel = None if bdist_wheel is not None: class bdist_wheel_abi3(bdist_wheel): def get_tag(self): python, abi, plat = super().get_tag() if python.startswith("cp"): # on CPython, our wheels are abi3 and compatible back to 3.5 return "cp38", "abi3", plat return python, abi, plat else: bdist_wheel_abi3 = None should_build_abi3_wheel = ( bdist_wheel_abi3 and platform.python_implementation() == "CPython" and sys.version_info >= (3, 8) ) # Define the extension macros = [] if should_build_abi3_wheel: macros.append(("Py_LIMITED_API", "0x03090000")) cmdclass = {} if should_build_abi3_wheel: cmdclass["bdist_wheel"] = bdist_wheel_abi3 setup( ext_modules = [ Extension('leidenalg._c_leiden', sources = glob.glob(os.path.join('src', 'leidenalg', '*.cpp')), py_limited_api=should_build_abi3_wheel, define_macros=macros, libraries = ['libleidenalg', 'igraph'], include_dirs=['include', 'build-deps/install/include'], library_dirs=['build-deps/install/lib', 'build-deps/install/lib64'], ) ], cmdclass=cmdclass ) leidenalg-0.11.0/setup.cfg0000664000175000017510000000017215101156755014675 0ustar nileshnilesh[build_sphinx] source-dir = doc/source build-dir = doc/build all_files = 1 [upload_sphinx] upload-dir = doc/build/html leidenalg-0.11.0/scripts/0000775000175000017510000000000015101156755014543 5ustar nileshnileshleidenalg-0.11.0/scripts/build_libleidenalg.sh0000775000175000017510000000311115101156755020670 0ustar nileshnileshLIBLEIDENALG_VERSION=0.12.0 ROOT_DIR=`pwd` echo "Using root dir ${ROOT_DIR}" # Create source directory if [ ! -d "${ROOT_DIR}/build-deps/src" ]; then echo "" echo "Make directory ${ROOT_DIR}/build-deps/src" mkdir -p ${ROOT_DIR}/build-deps/src fi cd ${ROOT_DIR}/build-deps/src if [ ! -d "libleidenalg" ]; then echo "" echo "Cloning libleidenalg into ${ROOT_DIR}/build-deps/src/libleidenalg" # Clone repository if it does not exist yet git clone --branch ${LIBLEIDENALG_VERSION} https://github.com/vtraag/libleidenalg.git --single-branch fi # Make sure the git repository points to the correct version echo "" echo "Checking out ${LIBLEIDENALG_VERSION} in ${ROOT_DIR}/build-deps/src/libleidenalg" cd ${ROOT_DIR}/build-deps/src/libleidenalg git fetch origin tag ${LIBLEIDENALG_VERSION} --no-tags git checkout ${LIBLEIDENALG_VERSION} # Make build directory if [ ! -d "${ROOT_DIR}/build-deps/build/libleidenalg" ]; then echo "" echo "Make directory ${ROOT_DIR}/build-deps/build/libleidenalg" mkdir -p ${ROOT_DIR}/build-deps/build/libleidenalg fi # Configure, build and install cd ${ROOT_DIR}/build-deps/build/libleidenalg echo "" echo "Configure libleidenalg build" cmake ${ROOT_DIR}/build-deps/src/libleidenalg \ -DCMAKE_INSTALL_PREFIX=${ROOT_DIR}/build-deps/install/ \ -DBUILD_SHARED_LIBS=ON \ -Digraph_ROOT=${ROOT_DIR}/build-deps/install/lib/cmake/igraph/ \ ${EXTRA_CMAKE_ARGS} echo "" echo "Build libleidenalg" cmake --build . --config Release echo "" echo "Install libleidenalg to ${ROOT_DIR}/build-deps/install/" cmake --build . --target install --config Release leidenalg-0.11.0/scripts/build_libleidenalg.bat0000775000175000017510000000272415101156755021035 0ustar nileshnilesh@echo off set LIBLEIDENALG_VERSION="0.12.0" set ROOT_DIR=%cd% echo Using root dir %ROOT_DIR% if not exist "%ROOT_DIR%\build-deps\src\" ( md %ROOT_DIR%\build-deps\src ) cd "%ROOT_DIR%\build-deps\src" if not exist "libleidenalg\" ( echo. echo Cloning libleidenalg into %ROOT_DIR%\build-deps\src\libleidenalg REM Clone repository if it does not exist yet git clone --branch %libleidenalg_VERSION% https://github.com/vtraag/libleidenalg.git ) REM Make sure the git repository points to the correct version echo. echo Checking out %libleidenalg_VERSION} in ${ROOT_DIR%\build-deps\src\libleidenalg cd "%ROOT_DIR%\build-deps\src\libleidenalg" git fetch origin tag ${LIBLEIDENALG_VERSION} --no-tags git checkout ${LIBLEIDENALG_VERSION} REM Make build directory if not exist "%ROOT_DIR%\build-deps\build\libleidenalg\" ( echo. echo Make directory %ROOT_DIR%\build-deps\build\libleidenalg md %ROOT_DIR%\build-deps\build\libleidenalg ) REM Configure, build and install cd "%ROOT_DIR%\build-deps\build\libleidenalg" echo. echo Configure libleidenalg build cmake %ROOT_DIR%\build-deps\src\libleidenalg ^ -DCMAKE_INSTALL_PREFIX=%ROOT_DIR%\build-deps\install\ ^ -DBUILD_SHARED_LIBS=ON ^ -Digraph_ROOT=%ROOT_DIR%\build-deps\install\lib\cmake\igraph\ ^ %EXTRA_CMAKE_ARGS% echo. echo Build libleidenalg cmake --build . --config Release echo. echo Install libleidenalg to %ROOT_DIR%\build-deps\install\ cmake --build . --target install --config Release cd "%ROOT_DIR%"leidenalg-0.11.0/scripts/build_igraph.sh0000775000175000017510000000337515101156755017543 0ustar nileshnileshIGRAPH_VERSION=1.0.0 ROOT_DIR=`pwd` echo "Using root dir ${ROOT_DIR}" # Create source directory if [ ! -d "${ROOT_DIR}/build-deps/src" ]; then echo "" echo "Make directory ${ROOT_DIR}/build-deps/src" mkdir -p ${ROOT_DIR}/build-deps/src fi cd ${ROOT_DIR}/build-deps/src if [ ! -d "igraph" ]; then echo "" echo "Cloning igraph into ${ROOT_DIR}/build-deps/src/igraph" # Clone repository if it does not exist yet git clone --depth 1 --branch ${IGRAPH_VERSION} https://github.com/igraph/igraph.git --single-branch fi # Make sure the git repository points to the correct version echo "" echo "Checking out ${IGRAPH_VERSION} in ${ROOT_DIR}/build-deps/src/igraph" cd ${ROOT_DIR}/build-deps/src/igraph git fetch origin tag ${IGRAPH_VERSION} --no-tags git checkout ${IGRAPH_VERSION} # Make build directory if [ ! -d "${ROOT_DIR}/build-deps/build/igraph" ]; then echo "" echo "Make directory ${ROOT_DIR}/build-deps/build/igraph" mkdir -p ${ROOT_DIR}/build-deps/build/igraph fi # Configure, build and install cd ${ROOT_DIR}/build-deps/build/igraph echo "" echo "Configure igraph build" cmake ${ROOT_DIR}/build-deps/src/igraph \ -DCMAKE_INSTALL_PREFIX=${ROOT_DIR}/build-deps/install/ \ -DBUILD_SHARED_LIBS=ON \ -DIGRAPH_GLPK_SUPPORT=OFF \ -DIGRAPH_GRAPHML_SUPPORT=OFF \ -DIGRAPH_OPENMP_SUPPORT=OFF \ -DIGRAPH_USE_INTERNAL_BLAS=ON \ -DIGRAPH_USE_INTERNAL_LAPACK=ON \ -DIGRAPH_USE_INTERNAL_ARPACK=ON \ -DIGRAPH_USE_INTERNAL_GLPK=OFF \ -DIGRAPH_USE_INTERNAL_GMP=ON \ -DIGRAPH_WARNINGS_AS_ERRORS=OFF \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_TESTING=OFF \ ${EXTRA_CMAKE_ARGS} echo "" echo "Build igraph" cmake --build . --config Release echo "" echo "Install igraph to ${ROOT_DIR}/build-deps/install/" cmake --build . --target install --config Release leidenalg-0.11.0/scripts/build_igraph.bat0000775000175000017510000000320515101156755017667 0ustar nileshnilesh@echo off set IGRAPH_VERSION=1.0.0 set ROOT_DIR=%cd% echo Using root dir %ROOT_DIR% if not exist "%ROOT_DIR%\build-deps\src\" ( md %ROOT_DIR%\build-deps\src ) cd "%ROOT_DIR%\build-deps\src" if not exist "igraph\" ( echo. echo Cloning igraph into %ROOT_DIR%\build-deps\src\igraph REM Clone repository if it does not exist yet git clone --depth 1 --branch %IGRAPH_VERSION% https://github.com/igraph/igraph.git ) REM Make sure the git repository points to the correct version echo. echo Checking out %IGRAPH_VERSION} in ${ROOT_DIR%\build-deps\src\igraph cd "%ROOT_DIR%\build-deps\src\igraph" git fetch origin tag %IGRAPH_VERSION% --no-tags git checkout %IGRAPH_VERSION% REM Make build directory if not exist "%ROOT_DIR%\build-deps\build\igraph\" ( echo. echo Make directory %ROOT_DIR%\build-deps\build\igraph md %ROOT_DIR%\build-deps\build\igraph ) REM Configure, build and install cd "%ROOT_DIR%\build-deps\build\igraph" echo. echo Configure igraph build cmake %ROOT_DIR%\build-deps\src\igraph ^ -DCMAKE_INSTALL_PREFIX=%ROOT_DIR%\build-deps\install\ ^ -DBUILD_SHARED_LIBS=ON ^ -DIGRAPH_GLPK_SUPPORT=OFF ^ -DIGRAPH_GRAPHML_SUPPORT=OFF ^ -DIGRAPH_OPENMP_SUPPORT=OFF ^ -DIGRAPH_USE_INTERNAL_BLAS=ON ^ -DIGRAPH_USE_INTERNAL_LAPACK=ON ^ -DIGRAPH_USE_INTERNAL_ARPACK=ON ^ -DIGRAPH_USE_INTERNAL_GLPK=OFF ^ -DIGRAPH_USE_INTERNAL_GMP=ON ^ -DIGRAPH_WARNINGS_AS_ERRORS=OFF ^ -DCMAKE_BUILD_TYPE=Release ^ -DBUILD_TESTING=OFF ^ %EXTRA_CMAKE_ARGS% echo. echo Build igraph cmake --build . --config Release echo. echo Install igraph to %ROOT_DIR%\build-deps\install\ cmake --build . --target install --config Release cd "%ROOT_DIR%"leidenalg-0.11.0/pyproject.toml0000664000175000017510000000221715101156755015772 0ustar nileshnilesh[build-system] requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] build-backend = "setuptools.build_meta" [project] name = "leidenalg" authors = [ {name = "V.A. Traag", email = "vincent@traag.net"}, ] description = "Leiden is a general algorithm for methods of community detection in large networks." readme = "README.rst" requires-python = ">=3.7" keywords=[ "graph", "network", "community detection", "clustering" ] license = "GPL-3.0-or-later" classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: End Users/Desktop", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: C++", "Topic :: Scientific/Engineering :: Mathematics", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Sociology" ] dependencies = [ "igraph >= 1.0.0,< 2.0", ] dynamic = ["version"] [tool.setuptools_scm] write_to = "src/leidenalg/version.py" leidenalg-0.11.0/include/0000775000175000017510000000000015101156755014477 5ustar nileshnileshleidenalg-0.11.0/include/python_partition_interface.h0000664000175000017510000001020615101156755022301 0ustar nileshnilesh#ifndef PYNTERFACE_PARTITION_H_INCLUDED #define PYNTERFACE_PARTITION_H_INCLUDED #include #include #include #include #include #include #include #include #include #include #include #ifdef DEBUG #include using std::cerr; using std::endl; #endif MutableVertexPartition* create_partition(Graph* graph, char* method, vector* initial_membership, double resolution_parameter); MutableVertexPartition* create_partition_from_py(PyObject* py_obj_graph, char* method, PyObject* py_initial_membership, PyObject* py_weights, PyObject* py_node_sizes, double resolution_parameter); Graph* create_graph_from_py(PyObject* py_obj_graph, PyObject* py_node_sizes); Graph* create_graph_from_py(PyObject* py_obj_graph, PyObject* py_node_sizes, PyObject* py_weights); Graph* create_graph_from_py(PyObject* py_obj_graph, PyObject* py_node_sizes, PyObject* py_weights, bool check_positive_weight, bool correct_self_loops); vector create_size_t_vector(PyObject* py_list); PyObject* capsule_MutableVertexPartition(MutableVertexPartition* partition); MutableVertexPartition* decapsule_MutableVertexPartition(PyObject* py_partition); void del_MutableVertexPartition(PyObject *self); #ifdef __cplusplus extern "C" { #endif PyObject* _new_ModularityVertexPartition(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _new_SignificanceVertexPartition(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _new_SurpriseVertexPartition(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _new_CPMVertexPartition(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _new_RBERVertexPartition(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _new_RBConfigurationVertexPartition(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_diff_move(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_move_node(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_aggregate_partition(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_get_py_igraph(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_from_coarse_partition(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_renumber_communities(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_quality(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_total_weight_in_comm(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_total_weight_from_comm(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_total_weight_to_comm(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_total_weight_in_all_comms(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_total_possible_edges_in_all_comms(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_weight_to_comm(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_weight_from_comm(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_get_membership(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _MutableVertexPartition_set_membership(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _ResolutionParameterVertexPartition_get_resolution(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _ResolutionParameterVertexPartition_set_resolution(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _ResolutionParameterVertexPartition_quality(PyObject *self, PyObject *args, PyObject *keywds); #ifdef __cplusplus } #endif #endif // PYNTERFACE_PARTITION_H_INCLUDED leidenalg-0.11.0/include/python_optimiser_interface.h0000664000175000017510000000557615101156755022321 0ustar nileshnilesh#ifndef PYNTERFACE_OPTIMISER_H_INCLUDED #define PYNTERFACE_OPTIMISER_H_INCLUDED #include #include #include #include #include #include #include #include #include #include #include "python_partition_interface.h" #ifdef DEBUG #include using std::cerr; using std::endl; #endif PyObject* capsule_Optimiser(Optimiser* optimiser); Optimiser* decapsule_Optimiser(PyObject* py_optimiser); void del_Optimiser(PyObject* py_optimiser); #ifdef __cplusplus extern "C" { #endif PyObject* _new_Optimiser(PyObject *self, PyObject *args); PyObject* _Optimiser_optimise_partition(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_optimise_partition_multiplex(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_move_nodes(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_move_nodes_constrained(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_merge_nodes(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_merge_nodes_constrained(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_set_consider_comms(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_set_refine_consider_comms(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_set_optimise_routine(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_set_refine_routine(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_set_consider_empty_community(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_set_refine_partition(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_set_max_comm_size(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_set_rng_seed(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_get_consider_comms(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_get_refine_consider_comms(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_get_optimise_routine(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_get_refine_routine(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_get_consider_empty_community(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_get_refine_partition(PyObject *self, PyObject *args, PyObject *keywds); PyObject* _Optimiser_get_max_comm_size(PyObject *self, PyObject *args, PyObject *keywds); #ifdef __cplusplus } #endif #endif // PYNTERFACE_OPTIMISER_H_INCLUDED leidenalg-0.11.0/include/pynterface.h0000664000175000017510000002315515101156755017016 0ustar nileshnilesh#ifndef PYNTERFACE_H_INCLUDED #define PYNTERFACE_H_INCLUDED #include #include #include #include #include #include #include #include #include #include #include "python_partition_interface.h" #include "python_optimiser_interface.h" #ifdef __cplusplus extern "C" { #endif PyObject* _set_rng_seed(PyObject *self, PyObject *args, PyObject *keywds); static PyMethodDef leiden_funcs[] = { {"_new_ModularityVertexPartition", (PyCFunction)_new_ModularityVertexPartition, METH_VARARGS | METH_KEYWORDS, ""}, {"_new_SignificanceVertexPartition", (PyCFunction)_new_SignificanceVertexPartition, METH_VARARGS | METH_KEYWORDS, ""}, {"_new_SurpriseVertexPartition", (PyCFunction)_new_SurpriseVertexPartition, METH_VARARGS | METH_KEYWORDS, ""}, {"_new_CPMVertexPartition", (PyCFunction)_new_CPMVertexPartition, METH_VARARGS | METH_KEYWORDS, ""}, {"_new_RBERVertexPartition", (PyCFunction)_new_RBERVertexPartition, METH_VARARGS | METH_KEYWORDS, ""}, {"_new_RBConfigurationVertexPartition", (PyCFunction)_new_RBConfigurationVertexPartition, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_diff_move", (PyCFunction)_MutableVertexPartition_diff_move, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_move_node", (PyCFunction)_MutableVertexPartition_move_node, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_get_py_igraph", (PyCFunction)_MutableVertexPartition_get_py_igraph, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_aggregate_partition", (PyCFunction)_MutableVertexPartition_aggregate_partition, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_from_coarse_partition", (PyCFunction)_MutableVertexPartition_from_coarse_partition, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_renumber_communities", (PyCFunction)_MutableVertexPartition_renumber_communities, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_quality", (PyCFunction)_MutableVertexPartition_quality, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_total_weight_in_comm", (PyCFunction)_MutableVertexPartition_total_weight_in_comm, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_total_weight_from_comm", (PyCFunction)_MutableVertexPartition_total_weight_from_comm, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_total_weight_to_comm", (PyCFunction)_MutableVertexPartition_total_weight_to_comm, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_total_weight_in_all_comms", (PyCFunction)_MutableVertexPartition_total_weight_in_all_comms, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_total_possible_edges_in_all_comms", (PyCFunction)_MutableVertexPartition_total_possible_edges_in_all_comms, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_weight_to_comm", (PyCFunction)_MutableVertexPartition_weight_to_comm, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_weight_from_comm", (PyCFunction)_MutableVertexPartition_weight_from_comm, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_get_membership", (PyCFunction)_MutableVertexPartition_get_membership, METH_VARARGS | METH_KEYWORDS, ""}, {"_MutableVertexPartition_set_membership", (PyCFunction)_MutableVertexPartition_set_membership, METH_VARARGS | METH_KEYWORDS, ""}, {"_ResolutionParameterVertexPartition_get_resolution", (PyCFunction)_ResolutionParameterVertexPartition_get_resolution, METH_VARARGS | METH_KEYWORDS, ""}, {"_ResolutionParameterVertexPartition_set_resolution", (PyCFunction)_ResolutionParameterVertexPartition_set_resolution, METH_VARARGS | METH_KEYWORDS, ""}, {"_ResolutionParameterVertexPartition_quality", (PyCFunction)_ResolutionParameterVertexPartition_quality, METH_VARARGS | METH_KEYWORDS, ""}, {"_new_Optimiser", (PyCFunction)_new_Optimiser, METH_NOARGS, ""}, {"_Optimiser_optimise_partition", (PyCFunction)_Optimiser_optimise_partition, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_optimise_partition_multiplex", (PyCFunction)_Optimiser_optimise_partition_multiplex, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_move_nodes", (PyCFunction)_Optimiser_move_nodes, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_move_nodes_constrained", (PyCFunction)_Optimiser_move_nodes_constrained, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_merge_nodes", (PyCFunction)_Optimiser_merge_nodes, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_merge_nodes_constrained", (PyCFunction)_Optimiser_merge_nodes_constrained, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_set_consider_comms", (PyCFunction)_Optimiser_set_consider_comms, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_set_refine_consider_comms", (PyCFunction)_Optimiser_set_refine_consider_comms, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_set_optimise_routine", (PyCFunction)_Optimiser_set_optimise_routine, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_set_refine_routine", (PyCFunction)_Optimiser_set_refine_routine, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_set_consider_empty_community", (PyCFunction)_Optimiser_set_consider_empty_community, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_set_refine_partition", (PyCFunction)_Optimiser_set_refine_partition, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_set_max_comm_size", (PyCFunction)_Optimiser_set_max_comm_size, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_get_consider_comms", (PyCFunction)_Optimiser_get_consider_comms, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_get_refine_consider_comms", (PyCFunction)_Optimiser_get_refine_consider_comms, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_get_optimise_routine", (PyCFunction)_Optimiser_get_optimise_routine, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_get_refine_routine", (PyCFunction)_Optimiser_get_refine_routine, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_get_consider_empty_community", (PyCFunction)_Optimiser_get_consider_empty_community, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_get_refine_partition", (PyCFunction)_Optimiser_get_refine_partition, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_get_max_comm_size", (PyCFunction)_Optimiser_get_max_comm_size, METH_VARARGS | METH_KEYWORDS, ""}, {"_Optimiser_set_rng_seed", (PyCFunction)_Optimiser_set_rng_seed, METH_VARARGS | METH_KEYWORDS, ""}, {NULL} }; struct module_state { PyObject *error; }; #define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) static int leiden_traverse(PyObject *m, visitproc visit, void *arg) { Py_VISIT(GETSTATE(m)->error); return 0; } static int leiden_clear(PyObject *m) { Py_CLEAR(GETSTATE(m)->error); return 0; } static struct PyModuleDef leidendef = { PyModuleDef_HEAD_INIT, "_c_leiden", NULL, sizeof(struct module_state), leiden_funcs, NULL, leiden_traverse, leiden_clear, NULL }; #define INITERROR return NULL PyObject * PyInit__c_leiden(void) { PyObject* module = PyModule_Create(&leidendef); PyModule_AddIntConstant(module, "ALL_COMMS", Optimiser::ALL_COMMS); PyModule_AddIntConstant(module, "ALL_NEIGH_COMMS", Optimiser::ALL_NEIGH_COMMS); PyModule_AddIntConstant(module, "RAND_COMM", Optimiser::RAND_COMM); PyModule_AddIntConstant(module, "RAND_NEIGH_COMM", Optimiser::RAND_NEIGH_COMM); PyModule_AddIntConstant(module, "MOVE_NODES", Optimiser::MOVE_NODES); PyModule_AddIntConstant(module, "MERGE_NODES", Optimiser::MERGE_NODES); if (module == NULL) INITERROR; struct module_state *st = GETSTATE(module); st->error = PyErr_NewException("leidenalg.Error", NULL, NULL); if (st->error == NULL) { Py_DECREF(module); INITERROR; } return module; } #ifdef __cplusplus } #endif #endif // PYNTERFACE_H_INCLUDED leidenalg-0.11.0/etc/0000775000175000017510000000000015101156755013627 5ustar nileshnileshleidenalg-0.11.0/etc/arith_apple_m1.h0000664000175000017510000000030215101156755016660 0ustar nileshnilesh#define IEEE_8087 #define Arith_Kind_ASL 1 #define Long int #define Intcast (int)(long) #define Double_Align #define X64_bit_pointers #define NANCHECK #define QNaN0 0x0 #define QNaN1 0x7ff80000 leidenalg-0.11.0/doc/0000775000175000017510000000000015101156755013621 5ustar nileshnileshleidenalg-0.11.0/doc/source/0000775000175000017510000000000015101156755015121 5ustar nileshnileshleidenalg-0.11.0/doc/source/reference.rst0000664000175000017510000000273215101156755017615 0ustar nileshnileshReference =============== Module functions ---------------- .. automodule:: leidenalg :members: find_partition, find_partition_multiplex, find_partition_temporal, slices_to_layers, time_slices_to_layers, :undoc-members: :show-inheritance: Optimiser --------- .. autoclass:: Optimiser :members: :undoc-members: :show-inheritance: MutableVertexPartition ---------------------- .. autoclass:: leidenalg.VertexPartition.MutableVertexPartition :members: :undoc-members: :show-inheritance: ModularityVertexPartition ------------------------- .. autoclass:: ModularityVertexPartition :members: :undoc-members: :show-inheritance: RBConfigurationVertexPartition ------------------------------ .. autoclass:: RBConfigurationVertexPartition :members: resolution_parameter :undoc-members: :show-inheritance: RBERVertexPartition ------------------- .. autoclass:: RBERVertexPartition :members: :undoc-members: :show-inheritance: CPMVertexPartition ------------------ .. autoclass:: CPMVertexPartition :members: :undoc-members: :show-inheritance: SignificanceVertexPartition --------------------------- .. autoclass:: SignificanceVertexPartition :members: :undoc-members: :show-inheritance: SurpriseVertexPartition ----------------------- .. autoclass:: SurpriseVertexPartition :members: :undoc-members: :show-inheritance: leidenalg-0.11.0/doc/source/multiplex.rst0000664000175000017510000004504415101156755017705 0ustar nileshnileshMultiplex ========= The implementation of multiplex community detection builds on ideas in [1]_. The most basic form simply considers two or more graphs which are defined on the same vertex set, but which have differing edge sets. In this context, each node is identified with a single community, and cannot have different communities for different graphs. We call this *layers* of graphs in this context. This format is actually more flexible than it looks, but you have to construct the layer graphs in a smart way. Instead of having layers of graphs which are always identified on the same vertex set, you could define *slices* of graphs which do not necessarily have the same vertex set. Using slices we would like to assign a node to a community for each slice, so that the community for a node can be different for different slices, rather than always being the same for all layers. We can translate *slices* into *layers* but it is not an easy transformation to grasp fully. But by doing so, we can again rely on the same machinery we developed for dealing with layers. Throughout the remained of this section, we assume an optimiser has been created: >>> optimiser = la.Optimiser() Layer multiplex --------------- If we have two graphs which are identified on exactly the same vertex set, we say we have two *layers*. For example, suppose graph ``G_telephone`` contains the communication between friends over the telephone and that the graph ``G_email`` contains the communication between friends via mail. The exact same vertex set then means that ``G_telephone.vs[i]`` is identical to the node ``G_email.vs[i]``. For each layer we can separately specify the type of partition that we look for. In principle they could be different for each layer, but for now we will assume the type of partition is the same for all layers. The quality of all partitions combined is simply the sum of the individual qualities for the various partitions, weighted by the ``layer_weight``. If we denote by :math:`q_k` the quality of layer :math:`k` and the weight by :math:`w_k`, the overall quality is then .. math:: q = \sum_k w_k q_k. The optimisation algorithm is no different from the standard algorithm. We simply calculate the overall difference of moving a node to another community as the sum of the individual differences in all partitions. The rest (aggregating and repeating on the aggregate partition) simple proceeds as usual. The most straightforward way to use this is then to use :func:`~leidenalg.find_partition_multiplex`: .. testsetup:: G_telephone = ig.Graph.Erdos_Renyi(100, 0.1); G_email = ig.Graph.Erdos_Renyi(100, 0.1); >>> membership, improv = la.find_partition_multiplex( ... [G_telephone, G_email], ... la.ModularityVertexPartition); .. note:: You may need to carefully reflect how you want to weigh the importance of an individual layer. Since the :class:`~leidenalg.ModularityVertexPartition` is normalised by the number of links, you essentially weigh layers the same, independent of the number of links. This may be undesirable, in which case it may be better to use :class:`RBConfigurationVertexPartition`, which is unnormalised. Alternatively, you may specify different ``layer_weights``. Similar to the simpler function :func:`~leidenalg.find_partition`, it is a simple helper function. The function returns a membership vector, because the membership for all layers is identical. You can also control the partitions and optimisation in more detail. Perhaps it is better to use :class:`~leidenalg.CPMVertexPartition` with different resolution parameter for example for different layers of the graph. For example, using email creates a more connected structure because multiple people can be involved in a single mail, which may require a higher resolution parameter for the email graph. >>> part_telephone = la.CPMVertexPartition( ... G_telephone, resolution_parameter=0.01); >>> part_email = la.CPMVertexPartition( ... G_email, resolution_parameter=0.3); >>> diff = optimiser.optimise_partition_multiplex( ... [part_telephone, part_email]); Note that ``part_telephone`` and ``part_email`` contain exactly the same partition, in the sense that ``part_telephone.membership == part_email.membership``. The underlying graph is of course different, and hence the individual quality will also be different. Some layers may have a more important role in the partition and this can be indicated by the ``layer_weight``. Using half the weight for the email layer for example would be possible as follows: >>> diff = optimiser.optimise_partition_multiplex( ... [part_telephone, part_email], ... layer_weights=[1,0.5]); Negative links ^^^^^^^^^^^^^^ The layer weights are especially useful when negative links are present, representing for example conflict or animosity. Most methods (except CPM) only accept positive weights. In order to deal with graphs that do have negative links, a solution is to separate the graph into two layers: one layer with positive links, the other with only negative links [2]_. In general, we would like to have relatively many positive links within communities, while for negative links the opposite holds: we want many negative links between communities. We can easily do this within the multiplex layer framework by passing in a negative layer weight. For example, suppose we have a graph ``G`` with possibly negative weights. We can then separate it into a positive and negative graph as follows: .. testsetup:: import numpy as np G = ig.Graph.Erdos_Renyi(100, 0.1) G.es['weight'] = np.random.randn(G.ecount()); >>> G_pos = G.subgraph_edges(G.es.select(weight_gt = 0), delete_vertices=False); >>> G_neg = G.subgraph_edges(G.es.select(weight_lt = 0), delete_vertices=False); >>> G_neg.es['weight'] = [-w for w in G_neg.es['weight']]; We can then simply detect communities using; >>> part_pos = la.ModularityVertexPartition(G_pos, weights='weight'); >>> part_neg = la.ModularityVertexPartition(G_neg, weights='weight'); >>> diff = optimiser.optimise_partition_multiplex( ... [part_pos, part_neg], ... layer_weights=[1,-1]); Bipartite ^^^^^^^^^ For some methods it may be possible to to community detection in bipartite networks. Bipartite networks are special in the sense that they have only links between the two different classes, and no links within a class are allowed. For example, there might be products and customers, and there is a link between :math:`i` and :math:`j` if a product :math:`i` is bought by a customer :math:`j`. In this case, there are no links among products, nor among customers. One possible approach is simply project this bipartite network into the one or the other class and then detect communities. But then the correspondence between the communities in the two different projections is lost. Detecting communities in the bipartite network can therefore be useful. Setting this up requires a bit of a creative approach, which is why it is also explicitly explained here. We will explain it for the CPM method, and then show how this works the same for some related measures. In the case of CPM you would like to be able to set three different resolution parameters: one for within each class :math:`\gamma_0, \gamma_1`, and one for the links between classes, :math:`\gamma_{01}`. Then the formulation would be .. math:: Q = \sum_{ij} [A_{ij} - (\gamma_0\delta(s_i,0) + \gamma_1\delta(s_i,1)) \delta(s_i,s_j) - \gamma_{01}(1 - \delta(s_i, s_j)) ]\delta(\sigma_i, \sigma_j) where :math:`s_i` denotes the bipartite class of a node and :math:`\sigma_i` the community of the node as elsewhere in the documentation. Rewriting as a sum over communities gives a bit more insight .. math:: Q = \sum_c (e_c - \gamma_{01} 2 n_c(0) n_c(1) - \gamma_0 n^2_c(0) - \gamma_1 n^2_c(1)) where :math:`n_c(0)` is the number of nodes in community :math:`c` of class 0 (and similarly for 1) and :math:`e_c` is the number of edges within community :math:`c`. We denote by :math:`n_c = n_c(0) + n_c(1)` the total number of nodes in community :math:`c`. Note that .. math:: n_c^2 &= (n_c(0) + n_c(1))^2 \\ &= n_c(0)^2 + 2 n_c(0) n_c(1) + n_c(1)^2 We then create three different layers: (1) all nodes have ``node_size = 1`` and all relevant links; (2) only nodes of class 0 have ``node_size = 1`` and no links; (3) only nodes of class 1 have ``node_size = 1`` and no links. If we add the first with resolution parameter :math:`\gamma_{01}`, and the others with resolution parameters :math:`\gamma_{01} - \gamma_0` and :math:`\gamma_{01} - \gamma_1`, but the latter two with a layer weight of -1 while the first layer has layer weight 1, we obtain the following: .. math:: Q &= \sum_c (e_c - \gamma_{01} n_c^2) -\sum_c (- (\gamma_{01} - \gamma_0) n_c(0)^2) -\sum_c (- (\gamma_{01} - \gamma_1) n_c(0)^2) \\ &= \sum_c [e_c - \gamma_{01} 2 n_c(0) n_c(1) - \gamma_{01} n_c(0)^2 - \gamma_{01} n_c(1)^2) + ( \gamma_{01} - \gamma_0) n_c(0)^2 + ( \gamma_{01} - \gamma_1) n_c(1)^2 ] \\ &= \sum_c (e_c - \gamma_{01} 2 n_c(0) n_c(1) - \gamma_{0} n_c(0)^2 - \gamma_{1} n_c(1)^2) \\ Hence detecting communities with these three layers corresponds to detecting communities in bipartite networks. Although we worked out this example for directed network including self-loops (since it is easiest), it works out similarly for undirected networks (with or without self-loops). This only corresponds to the CPM method. However, using a little additional trick, we can also make this work for modularity. Essentially, modularity is nothing else than CPM with the ``node_size`` set to the degree, and the resolution parameter set to :math:`\gamma = \frac{1}{2m}`. In particular, in general (i.e. not specifically for bipartite graph) if ``node_sizes=G.degree()`` we then obtain .. math:: Q = \sum_{ij} A_{ij} - \gamma k_i k_j In the case of bipartite graphs something similar is obtained, but then correctly adapted (as long as the resolution parameter is also appropriately rescaled). Note that this is only possible for modularity for undirected graphs. Hence, we can also detect communities in bipartite networks using modularity by using this little trick. The definition of modularity for bipartite graphs is identical to the formulation of bipartite modularity provided in [3]_. All of this has been implemented in the constructor :func:`~leidenalg.CPMVertexPartition.Bipartite`. You can simply pass in a bipartite network with the classes appropriately defined in ``G.vs['type']`` or equivalent. This function assumes the two classes are coded by ``0`` and ``1``, and if this is not the case it will try to convert it into such categories by :func:`ig.UniqueIdGenerator`. An explicit example of this: .. testsetup:: import numpy as np G.vs['type'] = np.random.randint(0, 2, G.vcount()) >>> p_01, p_0, p_1 = la.CPMVertexPartition.Bipartite(G, ... resolution_parameter_01=0.1); >>> diff = optimiser.optimise_partition_multiplex([p_01, p_0, p_1], ... layer_weights=[1, -1, -1]); Slices to layers ---------------- The multiplex formulation as layers has two limitations: (1) each graph needs to have an identical vertex set; (2) each node is only in a single community. Ideally, one would like to relax both these requirements, so that you can work with graphs that do not need to have identical nodes and where nodes can be in different communities in different layers. For example, a person could be in one community when looking at his professional relations, but in another community looking at his personal relations. Perhaps more commonly: a person could be in one community at time 1 and in another community at time 2. Fortunately, this is also possible with this package. We call the more general formulation *slices* in contrast to the *layers* required by the earlier functions. Slices are then just different graphs, which do not need to have the same vertex set in any way. The idea is to build one big graph out of all the slices and then decompose it again in layers that correspond with slices. The key element is that some slices are coupled: for example two consecutive time windows, or simply two different slices of types of relations. Because any two slices can be coupled in theory, we represent the coupling itself again with a graph. The nodes of this *coupling graph* thus are slices, and the (possibly weighted) links in the coupling graph represent the (possibly weighted) couplings between slices. Below an example with three different time slices, where slice 1 is coupled to slice 2, which in turn is coupled to slice 3: .. image:: figures/slices.png The coupling graph thus consists of three nodes and a simple line structure: ``1 -- 2 -- 3``. We convert this into layers by putting all nodes of all slices in one big network. Each node is thus represented by a tuple ``(node, slice)`` in a certain sense. Out of this big network, we then only take those edges that are defined between nodes of the same slice, which then constitutes a single layer. Finally, we need one more layer for the couplings. In addition, for methods such as :class:`~leidenalg.CPMVertexPartition`, so-called ``node_sizes`` are required, and for them to properly function, they should be set to 0 (which is handled appropriately by the package). We thus obtain equally many layers as we have slices, and we need one more layer for representing the interslice couplings. For the example provided above, we thus obtain the following: .. image:: figures/layers_separate.png To transform slices into layers using a coupling graph, this package provides :func:`~leidenalg.layers_to_slices`. For the example above, this would function as follows. First create the coupling graph assuming we have three slices ``G_1``, ``G_2`` and ``G_3``: .. testsetup:: G_1 = ig.Graph.Erdos_Renyi(100, 0.1) G_2 = ig.Graph.Erdos_Renyi(100, 0.1) G_3 = ig.Graph.Erdos_Renyi(100, 0.1) G_1.vs['id'] = range(100) G_2.vs['id'] = range(100) G_3.vs['id'] = range(100) >>> G_coupling = ig.Graph.Formula('1 -- 2 -- 3'); >>> G_coupling.es['weight'] = 0.1; # Interslice coupling strength >>> G_coupling.vs['slice'] = [G_1, G_2, G_3] Then we convert them to layers >>> layers, interslice_layer, G_full = la.slices_to_layers(G_coupling); Now we still have to create partitions for all the layers. We can freely choose here to use the same partition types for all partitions, or to use different types for different layers. .. warning:: The interslice layer should usually be of type :class:`~leidenalg.CPMVertexPartition` with a ``resolution_parameter=0`` and ``node_sizes`` set to 0. The ``G.vs[node_size]`` is automatically set to 0 for all nodes in the interslice layer in :func:`~leidenalg.slices_to_layers`, so you can simply pass in the attribute ``node_size``. Unless you know what you are doing, simply use these settings. .. warning:: When using methods that accept a node_size argument, this should always be used. This is the case for :class:`~leidenalg.CPMVertexPartition`, :class:`~leidenalg.RBERVertexPartition`, :class:`~leidenalg.SurpriseVertexPartition` and :class:`~leidenalg.SignificanceVertexPartition`. .. testsetup:: gamma = 0.5; >>> partitions = [la.CPMVertexPartition(H, node_sizes='node_size', ... weights='weight', resolution_parameter=gamma) ... for H in layers]; >>> interslice_partition = la.CPMVertexPartition(interslice_layer, resolution_parameter=0, ... node_sizes='node_size', weights='weight'); You can then simply optimise these partitions as before using :func:`~leidenalg.Optimiser.optimise_partition_multiplex`: >>> diff = optimiser.optimise_partition_multiplex(partitions + [interslice_partition]); Temporal community detection ---------------------------- One of the most common tasks for converting slices to layers is that we have slices at different points in time. We call this temporal community detection. Because it is such a common task, we provide several helper functions to simplify the above process. Let us assume again that we have three slices ``G_1``, ``G_2`` and ``G_3`` as in the example above. The most straightforward function is :func:`~leidenalg.find_partition_temporal`: >>> membership, improvement = la.find_partition_temporal( ... [G_1, G_2, G_3], ... la.CPMVertexPartition, ... interslice_weight=0.1, ... resolution_parameter=gamma) This function only returns the membership vectors for the different time slices, rather than actual partitions. Rather than directly detecting communities, you can also obtain the actual partitions in a slightly more convenient way using :func:`~leidenalg.time_slices_to_layers`: >>> layers, interslice_layer, G_full = \ ... la.time_slices_to_layers([G_1, G_2, G_3], ... interslice_weight=0.1); >>> partitions = [la.CPMVertexPartition(H, node_sizes='node_size', ... weights='weight', ... resolution_parameter=gamma) ... for H in layers]; >>> interslice_partition = \ ... la.CPMVertexPartition(interslice_layer, resolution_parameter=0, ... node_sizes='node_size', weights='weight'); >>> diff = optimiser.optimise_partition_multiplex(partitions + [interslice_partition]); Both these functions assume that the interslice coupling is always identical for all slices. If you want more finegrained control, you will have to use the earlier explained functions. References ---------- .. [1] Mucha, P. J., Richardson, T., Macon, K., Porter, M. A., & Onnela, J.-P. (2010). Community structure in time-dependent, multiscale, and multiplex networks. Science, 328(5980), 876–8. `10.1126/science.1184819 `_ .. [2] Traag, V. A., & Bruggeman, J. (2009). Community detection in networks with positive and negative links. Physical Review E, 80(3), 036115. `10.1103/PhysRevE.80.036115 `_ .. [3] Barber, M. J. (2007). Modularity and community detection in bipartite networks. Physical Review E, 76(6), 066102. `10.1103/PhysRevE.76.066102 `_leidenalg-0.11.0/doc/source/intro.rst0000664000175000017510000001127115101156755017010 0ustar nileshnileshIntroduction ============ The :mod:`leidenalg` package facilitates community detection of networks and builds on the package :mod:`igraph`. We abbreviate the :mod:`leidenalg` package as ``la`` and the ``igraph`` package as ``ig`` in all ``Python`` code throughout this documentation. Although the options in the :mod:`leidenalg` community detection package are extensive, most people are presumably simply interested in detecting communities with a robust method that works well. This introduction explains how to do that. For those without patience (and some prior experience), if you simply want to detect communities given a graph ``G`` using modularity, you simply use .. testsetup:: G = ig.Graph.Erdos_Renyi(100, p=5./100); >>> partition = la.find_partition(G, la.ModularityVertexPartition); That's it. The result ``partition`` is in this case a :class:`~leidenalg.ModularityVertexPartition` which is derived from the :mod:`igraph` type :class:`ig.VertexClustering`, see the `documentation `_ for more details. Why then should you use this package rather than for example the Louvain algorithm :func:`community_multilevel` built into :mod:`igraph`? If you want to use modularity, and you work with a simple undirected, unweighted graph, then indeed you may use the built-in method. For anything else, the functionality is not built-in and this package is for you. Moreover, the Leiden algorithm is typically faster than the Louvain algorithm and returns partitions of a higher quality. For those less familiar with :mod:`igraph`, let us work out an example more fully. First, we need to import the relevant packages: >>> import igraph as ig >>> import leidenalg as la Let us then look at one of the most famous examples of network science: the Zachary karate club (it even has a prize named after it): >>> G = ig.Graph.Famous('Zachary') Now detecting communities with modularity is straightforward, as demonstrated earlier: >>> partition = la.find_partition(G, la.ModularityVertexPartition) You can simply plot the results as follows: >>> ig.plot(partition) # doctest: +SKIP .. image:: figures/karate_modularity.png In this case, the algorithm actually finds the optimal partition (for small graphs like these you can check this using :func:`~ig.Graph.community_optimal_modularity` in the :mod:`igraph` package), but this is generally not the case (although the algorithm should do well). Although this is the optimal partition, it does not correspond to the split in two factions that was observed for this particular network. We can uncover that split in two using a different method, :class:`~leidenalg.CPMVertexPartition`: >>> partition = la.find_partition(G, la.CPMVertexPartition, ... resolution_parameter = 0.05); >>> ig.plot(partition) # doctest: +SKIP .. image:: figures/karate_CPM.png Note that any additional ``**kwargs`` passed to :func:`~leidenalg.find_partition` is passed on to the constructor of the given ``partition_type``. In this case, we can pass the ``resolution_parameter``, but we could also pass ``weights`` or ``node_sizes``. This is the real benefit of using this package: it provides implementations for six different methods (see :ref:`Reference`), and works also on directed and weighted graphs. In addition, it also provides flexible functionality for customizing to some extent the optimisation routines (see :ref:`Advanced`). Finally, it also allows to work with more complex multiplex graphs (see :ref:`Multiplex`). The Leiden algorithm [1] extends the Louvain algorithm [2], which is widely seen as one of the best algorithms for detecting communities. However, the Louvain algorithm can lead to arbitrarily badly connected communities, whereas the Leiden algorithm guarantees communities are well-connected. In fact, it converges towards a partition in which all subsets of all communities are locally optimally assigned. Finally, the Leiden algorithm is also much faster, because it relies on a fast local move routine. The `"canonical" `_ Leiden algorithm is implemented in ``Java`` and is faster than this implementation, but less extensive. References ---------- .. [1] Traag, V.A., Waltman. L., Van Eck, N.-J. (2018). From Louvain to Leiden: guaranteeing well-connected communities. `arXiv:1810.08473 `_ .. [2] Blondel, V. D., Guillaume, J.-L., Lambiotte, R., & Lefebvre, E. (2008). Fast unfolding of communities in large networks. Journal of Statistical Mechanics: Theory and Experiment, 10008(10), 6. `10.1088/1742-5468/2008/10/P10008 `_ leidenalg-0.11.0/doc/source/install.rst0000664000175000017510000000332015101156755017317 0ustar nileshnileshInstallation ============ In short: ``pip install leidenalg``. Alternatively, use `Anaconda `_ and get the conda packages from the `conda-forge channel `_, which supports both Unix, Mac OS and Windows. For Unix like systems it is possible to install from source. For Windows this is overly complicated, and you are recommended to use the binary wheels. There are two things that are needed by this package: the igraph ``C`` core library and the python-igraph python package. For both, please see http://igraph.org. Make sure you have all necessary tools for compilation. In Ubuntu this can be installed using ``sudo apt-get install build-essential``, please refer to the documentation for your specific system. Make sure that not only ``gcc`` is installed, but also ``g++``, as the ``leidenalg`` package is programmed in ``C++``. You can check if all went well by running a variety of tests using ``python setup.py test``. There are basically two installation modes, similar to the python-igraph package itself (from which most of the ``setup.py`` comes). 1. No ``C`` core library is installed yet. The ``C`` core library of igraph that is provided within the ``leidenalg`` package is compiled. 2. A ``C`` core library is already installed. In this case, you may link dynamically to the already installed version by specifying ``--no-pkg-config``. This is probably also the version that is used by the igraph package, but you may want to double check this. In case the ``python-igraph`` package is already installed before, make sure that both use the **same versions** (at least the same minor version, which should be API compatible).leidenalg-0.11.0/doc/source/index.rst0000664000175000017510000000067115101156755016766 0ustar nileshnilesh.. la documentation master file, created by sphinx-quickstart on Fri Oct 21 11:26:44 2016. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. leidenalg documentation ===================== .. toctree:: :maxdepth: 2 install intro advanced multiplex implement Reference ========= .. toctree:: :maxdepth: 2 reference * :ref:`genindex` * :ref:`search` leidenalg-0.11.0/doc/source/implement.rst0000664000175000017510000000623115101156755017647 0ustar nileshnileshImplementation ============== If you have a cool new idea for a better method, and you want to optimise it, you can easily plug it in the current package. This section explains how the package is setup internally, and how you can extend it. Most of this concerns ``C++``, and Python only comes in when exposing the classes from ``C++``. The core of the Leiden algorithm is implemented in ``C++`` in the package ``libleidenalg``, available from https://github.com/vtraag/libleidenalg. This ``C++`` library is used as the core for the Python package, which is just an interface to the underlying ``C++`` library. New methods need to be added to the ``C++`` library first, and then exposed to the Python interface. For more information, please see the contributing guide of the ``libleidenalg`` package at https://github.com/vtraag/libleidenalg/blob/main/CONTRIBUTING.md Python ------ Exposing the method to ``python`` takes a bit more effort. There are various places in which you need to change/add things. In the following, we assume you created a new class called ``CoolVertexPartition`` in ``libleidenalg``. Please then follow the following steps: 1. Your own new VertexPartition class should add some specific methods. In particular, you need to ensure you create a method .. code-block:: c++ CoolVertexPartition* CoolVertexPartition::create(Graph* graph) { return new CoolVertexPartition(graph); } and .. code-block:: c++ CoolVertexPartition* CoolVertexPartition::create(Graph* graph, vector const& membership) { return new CoolVertexPartition(graph, membership); } These methods ensure that based on a current partition, we can create a new partition (without knowing its type). 2. In ``python_partition_interface.cpp`` some methods need to be added. In particular .. code-block:: c++ PyObject* _new_CoolVertexPartition(PyObject *self, PyObject *args, PyObject *keywds) You should be able to simply copy an existing method, and adapt it to your own needs. 3. These methods need to be exposed in ``pynterface.h``. In particular, you need to add the method you created in step (2) to ``leiden_funcs[]``. Again, you should be able to simply copy an existing line. 4. You can then finally create the Python class in ``VertexPartition.py``. The base class derives from the :class:`VertexClustering` from :mod:`igraph`, so that it is compatible with all operations in :mod:`igraph`. You should add the method as follows:: class CoolVertexPartition(MutableVertexPartition): def __init__(self, ... ): ... Again, you should be able to copy the outline for another class and adapt it to your own needs. Don't forget to change to ``docstring`` to update the documentation so that everybody knows how your new cool method works. 5. Expose your newly created ``python`` class directly in ``__init__.py`` by importing it:: from .VertexPartition import CoolVertexPartition That's it! You're done and should now be able to find communities using your new :class:`CoolVertexPartition`: >>> la.find_partition(G, la.CoolVertexPartition); # doctest: +SKIP leidenalg-0.11.0/doc/source/figures/0000775000175000017510000000000015101156755016565 5ustar nileshnileshleidenalg-0.11.0/doc/source/figures/speed.png0000664000175000017510000022211115101156755020372 0ustar nileshnileshPNG  IHDRsBIT|d pHYs.#.#x?v9tEXtSoftwarematplotlib version 2.2.2, http://matplotlib.org/ IDATx{tU79'9I+ RXmZ%`xFe: pEuG)ry@F >Tl@[ʥwJۤ#iړ&^ku@W{ B!DHt BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$HE $rqƽ'LL I! 2ظqcx{/^8&O\($D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$D@ " @ A BH!$p\_?3Z]ƆH׌ʙ3+wy@ мtilhdI4Ǒ;7^sud)CBP(w4֭Oi D@e dC 5t4/]ݷ.U1E=JN@e }gZy вz ͻ;7& T=!ϪH -HU}tLG!#r|S|[[_> =cGtn>ݟ]x~G-];պuuڻ5~mW=jmٹ(lu~(r몵%KbüZ$[`DΦx+_ݯRB[\[`|>=k}r(?nh㙹\}]}~f.kZ{mΝQشig˟oF %w% JJ@,KeDΦ|@-hE!Z|ݚk :ƴV+ܺuZ--]gV+h˵վ=,۽ g a=i[%KG? Uko^կuz =WO#r2˨] Y.H!@7lk٦E9b(47GMQhڣ}LΈ=MQhoGsS{{Ͼ]cm>ݺbEl5o`G*w @0vhmwwv_@(+ vz{u@V9sFKF@Ѕ}r'/矷{e\G״*}`8RtD&NE*ikIuutRE:~oT:Re2RHN߿ڣH??tDjkLv{-Z_-_,? asgmmQضm߶5 [_޷- [Enh]>=/꫱[PTDEE[δ}NOLߑIG*Ss:Q>fLE[XNg"~_gVTt:Rso۾ ƨ|==BnwJjRxj{;7uuP@05GOgG!5#r挨9obPxE^ E47K򨨈ȴ\#i :ƴZ_oo槞rF}ȼ`µ:>eZ=?weWIWs Ug BE6_ {ՃW@7RBP" i֭['x^/ɓ'"`(j^4\yU?Νczμ(B! ;wFakqp{㢥_ t:R_nq{TCEEG_vE ZYHM'隗.ݧ@j J؎GUQpa8/ !M9X(c^^[svm2Rl"HUWE*[HeUU}=#U]toUuȭ[[/\ć+mKM 5K㭯|5,vP 9PB'ٳiU^{]m½IEp=p"n :E@oLfоDֆ#λڶo鑣ڶ_0/.e' 2=0$v4D'Lwhu\_Vu襲U]`njmH M)J@e d) QhlM߸)r6E~sM۴96}~cm7l(w^jH1#=zL[{̮1Qhh>o[ˁg$SE M͑۴+^*)c½]ߘv]5*Rtj^VZ FވO,jm@BOW"poH FS7}kk.;gNL^";~隑m?x?ow H;BVuu S#GF**Wg9sv~okij `8 EKƖ+qKrm'OL^[sv9½Լtm-VPB>UR#N>i'O=U@Ap[aΦ88|//=~|'tm{{H㊾qd$XeDEDne\1_iUkK@0oF3Ѳ>.4WvΜ]B'Ba,iS,[-Ds}[({ro_8xϢx+_%K;7^sp3!0[-\,Z룥Ƚz]_m{7=y|mOhk#5f91{>+Z]9**gΈ9S("n]4/o룹>kו~*[W'`+ {ho&=oP""cb?`( {ս oTҺ5߰ # (BW^e3Ѳm`7KVCzĨ#vmΝ+8 Y!ܪ~^P[%!}EeHOT*. Ύ b5W^ !*ʶ3շ<mJVCmoٻQY7+2&u;>;gN.\/o!auU.\9sjJE@ \ pWP2o{Tήk_8+*"3aBqI1EWKt;.;wnj L@h]"&gcGjL=,*gmZ9.*gE?;gNLh;gGa{Gʙ3f|g BN --Šh&h]< ;wᇷr֬͊q%yvN0 D+47GϷo*hY|!w}Eଶ3g͌1y># hm[[66Dfd֖-m;碥 pY}<\DKTtTyd{ؾM̙54C@ҥʫڒ%Gv{Ց3Osv숖y.-gi >uZ:ٺmBf55y>tC@ՎG_-=, Ύڅ c']-<.h}aED.7VQӧٺQ1ݑ14~eӼti;bEد-qG4/guŋKeeT]Q9}u`]TuTK|OBl\yU]Y6>**fGnV[wE4A ʢyٲ.,ꪨ| pvTE廦Gܕex]e{v:*gl?3mЊw)  @Y<$ID嬙EۄVyd2<!PƆ35jTT͊Y";{v[x@؃(Ԉ7A&7ڶ UGtz@J@\7"ʚΏT$_J;E~gT$(Bo56`ܹ Yl1 |cclb}dU1H(!0ZWM0Z{~'څ #;g eQ`xX_V̘g8&;wnLgQ8, B`v{:v>cJh;gGa{Gʙ3f|g*eKlhzVWA^5>+[W'A& LMDV̔)Q{-5D8El8^?@L|A(+RhiF4~{cG_zIdJP!r?Ϳ]RFAv}8UtG@쓦?,]zW1}zzKT9D=q!/B!Ǐb95>}AB cGlҗg?y`:c|EHR)!'k /ǥkk7G /Qe@^\863QزqGω[=*=D B[B!xSl:^Ew `R~۶x󳟋=l6}yy) /B`/-+V濿0Z_zqc)Qe(Pd/ k8}b# c!nu쨋>ctE*_ 8͛O}&zq߉,Qe@@5/]/(rGnʣ*Qe`p!$X]wņ9p/OI_ V@bU?IS}cD*`8@^#6}hyǥƍn`*JA@ ;6ӑ߸q3fDmDԩ% ({@ ~˭q^ń0e! sr?Ŏy`EEȏ~$RTiJN@XʕFs8.=iR=)Qe@b>O^qŤ~)0Scw?(lؑHL:l1 H~˖|e=6j>|Ni  ! -'6}[J2SDmDv֬U %a gk8XR8 f! --5߈ouK/ї>RL **!r?Ϳ]RFAv}8U eB85aIlȯ]㸊ӣ[i% ABGV~zL|> P B8@v-W|)~w+bEE**MqC@5kb'>-MO=TMMDYg2`8@5/]/(rGnʣ*Qep B( sv`_A`@XA%Vhj-Ws4'=Lb埏ї^J(EO8.5nlpCTKTB(߱Sƍ=1#jo%*N-Qe@د YP/51o^L^ 0h Aol-bҤIq!1.o6 IDAT477ʕ+_kFCCC477GMMM;6MGuTd}>WuEDɓ3f s-eKlhzVWA^5>4A@8<0~.7.N=ԸKvxdk=XW,[,^~hmmql6ЇϏ?~jii;#~+嘩S9džu0 쿖gǦ /{KfʔΚUbthhh}sqeuFDlٲ%83',a~{} /F2|衇/~״jժ8sn`Dիn{z~?H{~RL *^+qlo~veee|_ۿ-:k_+hqI& hMk׮j?k9CWURnG>t:>ı3f̈+VM7=PQy_W{/cҥ}'Nk6N8ᄢ//}KaÆc9&Xn]x{/^8&O_s[>6_hz5*cD?Y`l͚5qꩧFKKKGM7'tRwGBs=7 z}qꩧ/ѾK3L|뮻..\NO{/|=nܸXhQor5kώz{^~3!ؚ$6_tQ׮q\Q{-QyU;gٍ7XΛ7p0":oj-5k j}dɒp0NǼyz`D[gWUr/vFDL2%/]]3p+ ?~p`Cv^;#BxZhQQmoge2x_Է癁]YdIɓ'Ǚg:묢U}JgcGl/}9b_K:cGzԨG2zꩧbǎc9&MJ+}:sg7nz_ud2^d j]&6h=KƄ;/x*,. /^\~{{;ֈX|ylܸ1&L05Ճ>Ї>4|׊ڽ?Oĭ^xq\q}wF˳#隑Q9sFTkzlweKWΙZVXQ>|oMMML>=/_^4_ۋuYt`_{}{ܼy{lQ{lsCSҥʫ,5 Ǹ_.`b^~ԩSu)S/~״/^zx駋s{۷g?غukG_]]]/G.h?>FlQFArjժ 9;},6;pOoA pLl[:mW_sr!ErVw]|&GGy͟brxꩧ⮻ 6t\7n\|[qΫ#o^zuL>CCҥ#v6cLmV1bDkڢjmm￿oW?o~}{QG6mZ\mѶ:Cʫ9̾}QȔi[g! ,vUUU]9K׿ulܸ]SSvڠ?㏏ . N>H{)Pxh^l߷1_p8` ˤs`5a9Kzj9rПdɒd2qI':f~fQ9700kG1GP5% "RTIHׯ'|o5Ǟ_KKK[O<#rJ|ת}ee;w{ uFkkkG{ڴiqFQFGǹ/B\~GD#<۷onF;;Ro`o|C|(Ơ V-ZTՃ}1}я~SNo~?񏻽g8IV{B@X&Wرgm޼=z?ĪU:ڕqYgƍ/| E}~{ .w~7]_lڴ=f̘~ 3g~rAcǎ-{75kV>>c%{~D>uݺu /t9vՆzs P%' ,wEիWW_}=mڴ/o?pQ_SEEEvaE}JcwD&hoڴ)ob۷o7|dJ+[Wxfjv pE# ,ӧ?xwH]ycǎɓ' 'PgwVQQQnnnr\6)S},jO:5lq_ZDuUn\=8֎3b„ V[O:o/:o޼yvڢvOۜ<OД3'j.k_] FvΜ- ` NO?/R/~Q>餓⋱tҎv*>$lŊ~<䓋w_r^q.4be:.;wnLgQ84L21bDr)E}v[\2}юvEEEq^_W:|{߻֝r 7gΜ|p;x^v^_WXn]Gc2`1"F~ćk 0lK.ʎ=կW\qEt}qaw]E~֖9~ϳ;V^ݯ{ B\#=ޗdK.)kW_W_}5o?cՁu.?1_ٺ0Ȥe6eʔ .O~E/R|#~oܸq%͛7wǎsvi|&~_Ɩ-[?x,X ];c޼y>3ό9{ڲeK̟??zꩽ>1x뭷:98UabYE /_|1/^mA5\7|s̘1#Fk֬˗GP踯2n馘4iRI\hQQ3Έ5{x""CSƘ1c*bbŊعs^7Tgsύ_=""6l~xyQ(_ke㡇7xc8rB aC@&믿>x;7mO>d?>8JRuZeۋv_zd ..,jjj>"ᡵ3P3B`Jܳ!cזׯ;vĄ Cc=6l|>V\J]6oQSSFɓ'nj3e3<VGD*?)Kds7U)1 K :EEEkٺ)w.!U'?gNb-[_b'Pul6jGH :3kGL6r7Pur EdzuiPBN>al]]}BJaÆϟ_& {ՈBhV@T!U%iyѨP BJx-fJ@@@U' ץ@*IKf?T !U%8xBFaӦ75YbBFnnSDssњ%Fj! j2Ȏn3%" jmDTĀ@P5l}]mkh,:OP 7Gh-[7:nJG@@UϛeAT!U!ifC@@UH k =mT+E-/ T!U!i6BB^@B@!ܫ j# Ϗؼh>nJK@@54&ֲukPe;fJL@@%Z^>B*^R@Xk!PTBss)Zץ @g ,ذh@5P ˋFDdGۃ>B*Z5DM6 hIam}}tB*ZR@hAZ hƢ44TBy3ZP/EkBZ XZRB*V.ayL~Q3tB*Vxntd2t$T|㜢h>F:!P($fUL@@Ej^, k *& "ktT|޽fmPr EdzuudRPr 3֍NNF@@) kh,Zץ @# 4/_Uֲ)wй8E#A $?#j JNF@@I k"ɤ @'# glSPqr E? 0+VDEkBl3'V+ x[/_w<//\6gl'>cXRW2i^*Yi?k+g BjhѢ8衇b%*Ǻo?wBn1"Sc<@1[?xie9G #/J+K>+ym, }B(!tU?q[gV:G^\_ifl5ݲ5q}qR謴T~V~J4 *J{~m""67͌%*i^*Yi?k+gc!qpRl?}֙k#}K&6G|[Qύ$ E Ii{9p冤;%ڿ{Lz6]s+Yi?k+Ə~m( !:΂ G%[O#+?{*į: ַmT5kO+u @ɦRl#!#`DD.6 W(B*FxK6,^MRT@P1ffGLmm]n N;#) @qc{w][e26#bIN|bBҹobIw}[zv}oӹ*Yi?k}HH8.6{[9w=mǎ:毊Oo}?mT~9u#Z ]b]ss!~hv_?~d #"71v]K>+ym,Tuyᢢ%F̎t햭 ٣;\.7?,f/ZS޳[m\vxeձnS.zvC^*|)K>+ym, =B(!_J[w;o+AGKPO}:s:!>"PA@E54 nzխ:^8UB@@E76D̘<{g66ŎRP G~l}}t.KVmKn; =5qɤP*B^nZ@ۜkKnT5[SN{uM3\QSّ#R3/sW$ֿ|^ǰ~)vtB^xjV P9?nG 52oxOAn#ċ VŔ;M=_|=RLYUhMq3bch}@1y҄OV>%AX+ L_T_h&9yԯ{ʝVظ1eR~YY|pxg;:#!e-7)2zC/,_>Jbw 7+ L&F @lm||xd2)vtVBZÆE=ʷnc..aFݘ+Z-SN=eS @5( ;WI<'Q;J+Pg *p,L]=P\Ccl}]m^JoⳇbG@P 7Gh-[7efI IDAT:nҳx冸3#\(Z6G|s>Q[I3([\h@ڔkon*ZɧL~=P.#"jGATB<۴2qCPn\㜢5CGMϞ)w>/n}|nbNbG@9Pr E-/ T毌 GbG@Pק@[vS\tؔk.Zػk|wҾ%=|@'f$\n~*P^[Nrg@P |^}hM@T=reFH# (K#6o.ZůnH9~hLzߨ;*khLeF@i\&uӉ1{+2L]@@@Y5QS.Qӻwsqэ3b|zީEٔ;*ol,:nA gqs1q1g]D@@Y%om[X};*kSt B=ʲ_JI# (;)fS},\>.h.#1qɤPq7eR=lܜKn+m.Z֥&_%J$ Ȏ!P~xnު;bG@%Pvr aQӯ_۟h۟hJOz8za)vT:!e') Klӊ%'5.Ŏj g@YvS\r؜/ܧ[\~򾑭h_>m$ ץvόE+7d⻓tK3(+B! Ekf7X{>PME!Pxva1^C@PV(5,Y߹!}+L&Ůj# $fk\\tÌX)_޷G62!wM3(+9eP(ķn{:,][D|s>1|`ϔ;oSt\@tf=y~qbCwbG@5PVcN{9~]bG@P6 (+ VT4G _?q|dm jBFҥQXhV@t26fuֻw:!rg@P6fFDdjm Nj V%1\CcLQN6=wMX?w M#P6[09<3wE s?{& l$-1jyQXfc\|ӌ E9yzO&(Iam]]}7ǥKVm,Zfb Sn)wКP( 4 菱޸<~1{;FO#+jUZ B{fAos0,N|׈;H& ,5{gqt_.>(G3vxNyŊh^h B M6fĪ]k{gS ` rs$@Z B|⥅ϹwD)vvB:\ѽ[ j/@q.{Hl!^xvOzuy/$Y?0>|ll?%RlƸ䦙o.=s򾑭(>',1* :Z._XzczLL>eB 5_ Pܹs^ŋڵkcРA1lذoK.n hjjYfł bQ[[C>}-u@ʒ b̘<~{^v2WUL>hq1>;찘7o^O)S$֗-[Ї>5m{lܸqm:M#Fء{mӼzu4/]ZV+ :нO͏jb52ŎڇXvm\ve?aX"~}SL:(_nk㗿emWP'|2|ob* IewOpu|g{g{v|>/m80<8cL&R[ti|}zvi1{v< ]FСTU77NOݺԦ@0+/Kq.]⢋.I&E׮][g͚^zi6ms9'4hPtG.kuoEئkbĉq衇n*x}/~*.[,>]wݵՙqW!C|`%?8jTdj}8B|cSLķO7rgG@Xbs΍뮻؏~8#8wv_WO%$\bE\}o}] lu}Z-^ÞFuѼpQњ%F~S.rX=ƔS&DnV*zVy{w]C{..M/}ݱnݺޡϬmuZʐoXP(;ً$G.S c K_nu9/D^vwm6T%6%7͈|sh}H5m{P,1Z"WnuܣGٳg1pVk$Qr\q:z~x}?Ƨ>yֺu׿utI%]U943^[hk&:!rg0D֮][nmGݷz4cz' ;O]Xq{@%nݺVixsL-Źp8sb-c{| _{|_I&N;T{_ꫯk6 ח[dIwy1u6/g ]aϟ_& =3X?]#|le_Rn'p*uHxx[uV38#/^2ַo߸ꪫ}ىu-.¸Z?q]wm{vMV@l)w阿|}zmM&s1_uJSaDDP͛7w'CP75k֔D{nu~6!Y}ᾶ?-]t|#M6yZƺv\sM}kgm>k91k֬Ԟ*c,[7:FΊZX?烻;@iu@xGFmmmDk?Bgώ/<:蠸K駟.eE 0U`6c޼yRܓonu|衇n~*VLv%~ǁn5551tVc嶤-\㜢Ym y%~;iP]: ꪫ??B!"^ B_>N&M'7߼]{u]wݵ9? Ox̘1;ӶXfMl5֞ˋ bڴi-cl6-LK۞%a% G겵[(qɇn@(0"bw1mڴO~rHˇw}w4)yS[U>ayd";rd熿͉YX{P*2 |Cmmm}_2g>kUz0kŵ^Gydy:#:Vc?Zl_1o=7FÇַ}o|Xti'?i5v衇ګ:XÆE-{ Xn;bGOEo6jԨW=P|{ߋoGs9';O~ZuyE.]Zo喘6mZ7n/ղ'NQFm9ƍkks7ooX{xSL믿e]z J<mfɒ%qYgҥK[ƺtguVkh(: WnK?3ſ5}}V5vGw]tM1lذx=$| ,*>W՘7o^3r'>j _B7M6={v|Sӧ?=xVi~#ܡ{^uU_'>>MMMmv,Y$>8kƤ #b͚5&N8xgZ>v1 l] B!`S9.iF,_h[&:!rgO ٳw]qzV*ܴiSr-qwƹgyf1k֬x衇"zb^zܹski.]kACgԩ?֭{뮋뮻dDy`MMMr!ѻwӟ6mjoڴ)~+SLijkkG?Q\zqw/[,~N1eʔx׻ծ$YhQ<#J?ZzugϞqǤIR (lMEkٺ)wϋ[Xq)vйU|@8gΜ+VDĖ`cĉqiň#""bʕ1uԸb-3 o8##h>{?㨣_1cƌ?98cr-[{s=S{3fL|ssņ 8餓ONw .o,[_r7@g_w=X?|=R޼feq 7=X -?~|~qF׮ټys?keXRtű~ycذa'ʶinnƘ;wn,Z(VZ7nݻG߾}cРA1~ -Z|=P <Þ `ß>ɢ^=RVb_ػkbS sϏo1N˖-|#ڵk{},ƏҥK{x馛""g1r9rd?Įk[5):^3dpho.e7?dQaP?q7#<ͭf qΈ#N'F-Kr Edzuitr?{+9.D8@1e^s5q7… #bقL&:8n Ǜ}lnn(.Xt<[Wj@7$֏?4&oTU'intԴ}غ|C@DD5[N;.^;0JW {O;.vڮٳgxzOZ+r;hM@ݘn6{wƔS&DC']tc=6>>a0`@L<@D~͛jP B\~3Ѹdm98i|ܩW]/G? (u+#"ukt~hc<ܢ`L8nP8R@;54e;fNWRbcw?dLϟ!CfcѢ}#}ذa;mOAK Yb}y IDAT\\(^>G|c>QSI12VaL&2L-\08∈d2sϵgl%Fqs>.iFXW|on]jbʩo.)wP: (N^rs :]y ܼU?w7Ŏ*ىd2) DSDSb}{GX`{}@h@e/Xqcњ=<۴"C5.Ŏ*KaӦM-?w֭T|CCb-;zt|䦙9_K`;OZocOV"_WݻT\cc_tJ"o~?3P^[2!v ];B@~{Dȑ#K @uJ ?7XbQR2eK۹۶{7 6m/G}4f̘2>Gv\R@BUxمG;,>Q5P:}@xEE&9B!kQ(k;nK wO u Kwn{&>vHq{{BMo{żCSO=5Əc BֲumH nSholL9eBtZrg, rߑ#GƧ?8;9l]EQذhP B|붧cҵEL7&L3ɓ'/ q%D3?s d2ѵkӧO3& nvIˋF]HCʼn3>[.)vP:}@x'&.䒖eB>hA@J 3E6~(/^;vOkTR7:jQғK.>t,X>vSќ6nGMM N?pkMK ;B@c|\tÌXnsz.S'D]R zu@8|R@;',1* R(?</.XxW?w'ŮO/1 @y+ { xS5}^bFM#$ .ڵEk)wtg殈|b}bGK@@I%ץЁ4#rB.}w&ZoMi <[g2zN{(:NxO80fv7ǥKVm,Zfb)bR zuڀp޼ydPxLmiŞ@oeA'C澗bz#1i7lK^ gIZb4[7:Fvw3 wXЄaqһG8 6lX@kh(:^[Wn#@hu|g1l@; BJ&ИXByï/.Iuxn;oi};k֬QFEϞ=Kmkl,:3j J`%qg%cP|; ?7񍈈f LR@L&n3|]|}SQ(کg|QSVˣ>?~|߿VIaElؔn{t_^o=*JAاOd21dȐw%AK ` ]K W's AS )ۀp]viy%@yq_\Icǎm=WˋPzuy/$Y?0>blb(ۀ>ƍB!^|XhQ[ B9Xzc\r7>-۷??}anQ3dpm7W?3Xޥ6O7rgll'|rL6-|[>8R654ώ T毊;lurbXfSg=eFD?.(ᄌ+{qԭ9E-/ V~>iZ8]#;+vTY_|qDD;zk׮׿5z{G 80zd2~Z',1OG^\_ifl5o5{q[o52Lq&Bk֬'xM+ B$Ah!V9CD.6yd2-蜚WKjPr?6~}@X()`DD.>-0e|箈jhoeiJ!Xеkj/@kw<9oɦc^ ÇC2Q"Sk2(ޡ_xe('Xtn#vwz:T$Aǰ#sG$f1{XjU^: B?:3l6jO 3>\6_-[Gv@{^믿>xWw^B֎lg ڞ4sBnٚ|Ҿy@*?Oq%ڵk<[ L& Bd2v7K ui WB<=wEyG C8PF> ;+_J`a[omo@^i(:nA|/{'⥅cݦ\욍݇aA2Tܹsk_Z̿Bƍ~ѭ[~'O5kŋcOFsssd2iώ^zT| B!tZ{ { ۫mN: /~6lh x9L&͛FDx≭3gN|ߏ?^{bРAjRX>  QSWsssq-G{6!8z| 5kV|s͛7wdU-꫉Z!@*6 |cڵ-{s9u??8( u]מm&E&#G @*ۀ^:thn[= .o}` Ƣ#GDktRe\cnQR7nL׮cƌB /_' @z6 \vm}ݢޣG{Ӈӳgޱ(*8踀 =e9r[{xѢE[_Ϟ=[~^tv@1I{ wQv?;Y! ! I䎉 ;AD6* n+Z_ZZDhQl"Ȏ#)KLHd!d!\3ɽI&3ܹ^y>{ [~W_i~РAvfa5+V(aÆ>7GiҊs9!@f6 loo/hQ-MVeյ6l=XҖNq∮sB4 N\.4%Kƍw裏4M;U:Wu>Xo;:+O$I2m@8|8#Nq$Ilڴ)>׭[W^ye|ߍ$I""bȑqGd K#2$bXƱ?xDDw}q'v_'tR{q$I,Z(>СCc}UVETWB$IϢ5U¼2UL4~Ǻuvooc̘1J'aaÆX|yr0q衇?>@;SW~㦛nhi98qb|ߏ|3OǷiFqq׿u݃jal hruFD]:ujG?qƢEbڵ1rx^~zoΠZnK*b [u\.~z~.锖,Ù;d >T^4""7Y[8 ZͶ&' U ̷,jȞ~WA( \T|&M$(-xqŹ|{{0`$I2}gѕ{.b֊s:gQU”  "pa+}ԷbGgqel`O>dKtuԄ-FWŎ !ع⸀6TaN@PBM"l8o/dZ oJUl1 PBMxGˈ@DDk]@o|g/4jAک/$I|]!@ߨ5S6iz$I"M~ UBe6>LӴOBF(M(utVd[ eu^ve=T*ڵkg~8-[Iȑ#ӟt>T)@zH7m8 Bک~^?[,o+"֮]?kcaͫBZju}o̚5+  ,O~uZ*'FF~e\ 4m@͑G#M~fͪuI ZAj0" 4(""4bGg|{{Ѝ0" vXi?|uIu`@?/ZT,LP*?XԿ+#]\N@PS?_Կj݃BVeP0"xgcǎa5ɾFW> \lY|$IcG}t +]?d/_k׮ 7pC_>4$I oxC7eԵj[ S-u@xAzm`DD>ͮZ@+2u@iO$$4M#e]vXUМ4b碊s`GuaoβJ44c9&nx;ه4^tڊsB |g|>1z8CMozSL<hNΪsy[\]s̩u ɰa2vlbZ@/z-4}C@@*vtT^`@Чv,*bGgBA@@zHW8' EUCTsAIO]tQ5k׿ h]EʕrBn[#"⠃Yf2ĉcqGqw-_4j݃BVeuΚ52dH2dH|_._Ϟ=*h>ŎExU… '$IP(k~Q("Mx'c…}X)@s(U;pHv8کۀg)x[o+\yq뭷F$$I|1y.;:*O4)bإ #":4$I"My{g?.|0=W~~ۚLsmm.ھoquוCG}4>ȑ#CBODDb͚51"}{_-oWB :@}꜀`iؔ,^ZU\.rNʴv! vsNdРl`JSn1MT5kDDȑ##ո"RŨ``jpҥq-#<Ϗkv1bDz׿>wŁXJC3S+V;"""Mӝ[fM<5\x;ƍ˺dnK+RCA_":문뮻T*$I*x%<,Jq]wŻZ~T\8?ƈȷ2=Sc='cժUi0M5jTE[[[5<{W^w^<5Yy"I"?yrgzэ7y6m$I)g}vL>=F5kļyo.oG$Ilڴ)?뮻bС5DTܤI m1쑺 ={vXN2%n7'pN`Dȑ#㏏*nᆘ]ˊ+b~V?0`u@N4)8C=44iRyX1@cV .{Ř1czΘ1cK.)K /Ă \T8oq%쩺 |Ǐ{8O=Tjh-QZ-F WZI^^Ғ]]W7n,<|^ۯ @eM(@UQ?Xmȑ#{@p@ m1챺 njiƣ>6l6l>hzرU @m@׿>ZZZ"IزeK\{{֬YbGuT IDAT_ а,6 9rdqJƽuwID$qw۾J: Lg.7>O?H$n^xa|O~>wݘ5kVJH4$Uk/'|2/_ׯqĉcZMdɒX`A}֭[7M<|>|x7.:8](b1W luo;.zH$b\{qӧOBo$I֭Θ7o^iӦr0$I{q3y1{7o^QF駟\pA=8cҥ}ֻ˫οq=ǯ~Xzu{$ӧ?hi&UVW_?Os8N=Խ~4sElZqN@0%i.7֯_'$IbIdm_׾6>|x\K.{t1c/mRO_}{+?sOW\qEL2>/|!^|=3όK/4 wUlٲ8v衇b}O|0^|ߟUR?Kؽ=pÇǜ9sSO JXWDtSN9s,,J|fppqirHsʕO}*~_g]nn/7ĩvZy;m:o޼?Xpaկ~w^p0I8CN~u{n.(z.hTŎΊ- bt}7oƽfmh-$<ptIYY׾x׭qǹ */X /ݲeKwyqmŸq믿>bk-_O4)7ѳB!fΜ'pBL6m.իWǵ^^_|'>~{ T W^K/Ӻ8zv=sn׿>?F.(88!>cnc͚5ӧٳw  bĉqyg͚y{;PKa!:蹺?}󟏛o|=cƌvLGGGyζ|>wygk#uKKKw}1a„,Y$<+֩8mڴݞgqF9Xlmmo= 4.я~Tޓߣq!n'Fgvž_Sua=۴iSu]/rϵw0,qmy}=5wno}[wF^-xeov)p0"h;36o޼BIK(.ZTq.`ظqczە3ftZ~}'?6v9b}6|]>wwXԩS#(_oذ!~axJ/eKŹ|{!Z9a =Cݮ7G}tYqQGvI\~ʔ `k:#aÆk^]=묳޷vXzucV浯}m 6l7}n ,g;;+m1X~NwU$Is=})=ikk'x|px'… c޼ys{шƱcPɓ'};/ ;:*?Pt@tH$4IdիW7a„1qnםUzۍ7#iӦbΜ9N>]=ݮ{]^:֬Y#G:uua@deD[СC{e+m_u]=U,[o6݃gώz|.Yvm߿G>|x <86o\[nT- ̷,TaD]֯_z=^cȐ!\3 <@\|=lذ83}=X|6~0w܆ ]!Ct k}C]]Q\Tq._6F]iFkkkp 1s8k]RXE@Yq{O?=/z΋[^ą^gZ|Pk]˖EiS9gԇ$bw}w}1mڴ9sf]iz7ۨfj5˗/gk{ѵkG?X|yylĈqWUط7j}@Pm{шn:yZj]|_ӧG~=3qWqw^w}Q*j]nx*93==ðn(SQGٰaC|c~<6lذۭE{ov|&bEQeԨ`o s9'9o1nXbEyX,}w_qYgŌ3bԩ5z VΝ?7oԧb޼y!Cw>}, :,t6B!>ƃ>wxGrrWʕ+kw'~/\ʫg}]oܸgڭZԯ.(hmm:O߱e˖8EylРAq5ěww;ׯ) 1bDրFP8AQ6---qǷx衇s\կvOcŗx㗿e*n#Gv{{ҥK]2 o '_,,78c{ޔ)S]?s=z~G4RGGq!@p{G|#qm~8s]yiF+]yzk|N:)CUzUE*U͒%K]g/wyg^T*E]{oy,78jg}G/^xA3HӴBQ?K?y\~7뮻c\.9N9唽^w橧v7.׃fеbEUB FC <8>Oc„ 4M+~_g?xkXmn?׿bX>c̘1}Vۮ츽3gBFWWW| _o<]vYqZ{ܸq׾|],Gw9zUԣj݃BVeK n->}ovZ 4($uie{l 2|=o޼Xp='ܧU`xGI̙3{nwwq7w[+_JuY^?"xGni[pa똄zR8om1쵆 I$ C .4t8Sn舻ᄏ|3*ٱ{c^{饗nc_l3<?OsQ{ri.j?`6 |;Nr)| nشiSn)ުo޼9/t;?q̙1y]絯}m_կz\֭[[n6__}K.$z 8[n/86o\{[akkk|ӟӺ^Tb4Au}i˖-q=ܹs㗿etuuEiii-1s8kTşǵ^[ /s=7 T_pam̛7<6jԨ/VZU9rdr)ZꫯYfu?8cɒ%=ZkÇ . XfMD>WSN-߷e˖+G>4iRF! h Ο??ΝwqG]6"^9n-Ƃ ⡇W:۾Ŀ˿ÇŋO<-lmmk&ƍIsv}gzfΜ91gΜueŌ3vyַ//]oOOCXn]<ЈN8!. M(v.8ԕ WZzk̝;7,X hSN9%fΜsL-ݥ\.W]uU|_;w3 s,!+]/ƎLkw* x nxT*EDn4M#3ggO-KcÇ+2N=Ԙ5kVxߨQO . FY}7tS8C⠃}o{~qWO~;:##HzWKUl1 P_tC ܹs[o_|1"v3fLyq9t;K^mRtq3fLL81:n{[l7sʕ+cС1~8蠃_޹lٲ8v衇bN 7΍.ƓabOvm@wp qM7c=;f\.;9sf c[[[St7hРxӛT2`@+vvV A:3/}Kݺ:uj̘1#>kT@l/ Pwt@8b̙qGW^5jT PϪv=B赺 b}qw{$'x_Po:N]ηqQ_K/EZ[;_q5V]l `gE ?: 8qbKhzL -^}W^s)d[ ovإjE꓀]*UbT@PR3I@@U]kF׋/V ꒀU˿=J+B*vtV<8r&dZ }C@@UjNI# @UjaaJgTU8+-># b碊y!@PQѵ|yŹ|{!Z;B**UAPTTb3B*l/ PTT8o/dY}L@@Ej[ kBvn矯8' oBvR\꜀ IEI@jaI m1)!;)vTbO@NJU:@j[ BI7oҥrB' iZq.^ȴn'$mm@MxnҤHθnhB A@@7U;ۧd[B@@YeK/8gQ diDWWŹ|{{U^4""?yrvoU s&D2th/U ?0;:* @PV8A0DDDZ,FisB!  ""JKFC@@DD;; S+~%  "?2~\ m1!QE@@DDMA@@DD;U4!JQ\T9 (=|Ė-Lk bGgչ)@Ί--#Fd[ J@@y4!U\{{좃F@Ү(u.8o/dZ O@亖-tӦs h<B&Wm{!@#4bGgdԨh5*bwB&WI@u^1 \Z! Xշ4$!@Z" *3,X\{!2Ȑ;:+'#FD~e[ 4RD$@&MQq<_(d[4b碊B% hRiF9!@4U"]\i-dG@ФUl1 иMTe{dh;6bȌIU;0_(D$@fMZ@(@C4, cB&U8AM륗"]\0%jȒ U^4B!@4ja2dHm1dJ@Є* S"iFoPx=BȜ l1A' hB 4>!@Z6^|✀ LqѢsBfuPB&S}G#7aB9!@)U;pHZo`&Sd[5! h2\iԆ;U t_]˗W˿=j!@)UA,M34ja-|>b !@)vtT^iHBePCB&R8A4MkzᅊsB! hE IT;0rȵi-ԎIT smFښm1ԌI;*o1j{Q" h*B" hŎB" hMQzs9!@S4iZq.^ȴjK@'$mm@m @xnҤHζjJ@U|{{PsB&P5 ,LɶjN@B!:=!@Kl%Lk diDWWŹB# hpŎsɓ3@@ઞ?8aB$Cf[ 5' hpUBۋ4%!@ Y! hpŎΊyMI@b1JW˷g\ .(+ hNBV\aJv0`X[Əaò-A@m/ м Թ⸀y X3K@РR)>[q.' hZBUz-[* e IDAT! hPŎΪsh^BUr`˘1Ѳ! hP*A& hPœ Tx0%JH (ELk``4El\qM@Ѐ?! hvBT- LFQ-E@Ѐl `4bGg|{!2 TMO@`4Ũ L׊nPqN@;: $ h0նMF˶!@)vtVD$# h0* B@p* 44MAko϶$!@Z*u* $ h ŎΪs B@PJUM1c-I@@B$Iq5 DBRA( h |{!2 j@?׺*[xq<䓱|X~}7.&NӧOZ @]/5)W@% `Θ={v̛7QO . Foux≱t>Y~w\~{te<ϟϏիWw穧Zz?#9昽~S|{{v0 %\?wyի??/rHz^zius<@3gQRKKKazhzqNJ+⤓NګZv'ǁ/kC-B' o۱u3* 2$.83͝;7>яF[[[׻+g-]Ԫ}Wu5 &<7WYY6*# tM6ѣ:tت+V^]9@}յѣGc:~=C\x>B7ڰaU+pxl޽e4wV[Z vv LIoJ^Ѝ>l޽cu%yTu{#FM˛Xs4cǎYcbbXGՕW^Y/9ѣڵkյưh~~Mݻw+--M Rhh:u޽{kȐ!tw@LXh$''G999VZ; `6mIvdvug?v%))IV믿ÇkjѢ*jf? uiǾsnrv@@zFxxU;??u9d2iʕVAGiٲe1bmrLIm^7`X~y ?N>minpyV||Ν;m۶ VQQ222k.XB1k׮n@6b]Z IaaU>ªtۋ:TAAA.QcǎՠAԶm[;whĉZhf͚2Ig߇zHVR@@+˔d[bQ1)##C7nۋ}v nIzz>Æ,p>>2T9B7z`qqϨ:3 /d;t蠞={6p@qV>S7Ufed8T݆g; \tUWoZ3--M+  l/ MEEEN!en֬YrەT)ш#\6+jJ:uv!7UTgwa\k 4 nkNrVXZdUZh]-**ʪ]5ɔd1.֕e B7j߾UNOIIjwС59"??_յ9Vl 4"ټgqq5ƀЍ.VcǎRWM[YڑׯKvlvXX*F @@Fjoݺ۷od/REDD[mSu{[nE%sljM?(Am]Z q t~Ym]k.=zԡ˗/j4^kȑ#ڽ{QFdnwٰarss-mѨ^z"?hn+/h( @C {8.11QWF^Ru`>}풹ݡP߬]q vSE5{!ۋ! tGyD>V,[Lk׮ۿDSNUYYڨQԮ]өS'_[lqֲ2Xѣ~;dee/lvxL~~{1>|?\f(!B7]weuRG_ڵr-44T'NtI֭SVV˳333RWZZZ(33 ׿UC ќ9stA[RR/R#GԆ 5J{ۋꑽ3 ]ɓ'ȑ#0L/K/UPPuUTTXwQV\RҥKÇ_<{ҤIںuC}hW\EwlrrΝs*00P;vTxxURRL߿Z8+I^{^z%j\X'm3 Ao}Y}6nhsL-4sLfKOOצM5E),,ݻk㣇~X  *cJN*@eƸ8Wh,=DPP|M 2D|CGUxx[l}]vU.]\6]Gնm۴g8u>|n6iU1%&پ-ct[h<**,?[R4##CEEEP6mԳgOFB)))JJJRzzT\\,(<<\ݺuSdddՐTaÆMK+uP1`Vtthx$l^7r <]vL6cc\\ 1! Fʔtu+A@PEi))6b]Z q! FȔ"ۼ! FȜd];ht2  m+ Bhl/ !4BBc\+4Bl^7PBhd*L&m# ԄsJd2ټG@ !42$Cl 4J #[;0еBhdLI6(Șۼnsq%ƈ{[BhD*fNyBhḐNI6b]Z q" FĔd1&uukʔ).ɪ3g\[li.TSLzSRR]@#bNLy;"B͚@cD@))u8{ BhD Ac.ryZh(rw 4y F<-]*.yj!4$c [H bJU~T^X tT&n]@;wTFFd0:k׮vlm۶M*..VVݻ7H-Go,+$$D-[T^VshǎJMMUVVզM]~ s˕D@A!!!jݺԬYz|%)++ە,*22R=zPHH {!6֥u;ޭglvPG۫B^~Q n±qF͟?_;wTYY>aaa=z>N:Y+h"%%%^ӆ d2oݺnV?^~~~=Ӟl-X@+WTzz>ѣ&NʡN2E˗/׮]m*//OoV\jFnFM4I͕իWkڵھ}V~-zҸq?E~~y-Yf]4ydEGG+%%Eܿ5s3F”h1.֥u^[F ++ݱCR5.’{GǏז-[솃 m|֭[g3SNi3f2334נA4|tvUގ;t=詧Riii۳gF?ng2_jԨQ:xCϽ;3hݺu e۶mz5i$::uap zUZZ^7|֯_`@S Bh$LI6YAP{|P*.ql@q|P--e%a=:~ƍdAAAڵ"""d6\{G_5}ך:uesԾ}{(55USEE~W=ꫝ?k^^^SLL})++gʕ{'MJJғO>IRHHB۷O8qV\>ª:(,,L***Rjj>l7*,,Ի+///_#;KjWhhrrro>̙3z^ !42b@C(.[u<'OVNNO\QQJӦMSQQ$)99Y|~>KzҰa4`EGG엛%KwQAA$itm9Zjb6O[2e fL&}ך9s3[p F<#C?UOEEʙ~@ݧ0xB7y,hΜ9zjJ/ڷoo9.;;[of̘Q/5M>j+B3/"[jٲ>#:#ۜ禛nR``U dM0+}]m۶BBB4n8]}պ;uk3fŋk޽vxx>o߾Z_Ѩ#G*>>^wygq!4&;%2\r&=_5phWʙ 2O/Ҏٳ톃s=k,Ngs ㏖vPPf͚e3lʔ)ҥSs-X@ŖԩS톃hBfͲf>C5zm 4Hɓ'-=uY&Lk.q>|U瞳Vvzj{WH\[ &A4~PE1ϬΡ8qXy=X.++ƍ\ӊ+֥A'NtxB-]Ҏ֟g_veݻn:>>VdiӦq_|sXϝX\^Z?:jA=yyyw>jwͩ@`J}n!I: &3? lyLESSS5p:=3''NKKKm`08X´4W_}թu_|*Д$tfY ,лᆱB&UfGZn]/u<\EEL63U˜r$Ie){jhGe вeZ9xN]<[ 4j35kP?w:Vd2iҤIZjUS Ⱥ4956p# WϷyZ\8evw$ct|zTΝNKU]8*T I +X V w+>>^[Vxx|}}kϙ\UugOC@Δd[PNIrnS?bu jO ,pS5gڌ jgѣyR͛7Ҍ3t7860UͭڎnZϜdWP#"\[ oB͓y䛐а]"31kEhJJfSH~Gym۶Y#Fp(ꙃS:T<< x?ReK۫y꥖˖h= Vǎ-T-_-++Ӂ[uMUkk׮-uݪe;@@n@B&$/o[.ۻ2V-V-W~z֯_?ŋT駟P+R^n~w4UV&˗/r$k:gffjݺu R4eķ[7ZX5rr|uswiMm&hi:|+:5fu>s4͚;w󄄄hvaay IDAT^{5 U;vC֬Y۷7DIj޼nKTCcO 2BpWƺ ,&&Fr]RR_Gq9ZlYԮ];1 4ydwk_թ&N(___K{Ŋz7>ȑ#ڶmSc[Νڟ| ;f޽,KǏ_kƌ2L6i.hgg"'=clk\ЦLN:Y'OԨQoԩSvu5hԩV3<@K{֭;o߾j}O?tve/[]{tkݺuvC,IJII'|cjذaڼy6EEEYIII{uj} 4|;V o.b=V.\#Fh…ڽ{{nEI `u@Se ]{-Rk޼y7oվ}{5kLfYgΜQjjN8J;GEGG륗^O?rIҞ={4j(o^۷RSSo>K.]׿\#GTff[G=ԥKEDDOѣG>U &O'xrm׮]6l:uꤸ8yyy)##C{Qii$h4^}`w}:y>S˵#GhƌvW?˵[l# fJLy_ޑ-pkݺ,Y^_}սd%''ge6sY,z׻KS/vzO:u(33rH;wtknPRRfϞ IREE<V^{M hк /(&&FgϮq;S_}Lh[AhnYfiZ`~'>VZO>4hziȑJHHk7322RG}ݧZ5`]VK,_|ZB6[|||~oT4a뭷m GM0AڵsYms +Vh:qrrrVZo߾ձcGIJ388e@cUqzh6jÆ dU*z18j|7Tr:tHGQnn`EEECjݺҶm۔bjJm۶UϞ=]hPnnL&բE ƪ}u $]!11QwӧUQQ]tEٳ]^ =쳖itw"| fo!-ZЃ>c6`U2%>۷k۶mڷoN8ӧOHpuM}ѐ!C! eŨ4QFQ}U߾}] 4ib1%'K6<)1 ooۺ@D@Ĕd!*J^~~-$1'%ټbtw2 c\[Z7|fΜƊ଻K[n:ju]TIRTT֭[`ON,__qZh V1%y B@=! QQZ*sJ{q.T0%H6q!09P^^2kZM2  [ߵFaѢE.ޭ[%@ BD l/ G! 4ź @!LI6YAPڿ;,*,,Lի](++ѣGLO͛7Wtt.2Ɗ ٳGIIIȐZl^z颋.rkm8s挶nݪ4e˖jݺz%رc:|233uiذaj֬1NѣG|yjѢYukJKKSVV7o6mڨO>׹N:{*--M SN/oo/D@L'rgQaIFunV4wwyM^VVjʕʲG}#<;e-_^vڶm-[hرĉ#XYj}QK{5kCϯlܹ3g=i$=eddhժUZ~vڥB4߿Ə޽{;\]wݥ[Zڇwٲe:u=c r-*//ׇ~?X)v~wz뮻N($LNN믿׫09R>h7߬3gJ -Z{.]Xڥڴi~m޼YN:kرc5zhû9shܹG}>}(==]s7|juM7'PhhCsٳsN͙3G7oVyyyzuwnB@*L6}V79G˶%[t& .Q!n[f~iCeeeeڸq6nܨ1chڴi2ב]wZh]?(//O͛;*WTTXA#G믯}:d2iZ~n=S.y~w=V!-۷owܡ3fh ^M:U%%%vdgg>І ;y|M37o^yi2v>}ZӧOז-[] @w$bc\WxM2*1U_R=VE],Y{ڊvکC ЩSgp?ɓ'5o<>>>1b}IRII+q?cV `wȊ vDDڷoP@'NбcǬޯ LӦMs9X<+IU||"##U^^Gȑ#eee:u:vΝ;7hmoSOU u+ @ڳgJKKuQ=CNWUQQ)SXA///uIm۶Ҵo߾j~}jժTVVӧOV5vX-_}'N믿vmo' ~0%&ټܥ̬K]Ō)1kW4cL9-}cpw3X֫W/-X@륗^RLL^ukذaԩKjCѣv%Iڿ<Њ<^nٲZW\PѪU+=ӧ>7h@@}ᇺl˴pBx㍖@l˖-:~bbܿ믿|KGZ` O3M03{'|~O<% ;k.aJLyM_Ok5pӔʵf_~./wqáUհoQi{W:*DꫯB4x`Ν;}s"##u-8U QSVZeYm)IÇw8}衇j\W￿N(Kr?ڵ|A׎;Z:<A@nVa6t{\̬3]F`J5w ;vjtMرUPRR}jذavNN֡8p*WM&t)X{Z~wdeUϒjR5(:tc۴iZ}z|U*((PFFFDJJ,}Lj|MnFv{zO*,۶p 73<)U:á2C\Kk\͛7դGV[ݻW={kcV[L.Y*4ѣG;t%~$/JMMR}זvft8GQQ̙>Hev~8&P59v=+4in3?#ζ|둜?-Zz5k4c jX'wwhTmG[l^wf-Q}3zkPEzvsܭjfjppU;''N5՗gVTThٲe6͵ &>@Ǐׂ |TTTzlMzUj]pB=õ%?ξΪ??Of&;@]4{c5gn@Iz?"طQ~V]5֡  NMٓ7tqzS?& @]8íIv{>}9 KnXѣK.j/[jeVA "S j; /XY빝ڿ.-\F2?n1.֥'WpHg+JP_v Qv,<=zԩgܹӪݭ[7Vu%9rĩιeiYƲ\˗/M7T3,^{Ut/4ݻwjoٲfY;v:yՖw8LNNS@{nٳǪm@F@nT.ؼgd!Xש{E_Pͻ ckzӫW/ʕ+{Q߿EgՕJ?}+-biZ^߿ot6mdPFGGo߾=sg/S駟4k,'|Ak/ct@zk:.DoƏoVoڴf|΄ Զm[}wk#h͚5mFF>EEEK.WNNݫ\˸x@;w֭[k5O>7xCgΜ[oJHHPDDΜ9Vc5k,Bpҽ{U/6YA@ӟԼysM:juqF_|j=={~a8G۶mZ,___1B|A{WddSϻt 3fcP͝;W[v \k̙Zd***׳>[oUkuφЮ];͞=[>OjjRSSm7nx u]766VC=<ivwe]V9x>+ݽ[G(TcO*E vk͚5;vQ~駟W_u(<+_|]nFCN[Fz7{םwީoF_~yoӧOח_~{W;wVhh|}}/\SLڵk-V=ۯYf ^gt~F맅 駟y{+W[n: @3F}w^/\^q "==] v}Æ N4kRqI͝>o8Ayyۧcǎ);;[ Sddz:ϑm۶)--MjѢڶm޽{hlUTTСCڷod0(u޽Ѽ橧ʕ+-Z;vtO֎;t)+<<\-[TBBB΢3gΝkiGӧ]XX;v(--MYYYj޼ڴi>}߿N @yEJwv<e=Z.[*߄-Ze]֠[kȐ! |WRΝչsgwWQQ;vXځj߾Kkpg;00Pw< [<;p>|pڰaչ2 n\\tY;vtz”ӧ[]9r (3hvڥ^xA'OorrƎ'NXEDDhذa Y"x (R}uW_~ڵ"""\;vL7nԪUTVVf5W_ (/,zi*//͛yf M2E\sMW\;0nヂ%00P*--ux%\)Sꫯns tT;vi<֭~ھ}o|0ڻ說JH@Q)"2QPAuGDPg|PQ@D @P! !?\vor }>֚5;g뺳s T ]R^myW6 ]RjBYdvz*Tg}) BpfH-FBp!$Bp!$Bp!$Bp!$Bp!$Bp!$Bp!$Bp!$Bp!$Bp!$Bp!$Bp!$Bp!$Bp!pE'&&:8T"wwwg B ׯ#XTժUPbl1  !A qqvѣGՇ ,UJL#A\ B \ B \ B \ B \\љ3gta]tI SժU+yzz:;aÆiҥ뮻, ( B>S'NԓO>)///S;K/k*,,q|iܸ.]xŋ5{l-[TW_}U-U=C0aB$iڴijѢ[fʅ /nnZSLQzLuZtݶmf̘_K!RxxN>m9rd.lٕ'<ӧZjsNN6oެ>@Ν3GEE~sѢE \-ZдiӬjcg1+K+K.wG<:x&MdHCTZ5S]ZZ͛9s/^ j<'N4$+Uɓ'/a1ݾ}o׮]ӨQvZ(59sF~nƌٳgkn-X@?iYfi勛x3حVɓ'A۞={~z=#V[s.TR.3g&hժ,X-/// KVpmٲDmx ݻTݻ 2G? z!͘1#~~~;vBBBg̘GyݻqFSS .,glѢ,Y'|ӧ7hȑL@QHv4k,/}8?P}1[rF5k=^/}<4HцIouk͚5ڱgϞ4qYf?{󅄄($$vs5;t`|d H=vݦ֑'sian-yfο ִiiڮĉڿ7on8Q8bu$\aĶG/^Ծ} u,łǑ϶vܩ_T{W͛k.Ink`bΘ1P~Lkʔ)F͛ZHNnܸaE$IÇ/]ݺu [niڵ6_I'4mpGYnM^zN:Ŷ1baݼyEܸqCׯ7ԕA+005~2v-솄XհaCiP1: c눌Ԟ={fM2E P&MԸVZe_ԩS3-!ɱ϶{C .[>Ξ=~Tѳ>[l|:t0,RJII)@.Ϻ~ܪU+Ù,E۷?46(76oܸ'E؇j>^z322lذAr@@452/]Hauwbb;''GV2IѠVvvnjt{+44T>}>\dϞ=hQ_5㏆r-n۶m[yx;6 ({[e]bba֭[[|<7K\Pӧˑ'si)~$(sLv2}H'Fg[WrruZW Ԯ];C]Qܟ~Pf5vǎЁ Aɱc -[ZW/~JQ @Yb>6hmZje(?~&q>}ڰ6̑'si)N:e(}C(/sL}ܵkWZ϶-ykq5ϭWL匌 ={9/P'N0k׮mU5k*66TSNlP 6M6-@>|XƍӁteݼySAAA QfԮ];իDg,[L_~├lW^]mڴoZjY$Q+WTNNܤI|wG̥q'HKK/b{̙89sFQ``j׮6mڨ[nB r9M4I.]ׯ+ @jԨڵkzpTmڴPǙܰkm9j/5jv<~j֬i=\$;HNN6 ԒTZ5QzuC9>>aJOOϷ]C-u!:tPD9rD}>C +V0رC3gTǎoiӦWBB uW\ѕ+Wtq]VQQQ߿^}UUT{2™֭[7nʡҥKǂ H%{ecVq~bŊVUzu8pؾK(`rŊ~0b(:.8ӦMSbb`-5sL 4H.\KpСC5}t[Y1?rVUR|ę@Q~'êQӧD:t֭[x)++˰7Tn4{lB9jd.;ABB CevܩzJ-:222pBע>sLۋFFF;+V޽{XbH=s ʟ<2 ǵϋ <byJ7;HOO7Xǧ{?^j{ooouA;v=ܣ0UX4)СCtRAWjUuU͛7]wݥ )99Yڶm~g9993gn߾qzߌ C|n޼i*3FY{e/wA5sLۭ/o?K2335vX&""BÇ>kV.]ԴiSխ[W~~~$߿_6mRLL,MZt `z+;;[~a /ZtƍpM>hAs 2򾍛 e-mv,,z{sy["AAJ <7.l.y 4ue̅?^򋡮-mvMx.???U?R~ j B q 5Ξ=k(;.4 >\4믿]zա1Q\DsYՏAAA|\FF֯_o{'? 2` ?KÑꭷ۷Mu}ԩS~.PDD\$UPAժU3yb̅Z06lPM6uh ,(ll\2tu2s  BNW#Y]ABB\^RdddhȑMu7o7o̷)ve$y{{ʅ}ӧO[ϙ3gP 6(==TRxς ؂#Op7j„ 6[}Qy1lT6vp|gr;r\.dmxyJc.l!`'7*NFFFKӍ74j(ٳTWbE͝;W[vJL999ɥ?W%a(d>9rĪ{{-~@A̷}xr\e7Nn2S~ݝs|s(;w4$7ԧO׳-q+&&ƪUX1nJ5Hvb~֮],n{n7V*Ul\͛7/>/ծ];,SB |Qbl*/o} L[n(xޥK++:ydϘWJ 4izW x@ӧOw⍼g(6m>OpEszRPP`AFdg[͚53|uI޾}[w6kk~mΝ>=tٰ")&&Fqqq]zܳgOג1cٳթS''F&[PnҤ*UhPݼyS6m2Եo߾|AC|;0rJCM67dV5~2رC/aLܹ>syyy9-+WhΝ:8)uׯ7VJ"c̅#~09c{Qemyxx[n:K?|ZjaÆ^o>Nn޼Y)))Ř [#AIŊCb۝Ö~~~E.v se}嗆nݺͭ6pk߈uq qԳ{P^bE綛1WF mTqㆾbٵk!s4 B^~eyzzʫVҖ-[ ͛4ia5k~TV-Ɖ);;[Ǐ7|<<<4}|Jc߾}R(ɓ'5rH>AAAzglʦ5kVY|f͚ew|^u?n*geeiĉ?Xm޼ٰScƌ*V۷+11TTzX[r\QFڵӜ9sEU˖-Sjj>°aTbEŅĉںuUm5j(|SF*-c.|W~d-z%I:u2$^w}FoFѦrppb;vرI&Zz VZ)$$t]ff/^o? Fo5`?6]Y;}ի[ɓ1bLuAAAOxp႒.?y6nhW>`oݺUdbQ'sibnlltS]ݺu[nY5/yfW^yEӹs㣰0g5WNN[o{Ӽ~ aѣG5|pmڴI\rgi?~|njct8{a€=s6C矛*TPTTT˖-Sڵ-h̙zu8ؗm_VZeȑ#jݺ󚖖/BӧO7뭷Ժubիرco߾׫F }^ЩSLujRTTs[NNNʳl %rjܸ*U3g(66VysԂ hР7ƪU4i$C]HHnbŊJKKSBB;~jEݮPj׮]:|pooU-/::ZÆ 3|vssS&MTfM*66֐\ܦi>r]|Y]vխ[Lu+VPKtUVӧwjժxmRR-[yKG}dx KuR#22RQQQE^񓹴g?Ϸ@Io^~m79<<rժUBװ(8]tѼy:ݻ+55U}(֚5k _ׯ_`^111ɓK lQ'siunѣG=+_W``"CY{{&M'|Ҫ3溆Zؾxb[(Wfff7lu&wJJ,Y%KhA_MrUVVL I>;̜}YM8Ѫ~|}}5w\_Mϟ lSV-}$a3$TOzH^uAAAz+X p6mhȐ!tÛqssS Կ=sP( 0XG> 4H:u*Q]vպu4sL+GIjٲʶXA 2L?K,=zo߮W=4h ,"AQ^= ڵkbcc gN:۷_q1?(99T Qm 2`KSgi˖-Da^*TPǎ5j(م l2-Zohhq=Z% ([N%ǥKtuUREիW/t;҉'K.)--MU@@Uf͚q2,cǎ… |nܸ۷o+ @W4h`7233w^;wN/_VŊUjU5jH5kִY?(٣ʞKԩSZd 2pGr\eŕ+W .())I7nܐ^zsܾ}[:s.^h:G Sf쒨c-y!C_~1}Ӫ } 2ڳgԼysٴǏѣt钲5ke˖PM$@ O,!AK\ B \ B \ B \ B \ B \ B \ B \ B \ B \ B \ B \ B ?~4h`߅ K_Mm۶URRCOIIQL1lݺաgJHHP=Odd{T5jBBBC@@^|E}ᇒS{Cx2.\'OJs9%A)""BҟI꯿)qpM$.ڵk;wv(4-^T۷*VĈ$///=3g$i=z-F޲e˔f*;\O>*TO -Yp$ҥKM_hBuq^0yDDDu֦e˔Ĉ2,''GөSt%UPAkVvݪ2##C{ɓ'@խ[Wm۶gcKKKSLL.^$yzz*88Xw}4i"77RaݻwӦr޽K|SNút&F{޺ݻ%IΝΝ;թS!A6~x]T޾} _~ѐ!CLW_}UGVffϟe˖ٳy{{k;v|}} KJJg}^ׯ_6((HcƌѠAL_Zc Q5|pX݇5֭[g(wͪZpVXSNy6l={jȐ!)ݻwo*]!"AeLRRFz͛77hϞ=o'Iڿ^|E%&&699YSLQllNjqʕ+;v-+-_\gV6m,$~Gaaa[mϞ=aÆɓ]-8p@P>}TFbT^]5kԙ3gL8 K!!1b8 IQׯk[oi̙Ӑ!C*I VӦMdݻWVR&M3ɓ'5lذ|o3qƪRCےWW_}{キ> 1m⶙>|x`PPׯʕ+CtN8aFvL D:tH7.ѽ8$ Yx]&ooo4ha۷okɒ%:u%I7nΝ;5uTz4izix;0==]QQQZ|n۷omJJOOK/dHիWOcǎUnŋϴrJI&Ə5k(,,t@f~wCQF]bN8a*׬YS?uܹ*srr~;lذCXi]&OOO԰aqWB 4HW^ѣGUV--[LzʗT}uT7G}8S[nZj||AIZ>?Tw}X)M\׷-[L_{zzjҥK[yzWuBϕ,H 墶"AeK/vyUbESڵk?طn(믅^{yZTUf̘/qY#Fk֬ѕ+Wmg AWnqۼoD6nX5kִ<<,ߴ'""P-X!!zg-e˖+Hve*:tk,Y,Sy̘1%s 2uff~gZLDk։KsUV`6PnZ~~~][vmC﷨jԨa*۱ckOOOˢ>rkΰe={j_) wu넄-\ЦeH^zn}^z_oiii^axZj%zO)))ӧO[ݾ(7n0}mU>}h֭|M6)22R]vUhh$]~]t-eff[!Ae5oۤ[ &11Q٦ӧգG(HYcߣ8?֮]kH޽[w$թSGZR֭զM1?0++! AeH8;[HNN$)==frssn޼)I̙35sL-\0_xkՒL3<@$UX{%8P"YYY6gNNMm;qi˖-z7ԦMyzzxm||fΜ={jÆ Vǚ7A[d07%l(wUsuR4 7Çky<oVFFڔ;V^^^޽EOJJ2$\Ufu`)#Jrʆr||s)B͚5 .jݺ 9s(::ZӦMSڵMܾ}[SLȋ/5j(uP RݺuMSN̙3N( 'Ny^^^zG|rϞ=CYt8CQF6"A(Ν;+VpR$k֬|={,jky`K$%OT^paᜩe˖4a({yyY.o\j۶MHJ^zzL7nhĈV' o޼իW:[n)::6z-m۶{Nϟ7յiF5k,={ƍ6`k_@4g :Tׯ/ԗ_~Zjnݺ׭[>}Zo6'VEQQQʒ$mڴI=zmjjV\+WMkV5(777]vMGŋ |}}5ydظqkwwwŸ J!"""rJZn{OӧG@@]b QM[*33s(>>ޔ-Lj4k,}3;;[?ܥKYX-F6O?Tk֬ѣ>jQ¯jժ_?\?b{L_+Hpp[ 6LM4GkիqiÆ jڴEqm۶M/_.0Ng(n߾Ç+..N׮]SJJ5j讻RjSO=IR۶mh"^~]ǎә3gteedd]*URjԠAEDDXO?I7n:IDATիW[} BK5l0SSf͜ϱcԧOY_X|6"[\BΝձcGSrb4Ҝ9sLVZ0$.7ߔ$i͊uJǏ?I&N8&Ѹqc 8P>)qPvv$'P˖-p^{MAAA=SRRԼys<Ir@ !A !A !A !A !A !A !A !A !A !A !A !A !A !A !A !A !A !A !A !Ar5pFIENDB`leidenalg-0.11.0/doc/source/figures/slices.tex0000664000175000017510000000616715101156755020603 0ustar nileshnilesh\documentclass[tikz]{standalone} \renewcommand{\familydefault}{\sfdefault} \usepackage{sansmath} \sansmath \usepackage{tikz} %TikZ is required for this to work. Make sure this exists before the next line \usepackage{tikz-3dplot} %requires 3dplot.sty to be in same directory, or in your LaTeX installation \begin{document} \tdplotsetmaincoords{70}{110} \definecolor{c0}{RGB}{228,26,28} \definecolor{c1}{RGB}{55,126,184} \begin{tikzpicture}[tdplot_main_coords, n/.style={circle,draw=black,shading=ball,outer sep=0pt,inner sep=0pt, minimum size=6pt}, nc0/.style={n,ball color=c0}, nc1/.style={n,ball color=c1}, e/.style={}] %Network t=1 \draw[fill=lightgray,opacity=0.9] (-1,0,-1) -- (3,0,-1) node[below] {$\scriptstyle t=1$} -- (3,0,3) -- (-1,0,3) -- (-1,0,-1); \node[nc0] (a0) at (0,0,0) {}; \node[nc0] (b0) at (1,0,0) {}; \node[nc0] (c0) at (0,0,1) {}; \node[nc0] (d0) at (1,0,1) {}; \node[nc1] (e0) at (2,0,1) {}; \node[nc1] (f0) at (1,0,2) {}; \node[nc1] (g0) at (2,0,2) {}; \draw[-] (d0) -- (b0) -- (a0) -- (c0) -- (d0) -- (e0) -- (g0) -- (f0) -- (d0); %ghost nodes for interslice links \node[e] (a1) at (0,2,0) {}; \node[e] (b1) at (1,2,0) {}; \node[e] (c1) at (0,2,1) {}; \node[e] (d1) at (1,2,1) {}; \node[e] (e1) at (2,2,1) {}; \node[e] (f1) at (1,2,2) {}; \node[e] (g1) at (2,2,2) {}; %interslice edge 0 - 1 \draw[-,gray] (a0) -- (a1) (b0) -- (b1) (c0) -- (c1) (d0) -- (d1) (e0) -- (e1) (f0) -- node[] (f01) {} (f1) (g0) -- (g1); %Network t=2 \draw[fill=lightgray,opacity=0.9] (-1,2,-1) -- (3,2,-1) node[below] {$\scriptstyle t=2$} -- (3,2,3) -- (-1,2,3) -- (-1,2,-1); \node[nc0] (a1) at (0,2,0) {}; \node[nc0] (b1) at (1,2,0) {}; \node[nc0] (c1) at (0,2,1) {}; \node[nc0] (d1) at (1,2,1) {}; \node[nc0] (e1) at (2,2,1) {}; \node[nc1] (f1) at (1,2,2) {}; \node[nc1] (g1) at (2,2,2) {}; \draw[-] (d1) -- (b1) -- (a1) -- (c1) -- (d1) -- (e1) -- (g1) -- (f1) -- node (fd1) {}(d1) (b1) -- (e1); %ghost nodes for interslice links \node[e] (a2) at (0,4,0) {}; \node[e] (b2) at (1,4,0) {}; \node[e] (c2) at (0,4,1) {}; \node[e] (d2) at (1,4,1) {}; \node[e] (e2) at (2,4,1) {}; \node[e] (f2) at (1,4,2) {}; %interslice edge 1 - 2 \draw[-,gray] (a1) -- (a2) (b1) -- (b2) (c1) -- (c2) (d1) -- (d2) (e1) -- (e2) (f1) -- (f2); %Network t=3 \draw[fill=lightgray,opacity=0.9] (-1,4,-1) -- (3,4,-1) node[below] {$\scriptstyle t=3$} -- (3,4,3) -- (-1,4,3) -- (-1,4,-1); \node[nc0] (a2) at (0,4,0) {}; \node[nc0] (b2) at (1,4,0) {}; \node[nc0] (c2) at (0,4,1) {}; \node[nc1] (d2) at (1,4,1) {}; \node[nc1] (e2) at (2,4,1) {}; \node[nc1] (f2) at (1,4,2) {}; \draw[-] (d2) -- (b2) -- (a2) -- (c2) -- (d2) -- (e2) (f2) -- (d2); %Create equations \node[anchor=base,align=left] (S_irs) at (0,1.2,3) {Interslice\\ coupling}; \draw[->] (S_irs) .. controls (1,1,2.5) .. (f01); \node[anchor=base,align=left] (A_ijs) at (0,3.2,3) {Intraslice\\ link}; \draw[->] (A_ijs) .. controls (1.5,3,2) .. (fd1); \end{tikzpicture} \end{document} leidenalg-0.11.0/doc/source/figures/slices.synctex.gz0000664000175000017510000000700215101156755022104 0ustar nileshnilesh]Q6~h/|"%!9 KٳXGY/Uoj|˫ES<XG$]ul#$xIBbtAB~gnWa8'xa,A=uwW/z 0u~h{׺)+W9DrM|V"7p f2tL WS+Y얜BB,Te.Yrܯ[VyUAKBm^j09YV7h# ^PZ'6ҦZv]lW N  y3\ R꼹0'vؽ.a<)lʬ*9w/ :pL78Kd,D^,#[#L ovw"$La-ڦaRx u2E^ɅSw(.\Alr7"GS48F&7섯ZGZWjj=(9E8ǽn3V*hvsJBTchDXfݜmlZV'C)LP;{;}qjcٸcܨѪ:+:F^٬@#۱xeR.+N${"{VȽY%/?'P".EWZUU0Q2O˕CrCR,(WQʎ((O]z8goc|b[q{*!o1a[*?86=M-N;_NW`3x8qjV@$ n=!]3z1FC X*qC[Y'/JRO7crq[|nA8}xkI2[dLANU*^ "2>Ҫ٬l{"Ť9B;NWۻ9]CFǼu!W,vRzFTWoW:cS/Nڡymηá;_=EO圭KQ^ uL [~En:fpSe}>ZSSYWNe y3 I:Cu, /Źz3شm"v?P8kac0|m]2{ C|O؊_ٿG- uIGOgfHftZ0Lɵ.Z(]Rc]jCt.^H&Lai==\E 3ۜ3 {X6$͉l 0 B(H-=G,Ji8B\(p,^E߄>`tVvf IN)c24APB2{CiEQ| KL~틂}:Tl&&Kڴ>4[J qOz, Gˠ,EG]A>l*6SԇmAbiăf䧋*{nomheS|(Z#>чVX!@[I'/6E%}:Tl&ԇ҇6͖(KB~A?I}PO'ahCvЇfK<(Y,uA?󐢣@N6ɇ!M;CXJ4"Q #~aUd_[TğC^I0 ``L敪OME>C & <ѣ\LO`}dp 9T3T0۽=X$.wGmGhRV $8MB8^288DE>C ޓ88IZ=(gء#lBc4 .㨏ctw@a ,%=.㨏cve݃M Pte2ΩwCF&Z+_ #ʃtDRb]vWDZkC{5a|D Lp"HTԀ({.W 2UQ`p+`fyl])A?i%* IL>DU;C6dOm,C v $l&W تR, G^2'm聀J}ZTlplylHƆ/6cts@a7 ɇplyll)RƆ/\2BE>a$l&WU;CXndO4[hKC"+у5ɾ{ͼ8$$.yOE>C ޓ11SE'zP<rq2>SQP!lB$II†@y|0>28c}G'2^ q P8#<.㨍c7* j^ q Q"6jqq1Z(g#lB$ QS>GDZ>)C {p^`q,( Ð&epq^LWc]koGt}E P45(϶F0TƵVvfaiFO(iҁ nB"i8pPc8 XI,8|ä -TMf8|ځ+"A* IL;w 񓰙|>-}hl)LT)dOe:AU3\Of! V͖by Iሊ}IL>DX~!R,}i%~!z@`f6/$_j}h)+!:.dOڲ2EwTTʦb30EZ~!K<RzɠyWEPtW3v/zO:7OJ"p^rI)xâG%5CSSsۈ1BjMQV]+W6Y ։eNر]]HD”?'&5l =+(WQ 6v/zOҀ=J4d`,"D%j7k ޞ Tife׮H8RLJn{#,<L~ {T|ABlL.?-Vɏagس>؄W@+pHKOowF,{ӿ3]xY2( LEP-c0+'poP<C( ZB7\kp}g )x!"t J:#qB<?$@T$ Gv"%H9R4 -OrF ,'PDP \ @ףqh{*=vл(*Dߠ `T)czs0,c[bkڱ^l{ 93c8=2p[qI\7}x]% f|'~?H XM9JH$l&:W ÄIH!HXD&"^!HTɘL # HVeiD#-h&rKG"/Q)kJ%REiܠSST՗@NޢNP?$h:4Z8-vvNkat}/~I!/&-MVKdD(YT]+A4OR=91XVZ bcb q#qoRV3D 'hBc%&C1v27SLSLd0O3&A$,rc%XgYX_R1R{ڥFc;GȰeddt<!;'ǔ˝{*o?&?(࢐Pp]aNhXxYqVdTtE5[e'yeyeW,!%M@Ujjj꼚ZZSu:G=^FnnMiM7<6q-VVCm6G;I}TT'^V.k{Xwx~ŪUhz\l6 }~~[50 M Iic35~y=ߙĘ1yl02mg܌on6kfa^g>ar|8[x {m->[Y ,Zagdj5FsM̚5*֑ BMQmmK;Uhfi67{Оoi`#X8$TY9ιyeUW~177[ۼ~GKOOguk|Ե}OϾ_ZW~F~~ jf-; C B M #5-sZwpTixQs !yå#7GGF,GzG6F.DEExxo+gccccccgW%8$$KtMO\LN:ܑBJH*ڟ6^.̰817g"3{LA`0K+kWDMvms⹩t64w|3n3os_r-- [Q[n+6eI;~)0,(/3xgoB].ڊDEcv!ᇡ=TV]|İdWzG~\w̬>¾}?Y.^W>y@WoWT:$VW\_3Zk_Q'_np#vGKM8QXWMAM9[eKH=!rۖ/kvkt_:̆oqwxۑk|G G?yK϶Nj=|!W_;fK/_>M-WWJ-33gg^zM{v ·O[L>-,qi̧9^Y&.W}J.P cHRMz&u0`:pQ<bKGD pHYs,,sRtIME 1lIDATxyT/TwWwu7&""$iMHbfLԄo@Efrf#dHLcK;cP PYZ~sNRB!R8(B!4 B!$!!BIpH!B(B!4 B!$!!BIpH!B(B!4 B!$!!BIpH!B2#BE./dP'@#ˠI)C !BHCB!!@ BH^ІB6ԕZKH!}ieB!FB!Q8$B!i !BHCB!Fᐐ¶l||1m:u&9bޥ89e ya $s[wwsy`Ρ?C\`| sO!GlɋYu:Csy7p ˆ,@0<_:ÿtzϵ?!&x J xpGȺO?8fC8[A% ]~7Xscaz._ko ~ww*Mm4rHH^ g䰿Ѱq9 ŭY8mϧhgg{3xGvn7h){/>nVo5 ( Yhpá?(;}L䜇{nWS# 䁦n{+!d4Q8$$/ }m8趃%Bmv89dh;ߺJBZsHHq5t} }8kus#3R mpKpn6ג6BF9qbp]]rO]ws6 9wHH^WEL] ɋb # 0 nP߇{~mu_ĄQEE !ñ4@S].['1źo&9$$/u03琋_1H_` yZy]]!dDh䐐Eܞ( >BnD!wA9ޣ]Kp^WBH9CBs/qu0XK!o4a u8hٌy= `SdBHP8$O~0<C #)}C 9 ^l^PpJ| 3ׄB!!9aÁBb^m+,@ftm ^y} ^ף/f\m<ջhBH4Ҁ'IQybpk4 w\.@?pވwoCm]CWL0|;B]p Tk`ub-@^Kp߭Yk06ף;`'79a{7z\B:9$y-9ϣtGyxa|01 Sy(;_~]`2FBGᐐ`p=J7n pGK Fo @Άp&vgM_R'z u]콆{mP g-*^ (w0|@8.B0pH_Q} kGlZ5}oD DI8Duwl}:as?zOGD&=}@uޥcw\zW罦W5(x搔ypܨCBFvoX 77B牬cv oPxZ y#ߎ̛ƢDᐔ ?FA2z V 1O2^ϳ?!!}(V8LC7yAkI)XO2J m]nFhZ_:#3R Spn;q n[(bw;^Qkii.(2Rkp':0zoތ3 &Ӿe]X{ 0& ,=q`H5-8e}U8kV@2_Upg:foV&, wa`H NZ:h6ܟR)PWoÑ-~,lـ|́,@&\ms@XQ _Nb{FϿa.szo!)T hNL* /s>.h>7uo>?Mj_p^a1s}$q20WHZ< Q0$|\j7#Smm^h%X7mr ~>{Ɍ@@C`ZZZ?hGqu;ey53O_e~6:-Fg#C.Zp؅% f!ZsH۝0f7vOKvR}= QGnF[5`k.G[_Sٝ[{|yXzc!!E!OOo]30K9jMC14Y5kz|{/f(RD(Bo]3t9NBXu~u#?D) = d< ޖs07n Cx~N}}=Nj} m!%!KQkۂ5O?wCHvqsBke1 Ԯ6 8ż+R2(◯<)_qn&!pG 5߲+P0$Q8$c =0.U3X djzOߏLȌA CŤ+Rr(і-C?܍Fp`*)sB\9t-H#U Q8$/_Ao[8xy#op#:|tH-~ף*R(ђ+Rehl[8~׮ $Xo6 tsmH*R(@zwـ]Pǹ;N]xPw}[.ތOߏܡnחQB[ ˽!%z+ ßE`ؿ'{~dn >8_{ndF|,xzۅᗘw&ob7l (}UY6R&(Lա݁幬7QC΃ wpAZ/n9(R>pH ގp*/n/$m.c0ӳnX!q_ֱCjs! ]] w)@ή@p_C)s$IKBE M0kk)b_474MdFF7}6x9wj}ܟqh6F?w ^oCOzhC `0\_n{O;jz#zo4I `i*ڭLAHFH n$(ip>_=!! wK d& f $NpteW (`QF'hC  @<.ewpy_nEb3^`/U >!X}B.Aed* d<M,l6RrV 6pHzP]3Bc~0x `HHyT $*xK8M_Ɨ(h!i(l X(mG0AF3B*P8$AV" hV܂m]ཱྀ*P8$>kFH``;xe]ȴͮ@Yo~"$aU pH @0r 8hyܵ{`C(W0$ao4pH(Rwİ ײ »_`{(Q - o h6vnm~8 dpF\畯!!P0{n(xp;*d:n`Hry`xpKSQ8,OT]& nڔ *n'1JhZP0$DX(,*P8;u%CFpԐ>hFRpğWT p8])*4j8v(TZR(mWni`H   hn@ zcOz& Ѩؠd<k( 4r8:.Fu )P.G5= xX]@SRhp]) A!/0ʺÑ`H -d}T?hFp5ms(B.dh=1o%>z IZÑ`HF#A. )Cw; T])hQ5`0 X w#.$(Q8n&/_C×]& dk )h/ OKK˝>!BrXШpF3/lQ0́ wK;@< V XBO!7wG @C ܔZOL]2SOd Q:!)e(T!)GypA) )9-)/8Lr Q0̿b9fRpX.%lruBRVVއ*hVe0 CRf{efP0$5׹Q0,P0rx'!gyH)`HHh[I@@F LC@ʝK}!CRji#Joz8]S0$λ.pHHM)BHŚC QJ+U h܀;T HQCBX) Tk+,`X(R%RRJroTQ8$p KmZN.x{HFHQ hV }C !ߩ\JꚑRڈ`X(RJ CRꨏz5 E )\0X!)U4g"EF )|<[̞:!R,_S o膃* XP]3RNQrݡ]61V&p5yCޥ 9LJ CB [1N)g`HJ]95| bRWP0$A5bRf]?ҪI:` K CBh`Hʕ_\6P2@ᐐV #`3K I9XL>q@CB [oH<`'>q@ІB b`3)OK9 e!!d8)waC@PhZ2T GhVh2:! Űil8(19,aʱcT>}3c s"CVHE-!Q*9 %${ 8p@{W+:::g͚e_}9sK>_2([!![(7o* bΝGuJ?*Oف6M{I|fϝ`'U---/##J)fâ?[qfA+Pw񕨘p!lۆiL ц Nw}we+VHDQBzY;rAu5~RR`H(;w]uLA W뿀+n@e/0I P;8>܊ȶ4}fܹv)T(`0\`O"ޮyXl-&L UQ plCM($ p |)zb_/q71(rfUh=1@lWILaU%w=Kfť&Q\<).8UjèD8\YQ!)*G_^7ܱpm99ϫx*y1m7y=Lۛ(al'A@mٲ%pݟߋS/@UHuZǴ ̜X uFFee0t= MCV4ȪIQQwusp_p)ܾ@}λXrz_$B @) OL{{>_EtdT*\ "*LQWJ'ҡi;,2dUd@E䢫cuX/O(W@ wb0aܡ\ EDdPd~T2+IW\K| ::̜XʐB:EjP!MC(CH H2$YFsNwll߾2ϳ =j/мwb;Q:YR'C*MECF`›Cʰ6xtv`9 \1p3$Hw@KKKhH'EF>u>}sp7 *Qal75?Ri*2 ڭ\Dblvu@+P3e&EAEqIl!K[&=,$Igej(zk{"k`HɏR(_CȐay饗P=mc`AO<=&N I987q2=䂃sYBZ$޽{) ȬkZ 1$$_Ju0R2(4rXD;ZU۲J& PUstDҀa,N1AA! 2ѪjaN4r),~'X<. D`P8,B3d2-$`t<,˂im 1! @e%G)}(FA "9aiR$,EVHdžmY0 e, 9Bx#nHGc㯖/$ILuGu*̻8pu 1TE)pXD.\h555!y=ؖd2Y9۰,3e7 ":n  8)$ib躞3<#i&`,8?{---`Haill4eMy̞NuBmvBV!cpl=tl ́s!TĿ}Ν;{!H$twwGEQxUUuQUUD"#!p \`HȐQ8,27.4No ^g/<2>w=Sx$g< dP8,2W\qgϞpP1iPQZP,!pw&IB#6g`DG`Μ9f !%&8jX,(QEȬ\2OڎwP3꯼@0LR9<ceB7^+1|nK(<BHp԰X}' Eg}fp4TL@U@ҭd?2wZY0'<IPx$tr1> 7 (pXΝkӟN+geLCxE ~[M6!ѿ+< ),u]Q0$axG;vh4׮]۳vڞve׮]5^{pB6Üa#!~?l XM5 n00ی3?|5L~Z@ e޽zssspBnB_kx1ppG O2s  #_~FsG%c۹sgW^x߆u͛7W˚~7hqA P.ypG `HUD"g餑470rx\n.hH,{챨t*'NGd,p g cNİcǎ;vT.[,bŊD4~eG )pX`b|wԷi @֬YstrNx9s&c, , I][/W;w ?똙¬n/@mòlF LbN;:455EgOQļ*Q`0| * dQ8,  hNxLR*@Es $ɖ$£ @Ȳl(ɑ: [Btt8 YQ`&L@HA4$ (tUpμV}Td\{(?0-p𽖖P0$aYf>l'M;Gxp}(BB!@?]kBLΝ;;vUH)Âi0B)( YqΡh:c ]ニ͛JFǝ?j p! ~2CRH$Ib$Yc,]u0wq姿UD*T8BL0gp9`IRP{BӾkFK'g~G ik $(CR)<>cQfpA<|3 /J\P0Lѕҷ!clQT0gκFqNlݺҏ`##9X`H <`HJ]ǝ;wVÕ@m jt| h?@b\3DUӠY,PU $RwBj}MMM Z4< uԐB0OoN[.s"d #< w=.W^ oFԋ]is|p& a3 -}T2EUݐ($@ I Lcp86xOG )G`޽?HW]u=Qm۶U>SQ`HH dι@Bh؄^m}`Ma, !Y9q9e Rqݐ <"ď1BScj1[tF !ؿܬZ[[c3|s%KJP0$dT@E#GE9p-#TY I2,ZGW% qAtp=%Y 2$Y8Oeh d XL%c5|n@$ b1fBpڪMMME=P,!!nDQ$g׮]aL %{ʝ 5iqB/$`FSc~-'F ݻWWoWbEtf\ h-aLďBa|6~7y]K.5lټys5{アn-F0<qXd2M AQ8LˀmYmcs; !CEP١hԐQ8ƍ"ڀk݋S!) @(B2|TL]-?q~g}utt(CByבJ%!2@ if18 2`8, e² pμ5 3466~9a'/IۀL>B>E_ĵ5t,sN-PBe j0叾s[`HH񪩩0 YqڶUs0 Akh{mZ]a"xfP;~]HTu0LuGu*̻PxȔY!XP8a ?}L|>ꢸ:ftdH$ BpΘWjuSpu'cRd.\hn޼:ulD2$~(=h)ؖ 2m ± 4:}c ۶UMTLT:7 5 ߜ>gr|̘zV`B$owܝJrM ِd$Df\#oᑿ{dRDΝkհ1:; Bz؛9Pc}`lۂmYplܱC@p 湓cǎ_~yw;wNCH$4 &Ba7V~Iȍ0aB=&Gøxb%&Մa9gqhTUE" qG `vy${O?M )"7}zQrǎGZ|7K`&4M,+e pμhc;6!˽G9q`9_֚lsM(,~*D갆 +n2*tHQ5{QQC dUG֟cϞ=aeFHq;vTCմYO0Em)$,]S !gRg"uH[neX2<*«lUU뎪"تH$bp&BFa޽{uf4>@b8z6 l& юq$#!~#$A W{̎ᥗ^ BGcclٲxSSSi .+$;@t7:TAdB{ Xzf̘1&#u# 1[i !#D09W@e(tJ]p6¢OȊd@k:gΈR(VXE3z5#H7C 'Bo[&[g#oA0Z+WyXx7mB!d(:tH݅vwgXô$IBwNulL B TL=Ӈ|rF5ކ3? E=rxk w2+#o:I|?#<~/t7hZCB0P8!zlXT*s1nXw,˂emqL'~:n2!!8-4P($,˒z#y}oEL6E{ lH]{OlXǛnn e}! wCz5k˺ۭp@ɄHP(:0NnLmCi,NE7bt-3Q0aB"!db|wԷiH?33N߽) OnJ-9#u(<"}Tjx4MSmoo߯(5ٴi?ڸqlFCCY&OE0d B:$-Oa6 #Y![жmX^oe0HanHtQL̞=wy'^%\F+Ν;wRJ8 wy[B(J@vJIѰj_sao^ڦMpwݐm|WX #!C0`ڴi=zpzXj^7smY0-F*N+[㜷Ǡ8{0aT5)x`8uTwзvڞ[n%745|s-ܒ,qs}~ҤI=u_ji GxL?Κ5kr>!a9s;v q0j.$ܝLB@p9dYmN2Яsذ{hQO]vkkkf͚e?쳝}&1c~{b;hw}w=|;lll4d(:u/?YnynӦM~Hs1.{?fJᑔ2 gz*:d F-K6Y2,ry]lۂc` !SNR`0c%Kcu;gϞ_Mᑐ߿_뮻|U::&̽``[5!+*$IcPCq9]ֱn2GCp1H쩩4<Ξ=ɾ?X`l ɛC26cPx,>룭}I?oNs==d47|srݺu|*d݆"6oܙU(g6Y׫'N}^/z%dS˂ ^ `v@{oIƧ6% Q0,* x.'4֬Ys_PQz"E(u*ѩ X1 tD ,;9ljg] n*N{-["Too0w\{ M)t+$OYBVC$x-2˂Y&"ZQ;g 7<& #ɧ`0\lY//E /{~ I$cmBe@4ݫ755Eqe^hB%L>Bg򞋔 㵴m \7uW4W^dIF3f`[l<3GqQhUuNVU Y ;4Ĺ1k޵d8Dᑌ% {アn-s"}j C0Q$[$>PW-p*uU`jm A)nÊLD(C 4@ArHO.\hQ5jjjLHq`XT47 o Bo "gbMǻn<↿[(n E=rm8p@נQ8s`SSSSTܧe£{xĉy^{PۼiooWcǎJL>-\̺!?F"~sFP@F&jX28$ݢMS Ϝ .29qS&lOS$s!6Ҋ烷_TFQ8$%m1\}eޅX,&oݺG?5bv4Hƭ_9~-$b,V=gy|m۶Uxߣ`XFe/8Eٻw5^ÐHZGg܂p8L@Q$$LmHDZPȹ;V(I I2j(z5CR ֭{bΜ9-Z:kܹ3Y)WˡVAp#pR=hjj^/Q;M`X$ Em@k$!;8-pm0N w=&Rr/c xH>z$O%-u*4 Nj/xx^V:bcEWO} 5S.cm2MF a6 N_Gؼysw_Iu?={ﯥ`XA< /}Nd\@`0paY&4M,+j;Hʰ`RLa's%5^(2hHB1]op._E]eȊ9ǁePBP)Y.vzWa;?/5kaÆ.gV㎞zꧠpHT*MӐJ&!IlFH AVd@38 ˲`)-˂mCᶹgfb'X(2:+p)p_/Eee8dʀa4+WvB\WBԩ! }٢NTmVVq98SOHM>}e"LBp6Lo72g{>lf 2=W!~?dpHHZg4V5kZ-[DzA WaɝOK/AJW3닪u K eYp̱"+3p8nZE K{{~hkk7|g>㩧&Op+NOlˆPsH Ɯ-;-1mKp,@pfIHZnr>  }74v![nsdcve?K5,ӽS/D]UqÁ9zm(-TTVz&cC Pj/μ "K,1@aزeK__/tRcG&$YfmmmZt;p]Z[PV Oqeö-8X^ =: P(q~Y?xo8UH>%̙cv]1sL>|e[G9׸2SjØsaǻR8mtxzQU5H +x(*lY$+zTD.ۿۙ|YhEz(FS/W_}֦uS {&P7s܋`8! Y CfHt̙CospHETLUE$4c89vQcԛz4 'P8$c&{ښ(2̨l BeLVT\seᇩ$Nq˗OQYPmZ3Ȳ q-h` B=U}Bt 7j(W Ik'#yiTci`i)2g ce?@tN6:p?FOț)4t/3GB0jE5hɘعsgr[SHm)SN2e 'N ֫9̶e@$熁566) R8D"UӐJ& IN Aɔ[̲Lc19n3QD7 ^?ޮ?PHJ4[|ff83W"2JȪI2?^@w 2@{o<|'9: db1G/ܚϜ H55P/ ąp,ۆn`3sB#g>3ߛ;s[^HPPU 3Ȳ8l at?gdt35XL~饗?O* X;w՗;c"uk'#I!8s'aǻ`t`6$EêC!u=X49+@~BmhtɨjooW( lPʔ)!Ad&qHxUq24ߠ$J)=F ?O+)ٳgۭzCńe ΄C^[2&qض 2+t l]@ h4kە]vU?Pϟ??mP)2Νkۿ{ھ}BoXfQ8$j׮]\TY5;q!R)q{$ː"$n:I$%Tdttt(TtEhͮp,88 nòLYd2a[lr[9[AT!Nn-ܒ?o3f`kf͛7w܊C2r!W9՝=N'w@ eYiZbYrG.Tt;3hĢ,]xᇑ:ݎvTLTMLw;B5VMwऺaNp{W,z#]v_}Պ? sךT-@ᐌ_* UBmT]@r7 Rn'`'c`^QcMSaYn]Q0Iqx JDzeMMM3 ~/'lK$ApI 8>%LZ^Xt?}-]h"w _;;wx =ȌR(BC2B`"8)z(;JݙR Øq8́ܚvyFՒGlHXbEbۏbqvc]d@-uvX:CxViHz(8\=R&!q 躇yp׮]aXp7Դ+ATr5XK,IBB!u8LB<( s4*8n4`Y ˂aH&Lۂذ` @cc#/o䪪*[UU뺣D"<Gill4ӭ/nv#*4CM쀐z" aѢEF1Ob1g?YԩS`Fbwސ{zO766Z !c!USNeA#+% 9y dI­cWF ӄ8Ł[(84rww׹()<+V$:gyɓGP;{!dMGfj9 ;,wT~>w_ݻWonn[sT҃2ɨRMMMm+s؎ASUwS`1˶a09ځC&>d M7tnҥF<Ǒk4Mq)Hh1 E&͛7wnٲ%Niy.zhB@p8g!u}0ݯlٲxNݻwcǎ#A 3UpBB!U~-%aa`"v;eI !lb6,`yпB.Ǚ;hjkksix|79r0y^{Zf;|֥lʕ%K֯_mmm{>x= YՠV=HBP+j?~LX,&oذ!7n/ Ξ=^pEm!!UVXAiPmf lAeH˲iلs2&8pG vK ,Hc$z޼yp 6msw/k֬ 2"3f`7oܹsgڶ{IXSn*tɧR/wqG^pƍ5S$5 dTEQVl޼G$.UUDe6P9G(iYnd@8 N-~gA_WFu@qݺuO̙3آEvhP`wػ}?@q4-]Xtvڞv]_23w:^zWW~W^Ia2n(Qr[Te SI,&IP% W˄CHA-[,>S/!{?ٟߴik chƌlʔ)L2)`\GOV>]yUW]US儐AᐌǶn=W_lP"ݓ%d\u 2ЁB@$+(M9G2Th\2rm*_ypSSSW_xb4zHkɨbkE{L9>@,C Iev![̈́LXB $IxvSqٲeB+:Oɐ!%#5Px s57\@>޽{o㜣ZUeS(i"e%5$RIL % T ^3M455E|M fE ڣ>ݱcG%@`s`FcP(^` XL~8X8SUg_3!E"VaC!cǽ$4|?%hjjЎfB C2 Vxɛwc>޽{gvDzY(moN@0, iC:,Ơ1MLWU\eM_u )I`D3>c sp.`$IJzG$YӜc. Q8,!/Xyg}_4uTv 7̙c766ZԩS?E VYfA4`X~ox۶49e˖WX8zr]wq!!#-%sm ݝgQ?V"4ay#q0x ¿xo6uR(;wx䑺V| bp"˰,eB7BH#˸XQXǩ$x]~ d2)Q0$8 e!eԔf,B`C$؎HR0Le6[8B[G0a͛;wNWUG"^&DQ8,R Fs?YP]x( hj C aӊJ\T'Io!!mr$R)(; 9jTsȲ q`6 ? &L˄la X#&]wϟ8pLEQxUUuQUUD"#!ōaz|Rs8a"qNeʔ'P @{aXQ*HwB )>fͲڴT $ @iP,{S m[vb:;j!>1A@ww΋#!ʼnaڲeKwjEņyP?s&@xi@ #U"syzJY?Kas2;w /]BW_mim\iۀlASȒ9cmòt0%l!!p99t\rIUW]+ԝ;wNCH$4`d_|1/+!eaiooW.ԚHμ0w[i4MCHӠ2Y*P `-_[&n]]cc#",\jjj[ϋ pǁÓl BUTH͢>ʆm  fߎ`89z@$!WZ3xm8nzneY_B/pĻk͚5R( qCv!*\vlDQH{IPHCH p(C O:>`2۷W\2JFs$cI*?eAu( g"KLh  HC4R?;a5qdZ2MSmoo߯(Źi& AᑐQCݶ=1yv(@߷a;w''8JTHHR$IWJ-,E)Bl9|{L*4I?p88m<񧑃>mG2ƪu0o^ڦMP`wݐ eH{{sP%˘]Q EQVnyg Zdxu@B$A ٻ\Z[[!$Νk/[,I*IUT2Tzpv Z^Pt@;c"oܺLW M6!q81x^f͚GH9pXDux;n t6ؙ91PZL[ @$BEa޽z{ !-\~SqL`ƅ K \~0dp78Bq_lY[bxRx$ea9ve m#2:E| X8 Ài9`3Qv!!l۶򩧞@N2&㐎O:dW8p!WP$k0|s GBa\ ePUw,IzV>4zB1ci´tjɁ;!# o=ӳu֪ȯ-0Wi!P= ?8޶mXB $IXjUO9lLH(!Gp"twZ 4!ND n˶a 2aB_2$I7-!. -[G֮]۳pB{?2wDYƅ­aA?f̘ASo{ɓ'9Ы6!akk0d $ 2M(ޙe{m6Y~[#PoŽbdҿ?u? mܸ){onK?h'o^yA_*|sqGZDp>3g} ҿǛ!Q$ۻH"h% G)$! "uhEV 'xf42M5̀^ܩ!׾`lPx$d};*Fypzx޽O~_~=7xcF c, EQd!"P w-G2(O|ƞ={¿-$j EQ IRM8ms`1`f:B;>~S_yb$رcGB6`Kcc9s k͚5G`k*^I1(BhGd߿_knn[ZZBO6͙3^pY?W\aٳ'#媆Ze!ĘOPp8wx˂pH fst0jx4MSuGJ$cLF?GGBH$B/m$1I # $ْ$@x #q~DVE_kk+v5ko|#VH%(+Wꓜa ؒ1U^ d6j}2a EhsE_U~k_KN6gG&{,gϞp$^GkZCF,6! g˖-}8瘦3/ZooeHiO6 !T{LMMMW^yb3pXf̘:׿ux&T!ievB6YN ~Xz:>F 9Wՠǎ^1 P0$cVqD<~UPQ 1(; 5hI`BE%.U5()ΰ|׀Hm۶׿up5%&ca(JojLHt@Re[ީ7|s.Vx?ĉ dY 6tuR(<l s<}:j'N aZ@-(WX$h$( p o p:!̘1暩nݺq-QC\9Qx,-{#[b…`mVV~f `xQU3F49 h} .uHK2$S!UUA6sY>.$Hl zE$\(1ܹ3<F)`'*_Qo͚5>UQmVo-U8 a՛7oK>OS~go4`HȨyQeSU_Tǫ", mC?q½*+I.'ऒp1pt ˲H8Iv4 ꚑ1}xߺu&;wNF,TXLj[[[UB968`8޳gO75]Lrx%LUDIB4a&D!? 0cm¡0؎[KBE[6p! a`Hb8sL>|eE|عsgGKpjEi(q`R 4mc -_vQڋ/V_ǎSrU ~cN ;,a@VD1C0`& oq0UQX) d ' D1PU@eh ŶH*Kpc8M&YphT !dɒ%͛7WU$cjP%"w(3aqsX~0ZBZYfd޽>XKoIN o$0v^y X=u.k Dʀs0ph @?(CUU!Q<]v`~(VBsss!!cƌlѢEƞ={ے $8u@QHH8@z t889~g3_2 ĉZn۶ﮧ`HȀ,˦,ˆ(=0RBvڥ(J\Q,˦?(P8:c1q'8Nmۓ,˚j$qcQX5缒sgmկɚ8͔'IQ xwRIp;B7p[gyLuߣ |؄y衇b$`a !B !#9C1$@R8GJ]QC[6˖- #f%,|_2_2<|svCTQ5E8pH PQM,IpRI(ٳTm O J I K2~0TL蒄NdZZBHFGz^LPU\(P>RC6oYQ%IXbE"_ϋ b1f7z)HzzkkԄ\kpٲeqZNȘQx$޵kW>a;,]L ?s֝JfW04Lm nEI\(88xw4Z&:NS"kC:CA 60 SPt _&:9ݫzfꕘE\p.jض 2alD!$NFoU_rg'?7.]jlٲ%yj R@ e!L"iP0aDm델:f~Hh!/_UUů:c׮]oX&w,,gC-۶ K$ |Ѯ|Ip؏7V755E`bG/| A@9lۂaRH&gAKP{8:::!!Ŋ 0BHR¶!M[G1WsӲa6 rk9@?wGm5!dts{gd*& .UUTpQ!c983(K/,% soK|B!,E.m  UEkxD &]}3soCBWMMnГHidBX햨Q0o:s ee]8>su$Uct]wt]gp8̼ GBV_ur֭UMMMÎp@?ZtRc'1(~c\>uQ\PqD2Cv wڈ9Ap֝꼿 … ͛7Wm8HwD;9琼n, Lǁ8靉7A<:}c ۶UMTLo UU;H$b#e'd }h]gŊ^z)|m߾}?P jPR8k͉0]uLH ӟNn[8Lӄi`lHPt"3B=VXCƄ;w]'893;!**E$ymLLI2SGcǎ_~yw;wNCH$4 1[ H92c!)o9BH^EQ>kfuBȲ&+P&{ \'kN~vS~n(Igo;Up0+2"`qyO G0.[,vڞ|S>P8!yF2$ bdIrg3Ƕ`&LӀiH$0McCp1`8{lyxקRB [ow~'wlxP.QUH238($ WB+<~kM6= !_Px$Ce˖͛hF0`ڴi㿇^;g]סdmǽBDHm88́\8{ Z*>uTy~x܄삷7b֮ٝ]s-$Tz[[w\sdɒT95k u6mD d};* ^̙cرQs|$^ɎJ[Lp9ple0 ؖmNzCpdžsG;3ggq:ؿG aƌ̛zb;C}P G2R G03SOES? nkU,˰,mǜ` w 8s7%.\0b,Ewy'$j l$o(A Gtb1]|qڴիWϞ=۞3ghH$ҥK|?B@0 盭z1a sت YQ!IˆR'FsGc;8C$cPBȞHٳ#!c'r7؀Lpl\(lu9vB #f͚#pk04X__.8Y(I,phve9>'_!2lˆe0 p%*'L=`N[/8{ ?pK/d8qB>|0~P8aڵ=?#v gK_y=P% n,IJ!88s+VHeG P[2;::޲+EBB(swlߵ$ْ$RXL^UUwOCUUض Eax%4-d2ιۺLm]^GF*><3+z^/񦕽feD$]'[`iSrXt?)¯J|rHTy1#O I_PQz"Ep[7lxܺuksi XrӘz z[Y"i %!2 !]r9YעbLl1c3)aΝkoԲc;wcP B 28~Ɏ?Ԋ<9S߄# h |pkjjL`0\z5&I$&IR1K=<ܹ3Uhe^hB%LcB$q z!fsk;mB"g޿F-Zd摢p؏3f-[DOƙ0VUP$hU BXS0OIK߿[ =\WU׾ix_#uGGGSx$w6)nc.arUULh }ښv6? 4MB: M Pa*'_ 3v ⯿7_a` p8+W/Y$iӦ_zoNtNt弭h7_Wd`x{f3g9>?ik d-)'#nZŬ.WeԸjBrzqgRC$ƙxB($ %\!ys:9l۶~ ͘1=][򢹹Yoii ={pBގZH 6Px s5B/0yx|7uhh<93n8+ \? +۴BQ(*$ū4wCꨜ|)z>x5(Daܹܹs+WTX|';;;]{@l󦽽]innz[m۶U.\L`'ן7r \=dã$Ivwwhkk3炋Ld\t q?&S$s!6烷_TFQ8$%mqiD"/2»Pq[V~a[4<:x`je&/_T15QQ{:Ps9 ut-TEBԆt cI[ ^f&~~'Fᐔ5k&_|dҥZg;wx䑺wG"?LUH8n88`89vQ?/WJ<`HH 9<ʲl8p jU-lۆi0NE-fb{L{.̆݇9l&YbH l!H604.m5|^)BlB}ShR.| >/dt9eBٝ}߿}d$dvgg~ϰ{5xR9l1~ k40~{m۶}v۫utFGϬ^z]O Sמ6[[F X,˶QZВI^XDB!O.Ӻ\9 r}M$T,ah|σ{J/A6щfDoSL߇jqئMR=;.=sΜ7V*==^ :x0JkAAj Ҷ` 7onk2zzs%xwqG^8jqcьeYPRAp:}x^-SR `t8Ŭ!(ˉMފhEx\v1Ggڵ+vW?sfg΁}t@kCfD;)|).VIv&TexOOOh,>TX`kw]8w\`ŽκZGf8s=Ⱦ>q`<1ׅmYp[׶ ,t:.~7/kIETG.B7v1+ Bp0SVJ7JBI $02Vx,X'`8 1rjy81xC)Hn gSWqibXֺf힞DʶOpfPzc? Ƕmkmc`HZxQHК]fDufʕVUL}bh%8<)'4F#J(ŠCP(e xJiUե: -xWK ZCHdzAēAԏؒM>>F3'pt$LM:RBשPZG eÎN?v<'5vܙQH|-2-RZN=T²OBƥ^?ԏ| _qb8q3r)iܱ`b@H (\\Ʃxg, +VQĚףxlmm2Tob1B#?Zklˆc0Q5v9)gՙe˖y0Irp"*k`nC%a,d1/3S|xd 4nJqj.bpl(:mLX 9a;5{l)I#ݻ$|-S[GBA@)$zN83ϰڵ+ϻcHr,AMxt^0 ͛'8pt 4.mۖ,",2̜NΜCpm*5֐B mXl£ _RUαcTm0/ ' yx?JA*5R@LDS͹\GdՑ]vŮ|>owvvYW_?Dj;|l7FO m`Ld_?do{٬~}Ns}vgͅG2 @B!za 2^ -s'w6@ A|у81HHBi Ƕ - +FXmSN9_|1ZɤJ&2LI$U \ξ k׮wݝʽ >4$O ˉ0P^dϾ,'^,gƆᐪ[oMFf>\;N3tg {)0<Ծ}@cmYp%$%*M6VZ;=vXpB 愻Ku":B}}C R@eLyј>}:_NW~D"L$u]GGڲeKwܑ`k֬Ztwm{{{c%u 3G|-o,gƎᐪپ}{28yHpf΄cܹs!^jOZ2q7q6b@BeIlwp8ϟ/zzz;gyl+\"D2}n|r<B@D`Rؿ?^yw1L&тa… 7}}}c=&3KKGf͚.'ᐪkORB CS,}uRagUضZ+yZ6>TBT(* PJB ?.,ۂς= /%ˁP@PJ7߄8seJСC>r |ZՕ`8NU9Pdf-Z߸qZ/ᐪTp!XsRЃCaH֘a0 Z`EQg]gg'׎4e˖yr z@8}R"1YUUR+((cIc4{UK.uCԩSN$<]32ؕ̔l :ZS(+F 4rt1#0K, w"駟N̟?ytG uM" Ԍ֭[޺uk+`8Y0RU\E\i8bYޡXE=vR)! W"[HАu쫡zrx W)g%[ 8XZLhdKЏ^ƠGث▅-<~O?tFR`HpHUbYŰQ=EGeQQh\ '4Cu!Nn5fxFV TJ_{C !U)B3\p>:zAP&@JxѴBJu]]]+O=ԩ|`l]]]~iz? y\LX%V[XɊ Y1+%~ O'/[;TT3l־뮻K>&CYf~ywX\Dž1P X Ku~8\IRuѢ7B JYre矏$XsBXJitIgB!A_F -Z7IwbÆ 0׿u(l־k;z{{c !UbwwwY!p+|Zk)wphۥp/DtOJQ-o;p饗ZlbRJ;|wV>kTL@[[Sѽo.N7شiS_J{(Sm@@`xUI~$OF!1da,]x_A;~;OO?|}Oi*Ymvpb8 NE8dtkp$b17lux2*v[i/EXmdɒ1!;J.Ooo֧Dž8._gZ Wcf}p˾M'֫իW#}3xf a+(w}~gӦM6Lcw##`-[޹឴ %_vTt`y"!5l`߯?h3_'?Iab89L&ޚ޾}{򧁏mokJJ輸eaW7M_h!ot|N䁱X,~3cdz.+V$S_~yO6h o$Z/അ]8D笷5b$`Âm4 O* Rh %8ޛE=X/fG?Za8R :Nu)(xO<11Ƹ..q….+lݺk_Z٧;w&~_nfl◿ٯzz~G6mڔX ?W8&\{C(硇jݱ%{?㏷r-Xhd>jǎB"f?u]Q"lZю ="|e>1 a0$11ㄅf.޴iS꾯~=mJ6ޙHbmӃZeouS6osoc᱿ 8q ߵkWLEiX ۶Ɛ ^z*p?e[l ,X)ez'rm !`|Qh۶K M]Kx,KVf+s>'2W~OSqՌx{k 6|^<0$v>x]+Xb:'FExs;scimΝ;=@GO ,P|s ucmʲg+eO?;w!M66n7pC+(wxTeT**//{7}饗cx֭;v$niv-e\F %|?@wc P`1yn yE EkxqqĤDL)ČlE ||3{ˍ'=~Bmh)8n m/+ ?C>C!CP@X{Aa@TFkcdOJǏᰁ9aoooQ׭[ڵGѯ )<dΝ;6(OH$^-ˆv wjm$ + gil18+</^@aC1C.7|>\ 18 _!ϣP#bZŋP N+73w~{+||1k,`~2Κ5Kvm tvv˗纻Sw}wƍg]HkZc}1yXlT%SҰ- R)pZrXVй<Ծ7*|K&`M/v\ܹ3a…v0ط g5²,n BhZp:Y h)C)B>16m۶%v.8n;00#%?_]vntoool޼yf*kyɄG˲4օY6:};=:;Xj`^nHTXIbʷv8OWZU&rƍ|Gh9e6d FK(g%[lˁ Za8/pOrjb8sl־N} /jkûZpA*3| >/H믿c幕+W_}Ur6!5t:?7n/|!|g_i4 %s\ obJK#s}A4RTxf1z,PYjUghx^%,k8aMiPIN)bᰎm۶-~Z^k;>qX9J)ض@x[,bmc$.=:;<2ɤ) !5UV;[li&[m7\(]ѮSQ}Ex?! 4a/p)w:p]WR)N/7>tK/੅_¡ާ1߅e(R1b*Kp׿:)l0֩> /m<N)(zbn{iPii;bq<\,`@)P.boݺuYCj&W]uU;H{\rW?@ %"hX+uŠ8a%K P BkcP*kT4\|Xh{tG uM"T*%G:֟soi` L; v%ǰ4N%?@86:pX>'2#5:L>̙ =x \dZ!18-أ$>d2NF4~dOOO^Ju ̵1R"uJE'H ? R9@5ǻoRZ|>G.l2O+~]ubxߺpz |2/apOq:1֡M6~hw\;ށsNχnOAk!DK@k<^۶mKF(Fe1ʕ+6z8|ӓ%"ZfX̍ (O! caL9$J{3[bRJ;|wO6<~}Meٲey睷o \4ħxj^7_Ybac`83}}}}_j;|1kXsΆ{\l z(|6ۆkp` '_vZWWɢ/Y۾}{j_vmt</^tww"$:I!.\Džm[1PJKO2[*cЧAeaԩSG0s"K/꥗^ G<AgիXF٩~oڴ).!?p11֙z55ޗv:bm{χ^}Vk+tWyu1qxR1p^"=JUիoߞܺuk+r48x])<G-ZC\)6lZi0"f1N$ѕW߄ }}}Y]K6l(}RHdxUVVZ۵kWK>󱞞r_h?|tR` >~PJA p ^@0B*|\SPZCGƥU$V;bq2TY~xc/lڴŊӷ>tcXW)8Z!D%u4x-<^uUk< 6` F8N.\(as(6[Z8ܖV8X vr[':v0ږ,ls\P LbpBQ .0-vI6fH=Kk +h:W"wn޼Vի u#mذᰎܹ3'ᨡrCHFݶ&a0B@qR)(Vhmذp-U HT tZ.tww~,X ,7 y,K$q`S*% 7! Xڠ&Ai|d=bx$>:k90ݶ|C"uU~Ps^d}*Dx<+1 m޼;t`@)qÀh 5/Hg@` ▅W_;PK!t6eGj8 uH w] EcGG9 ,*UNA|.9*M#n=cpW²zԔ2._RxUm;nxܞ1aa ¢En)[Oի_ґ^/y믿ޮ&p ueDXmrk8NygR ыQbeJSLamP(}c p(>& ËѳFc8AWW_:n2c[.uV``XNu;={(kx u687EkG[H{m߾=+ݖ{* 1ׅs BHH)P[NA(R4/DXdJc<&n)я~td+k0Mnt8ao/E1&xX/*{{{cpw?H.X@,^؟,W*1֙s=Wl߾=XARxUaԺ\t[yB)JƳQE]48:ddxNpGTM.+lݺuÆ S/5a*ׯ?İ,Re1<0R- ulƍJV|woԠxNlQ̛7O\xd5g? ^ڶmgG}]իk^B,c T5<ڶJ);J/c__SNs̜ =S;lȗ_)I P@ush*N Y q<׿EaYfК5k;w&zАv̅j]>CA qu X"ݝ } ]vYaBTccmG+<ܹ3: H:oxE?*Et:-Bq"j10۷'kp؀:;;Uggga"wT/|,8.\(J_Rnǎ;wK/ycڲeKwܑ b DcRx{5u,K)V@BAbRJ(F"Ayi)$ΰp{!"<]ChjT=#=P[gXHBkm -^U ۷oOn߾=Ȼ曳u DUGݘ82<\t˂||R@|c}U@)ãm KWOwB?|lMxǹs_DMS`{l~לv:L(zvԲN7$%JCEGuXc AumېyG0"@X[K(!Qx<l۶-uvqZ{|"\Dž|-Ym)p _g0$?->%qPۆ1X 1ׅ: JA_x{l0*GjopsHDD5)O?f/~/:NNJ=V- ~% RIHQ$'i E}}/|mXCi !\ׅ$*!/D_pXq+hF4ݼ`C"+ LGgzָ(_s>9pNS~:SP.$xE,He)O+W6s=Wl߾=kxB ifZ)(M' r8^hBg.ffG%JJJͨpai !QL2V v`hQEAJ0 ~x|hCJ庣~pòݻw8Mkt]פR)HT;G ?^߼ys[wwwE)"$phe˼^xc8$":ŋ7nl-N͛7O{G4Ne˼4"8V DDp?Xغuk} ZhЁe[0ڄ T B)Z*P> awTv;QԩS|ᑨ:v:r=`Xu}8u!D %\!`p]EiTPYAK稖F_(e//ȵ+3T٬}뭷oߞLR>pBa8dc8ވpԐ0UDz/y믿ޮmaUشiS꾯~=5RwDnmE{Hn~o׭;l2/^{mGooopt t2}n^d0$VсBJy[%,ҖehU3?ց: ۶!(z>ޕLBERxNjR[n_|Eã`8ë h\5~ӟ&`޼y _AᐚFE0"\_`HD5URPcMoRܶ6ܗ=̙5 ̜ 50Hs.lۆ h!"1dc1l/g0<2Cjx97a8~ a5ѤfY,h!ZXƘDю:i l?? V>gJ3{6LS00x ##zpl-m . _\9'!5,C"qʲ,nu'lmyqA*k>Sr z 0SNn,\ׅ-˂c \O࿥ěZc˖-,~+CjH#׀h :<?u&l!={`L=850 cp]'w{KkX$- bq<{ؽ{w  p2̻1 E8buա e7mdڧ@l)`m- Zkc`Tv+ƟǿֵkaCj(LR$`HD4ުڱcI(Bϡ|8Apc0CCRB+m `YhcÒ0նn2vqcaD0Mmwޝ鶍@Á{$j`G"ڵE Z;Yv7Rcppˆ`‚kC"H1 4#W(D=R,mi=bE߇B R Rk(c( oNXֱi1Rd2`8އ0QS=1,ۆ1߇81PZCH ?y ?PP@52CkLF ٳ["Z_7ߜ'(M>LDžmYMt+]PY!y5k %5<Ҥ D mVSpz__OmdC~{8XN:1-i|?`ɒ%޽{dǑC*`0$jhl cYpwyg{www @:a;8ݱn0; _ohJ! ?ne˖y'}1M&YS"c5k -]Իҽs8E~t:͓Pƈ&L&s)GA ?p䐈 D,\P|ܹ3SO%yx.`֬Y .,X .1Ҥ`HF4\ Z_՗NY`Wa1Ҥ`H@@4q2܈`x GG+GfF)e0$j@4\ CI#T D́ D&܈` ōfD4 C`HTi°9qZ&!QsTC'&3 4F ōfDME}!Qc0$j 4.XxK<@@X!nD0CÑCL&n~w)5(n4#jL T5'!׌15.C 5656C:i#!QaQs:)LcID D̓NOB jl j.V[pHmD0Z_Uʇ= t\2=`-QcQ@p!YECpnH1a-Qs`Qs8Z!ӈk`HԐ@@@ie:v᱁9F[0Cz Cp< \sH K?< D Dan4!U$0J135 7xDDW@F3Cb0$jl j'@5MnD0d-QXOa05Q#MD́ Dͣ 9lR* G8r؄xQs`Qfa #\@D ͈GOB j DaI$0 9l< 99wa75QnDT!Qc0$jѲG8'f8l@,%jl jь0565n n2:`0$jHl jh a~xvfD D͡V  D̓ D͡!Qs`QuֱQ!\ CÍfDc a~x^dQb0$j#uD41@@<&SG̈`x!g"j0l jᰎd2D4ьyL:`HԠ dn :0نh|d2 D oo4$7by= RI75 I+ p DaIlpgDTl j@p80565zh ) È`&y08r8iTLuDt@@Fi @ 7щcQsc0$jl j@pXC##x  75Fh `8L&s pC( ܭ\ѳ`0$jhl j@ Vhl@@<p5:":6n4#j@p8A2=huDtt D͡'@Sы D D͡7qC8kuD4Q!Qi`0>C, VWЀpl j@pXe0LDp="j0Ͷьᰊ CCkfu!9@aUD?<&yVAԬ@@Fi h 'Ո"f2{f0zVECf㯡7pQsd2f1$j@ au׀!E7 D ᦕuD D́ !&^ÄCCh5#.!QCbQs ᄩÊ9 0JOn&j#nD8@D `0 D́ D̓ C]9 YxKԠ* D m臇&9xV`H@@<@0!!5656LNuG"QF3ɫ.!15hWI25(65U/# D D́ cC.EōfDaku&{8Z"jH D́ gC5F7էzڐBD `0$jp ᐈ& odV&`H@Ph܍8 D ! C`8h\ypC U!Q`Aa8$$`0$jHh֘8LDUW{͈ab8$`-Q`Acc8$6">j0564fXsx> Ϣ{"L&1$ǍfͣQG߃QI{͈ֈ`5`0lX1rx;^KYS\B oC&y7o  m݇>p 8`-Q`Uf8|o=HD!Q`Tʕ6#[CM5GoHXkF@@GR͑ҮeC`HF4\CPcCV&ܞ`0$j8l E8neɍhTwiS#[h1V80q )DDDTCLF o5Qa8$""jQ0=zN!UG؈`=&o DDDu*܃`x#z*q2Qd2a/CQ5\Od #,QDEC*C"l0gbv5عFt*a2Qc^ =hkA@u #,Op)G5ރS&Ydp0 i05}kx#o{k>;Qd2 >:Z_5&CP z(M[ WC1b0pHTίZM1o=Q`D0|b08w @µODUd.g>f.w9$OM,D . j#D ;gw ߃M#} MUY c hC5Cp~@F؁XbD0GE4>1'u#0u{>LFiFXKFkYJđC:WYa3G|G !պX׶3h,!Zi jJ_`7uuh2yWeF'rM;i":zLjB DxDVC4ל.5aA``kGxޱy $pHTU#޶>08~DD4}ދPgQ9"":A8,U {ᐈᐈᐈᐈ]{+nD':.,&""_؈֟Exzgpuzc3ƜԍIݎp}45\SE5R=b8$"":!" ^5܎Oqo ! )B⦊s7+Z_QkZ_ @DDTGu"RkWa8W닡pHDD46c>u0L4B`}lG^BZnDvONq䐈1GyPQc?> `*8rHǃˇ18}k;sHcpHDDt\JZ_q\k(XeCDDt|Jӷx2/P}`8$"":>]+ YMc )DDDcWXpw+2Ƅᐈh'nR(?%tEXtdate:create2016-11-10T23:03:49+01:00#VJ>%tEXtdate:modify2016-11-10T23:03:49+01:00R tEXtpdf:VersionPDF-1.5 \ 9IENDB`leidenalg-0.11.0/doc/source/figures/slices.pdf0000664000175000017510000007100715101156755020547 0ustar nileshnilesh%PDF-1.5 % 4 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 100 100] /FormType 1 /Matrix [1 0 0 1 0 0] /Resources 5 0 R /Length 15 /Filter /FlateDecode >> stream xP( endstream endobj 7 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 100 100] /FormType 1 /Matrix [1 0 0 1 0 0] /Resources 8 0 R /Length 15 /Filter /FlateDecode >> stream xP( endstream endobj 11 0 obj << /Length 3535 /Filter /FlateDecode >> stream x\Ɏ$W>$r0`˶6>:-i,J 2\f]0<h]Q ./IO=w4J6L)x~igSU|ʕ&7**__Ǣb MI*ؘtf[qS%U!Hƭ36Q&~½b_f~U:z(|v$W7NMex~oSVnZ&`V?xf^K7 |.?*9gW:GbЉJ#i=H+‘=/?<^x`? J:i. \,Kʇ4g%*cM"eO/:lF9^U1{l,AMԀ pVF]D{D=m\qt*Zύ4 g7Fx'[ۨKގb9۹6¹J.ad;yghdKLFQB3]C8ʥcr篟)uwFU2쓔BZ `yV'A\1 **@<ƫ21풑vD{ߡ.nHgtB@7iIbAI \%8xm:xcEcG|ᆨt DNz)*/\ڔ"1N$61ʒ:9Qc:eV:0ͲSybW>;3UWp_q0ʒD;ߡnkBX)Y<U3GTqAԓ}ywъD9t0PE2|kPnt[Rx7/y:KgFYbmR5:i*z929GC?bʤ8G>̝68ЅSK.pt2d95FFٲOJR¬!u4]q^ /iDp#~Y}l gt MwǚTl]N!ՕyA Kީ( fj#[K^s/Ls?ӭY`la%R~d(W ٓ5 ,KkHVt-FjJ1PV.,W`RUXK2 6亥(zñW2Nd/6YB o]O<`m^rn٫jx~vѥ&!Yi:c \ 8X0XWb.as#a\2(9s$Vf ztn2,0J6($f0%Ԏ#jgQ58;[mxvsA+C2.9r $JL.%lo kΦB΃\Dw[SqX70O}):\EBth`3r$;p38y$jlld(k Cbs6.JGg׆w/ĈF⣟K.,8WiK^аXbEz?< q*S*U. [@.9`hib2Բ$g=IT#Da5*GQZ֑5DxM_L_;b#*__HSEa\an|ftpu2EzÉPZ&xM~M-ÿ5FlkbqgښH0Zh)ƠR6eH喂ZXd^3{D*; ,oZ~tAuMK? u endstream endobj 19 0 obj << /Length1 1578 /Length2 7625 /Length3 0 /Length 8653 /Filter /FlateDecode >> stream xڍ4k6KkD`D'EI(!ND$}[Z<{׾-]Y[5X E%꺺@(  `AX,`;B!e Ľ: Pp E$E%@(! .PyBlU"sCypEd]p  P!.m@] R/ŝxA; jr/ @Bf{3 uwڂ]5+_܀O@;ll`. j8Jjo7erv>o_coT?{mf&a65-ղ^Zz.M9JkXοXĚF i )NT?YUϱO}'μjR`9ͅ:y8ұiwĪ,X!2BpݗM` K{=hpޱKl3/fI]fvHk&Qsg7&<+-Ezԇwi[#Jdӹ\ 9j;>>W5&BYjZ%5eX ц]%'&zH"#5irT-t{6DBHɹ},' HaqCjhIPjsJ[b^2g-ΠSy<^rxKdp7s:\;YW";O]2åH&WFQ9윛UtXYJhSgHjS'Ԏ<16H>-1;ԆOFz)fod G<^}IScB9?zڒ{n]Oinxy"p?ߕ^(S֔ac:p;< d)Ky,$bǺh6={(\3۶1ʷurNS?M \.AI.ϲc#ؖcEOH}\6*?>T% tFE߽K-IXel-Ju ;׉fXhΜ^E 8-%ѶW3ԫ%(a}¤3q$=[$y3trsW c`oo2j3D",yKc;/d#IfE>L zQNˎN.ǠLMoQLFE8iIb*xg6ʢ04錤#ƄTn?wrA3ًפ9!p\`(T]!|f} SDaw];A*.:Kaft{2FQ& opVo 0|.ڟm_h#}P'  I9*pr Ckk^% ?}(Ʈn 7zEel+dEZ'eAߪQH7/o7 sR\m7`8(YV]nvmA#2I)6B!;LOnj"C"F&ZN&~r7SB'=㥰ǖf]̋HnyB#{Yգ"M?3j"} L4F!vH"Uk6Iܲ#Z\ܮLk{c~#J\`2ƶunOb#`9ā5Qa~P]WM$ġHFY٘z(v\*A93".ԛCng񭫾'A73J;ijm\Sx|9V8&t"f!JwI'de :kD_Ep}{jUz]K*lcg"o4I&M)%ǂ:iD6H]d$N| |{{ ĹgFt7CZ71LQ這}JQNGlk1h#90d[-ҧ LbM }O*L$ևb%$EF 桤uC@L Jx9L{q~η@԰^4!cO㦃Mfx坳ʙK2ΘLQ kL*H,:;=C<@"Q^Vg*SH%ݶoCC+P!PkN[--2-r[cLn?XW.ߡM(xs#X4]Aͼ!NpsvpTo [̉1<ƽT=vJ8 V&7fxA2u-,|Ǩ+cw:m``C=^S_ha`"^~,UN&.r% mFmlc/ R͓q6"PszsS\9\zy.Շoe9WZE6arjㄎzp,>&\H'#"Du͸5v)H黖dJ5Hzm],YBdO9+û5̤(i҉G\~M/(d 4^<}"*E]*OCI.O[EX^NՇ8[SVA qd7Ҙ+J)Lg ,צY&~w`4ߓUˏw7(վ!ݟ*tW_^RȔ2=h(]([YהhcsF v E9~13YTNIH)<rJ,9ӹ ˰!jKݑ"v3*:Cf|Iv>Rq!qEs*IfksGi/QT=m<=ʿ{ґ7p.ĂBG@{Y67QjeяCs,C5}ÅkgQVEϷK(&rto5݂dFɘA!}L<;f @As8؟6!n4"tVB4:6t?te?iE5R .4zTg 9nwV|cO e65UɧXdٌS"U 6@dQ\#!g ,r]Ӱ=jZ]v/6R[wOAI]8i#VfNmWn7EZjWaMեIɪ,zBCt:K8&uD~ZYm0:b8 VH<cf,,S;q(%y m򌽳ol\͌wmaP H¶M`9Ȧ;WSec]$v6yks3]<(b}fnRʍ_/'wXLԥc;BmX)D9ڙJOCy߲"ꦻ>@8JM##⟘YV}X8]}SM0 #N$+Tnӝ{䔺#[SHֶQA^-rSd,.?Ȼ}^%TYۊ.{/ \]!BVAa1~rU A/ORZ)pyvMӾL[m7𣮱lBE ]Sk"¸<)tF1&4F"a\ܬRmtrq/*7u-[w(K +y+X t 3rUշPx׿v1/Ҕ2{hn )x} ӎH|!䉳.9)MATZvLq& g#pi31 n,4ixadDZPBvțfc͖x%>JԮiDQ1jiV6F"xn+*=٘z#u6gT(LMWqOV5 :jO?TkfNRB0*z%X6Q*D{:k!y b=3Qx-?*<뵓>ЎsrTY݇ Z&r'N&T)<$R\尣>|xrRzo730GT>+4DtbT60%RBu4Xv]sg@-s3-Nә 9>ύiU%KDi'mMVE% Y=^r[ %y.0pX(| b%'PX؃޷#^ #Z[|N{$q)l,NXkIt{Kh!煞1M*DۃffgqwY{zy_T,Wǥi^:Gc B2売T` J|%تxM8f=Tp!;WKf@tXHn׏m;STaxMǿmOGOĵ^G~#5Y@خi-O<cp˸@AHuO⍖Uńv&K9)f~tpB}gW/`Cv֪gm[ODzs]2x""MK%O7&/ CQuuKagbɹS*3?= ^rh =/R$~y~3iѴzA?ٷȀAsKQFe͍7K1p7pyms3 ԁm!LCHY'}Z Q4S!*q2xZ#|eylbex'>jB6Y<Фa!ǥۊ77-nF.p}_?"dWN=RJ=ɔ Sšk0yX-')rud׾~|Jh׆QN y%qw?C>=fT' 6]9i'u<8{8rgb0zWřy&(ύ endstream endobj 21 0 obj << /Length1 1417 /Length2 6522 /Length3 0 /Length 7486 /Filter /FlateDecode >> stream xڍwTT64FrbKJPDC:%Q)o5kusu?km6f=C>yDG JچwAA~AAa66#LfDp0PQhL Bi#+ $K IH ‚2DxJJ`oh $"E+iIJJv 0{07`WaSBK ݐO'Y.^$*ATOAa?!hf#^p'>0Tt!?Z xz /_``{{;;0WŏE`/C+{a`;>FWyH{O; ɏ*QWt77$bnɺ>GW^pD]/4Do  k[FWpE@`A P^TS"`(;:8h ~Y倀|̔yTNA |b0 !.3WU;"?٢"_3Zo[ ڣBgv1Wo5o\2@ ^mz mj kQ`"Ý\n# 8P?l2W@~]+Ыe:hJVAЛ#_+&,&==~!%1 @$@8"< ~MMh7"* ePPO h[G2^}Mt/? 'x?ph=8Ѽ/Z >4u#[uC֡z8Coѻ@:c] P}k |g߻ ZqkdI 8)"[5.ۑ[$?ncs:*N>3V93*jOien<~?Qh9[Z^}ۗ!Hx[44IH vcι9O+Lٕă@ Ǝp'K9ڇ|*3N370<ի-+m) :WsZV,''Jԓi05 "Aybk0d8qs{K2OWbj %dLcG1W3Hb˩sHe.!+^A Y.€iTvQTInz7VV (Ӈ9O\0?d)8/_z5#|o){:Iq6{cr۟L%o6W*oUa w/+_m+'JC[%i}U*LծXq::+e@Cq7nK]4*/UwoMNN X.Cb! M4sIi{ 9yX rs$3K*S!wx dˑl wBL>}o >),y͕X#Q]zW;yc Kbiߎ+jޑ3V02gmí43f 65 G- wƪ[lYԏg]}5jIF&4ޚ iFUT\TrX$մy$&o})A$kD4ہmwQI֥ƴb|oȦdxH>lS$yTLUG/܎`o^`!`ujOC'Ke:w^gޢ&fW-)[)/pgj UMw$f>tݛ1JמXw|z[%`V8b.zq#qu~_K>>ڛ{)lJU36uXעcgrj2A"pkzSÏ*CZK1E#2K W8͐W$@3؜,?k y?̤ew.LS` tF{!9%~Mjy,8b5.fMj֨Y"bL`eݎء3s+F'ҐeQ2w!5\iG֢$mӈiY2MTM)Wokዼ*Fe"糼-ArOΏrfra:U40=L=28SV."X K43ǸI=e5'cTr j34yrBј{F_##|-}U)#2 <#-qqZDwHS Tbl*XŖ G%EzcSpQcJ/76q-Z,d (&'pTrƩg(li e#Am;1DHwS)Q7&DIGy)/,sն'v`Yfj,dIW Htjn/{Hq->ۧk|gG9B *Z86-\U8ֳ_SeHyy&[}ky|- YzQ05b887C$o8<Ġnl UBxwo#i_]T^K,w#^<EDc@VLb3S`=:QT{z[oN%6}9fo*Խp-Ur!`Ҏvj) 1&wꢄߓw:HR',M(£2Az}IVBORa O3dS8 3 e'Ba6}`B' !X l\Y{&_C"&};~EOa/Ȍr`P/=xɒ~蝌ǁ4p]LXrlW3'<Ilp+Y,Hv)S"̩rT,G(6?')@ʹ|_^sH(#IB]8(~rz!+ H96c.q/:#ݙ7c7Ԗ2zkd_LqC$ ;pVrI-TBn[8+RATt7XVZF_Xm|cY HE%Z>ڲp;L;lqNr (C+4o6M:(.z3s5RJelL8y;/P7.WF;yF1Db&+vd*qʶ7ک5.jiRuWz'U ^W .A nm;:}6+)+{1I mNlK1Iܥ*M=t`}lbƖsrI. <_ch7k/=G <, ce6)Y`}W5,U+KV*œ$DooxR81֋fɲzSU/2++6޲3 Y7&P:R罥a:Gwc&T%M(NK&&&\eFXZ)ky$]|Au- c);a,mk/h^Bی u"dGENiX F 3XX{E_!٭.匣qFDF G/ƭtq2#UNng 7 ?]4a~uƔto{Smq:Szy&wz%_Utߵn)J%8A&0#&,5=;e,^En( kzJA`88m!]xb_׬X?q:V[;]s?6/ks~t ]>V"IF6 XuxR;'h/,*HA j;˓A*T(p1_A1J;tiEYyU~)3}wfUcxmZWq%_va*}Χnp[yW_~!rݽŬ>GH>iiwleqYZ6hϓ,G~ȉwCmG'ÜLvG3S/~+y{eYl;fVԐ5sy֮8FU;FDDWuI J޲A_B-4t /wȜ-!ffOfƓ Ѕ5iw97S\ޒVŋj-*xOo$]& M ߓ^޷"hBq*@V.qbUoauwKcSҸ/øڐOOS. SܻZ-V~_M { l|sbye{⊕* f*q7u]h8}UX+\(&׬O]E~p0%Exܵ D@peC9Hz ~=se&y/*u5fvvm s0(qFx89R6]u}c׎n'*־e_øIp^(/7߻.Y'vO<BОnM+y0655u& O3dVLGa]) Hf}W\ :"qВ2,#-nN~RRcv!ldS>7&o[e73eJIedT .dAw׍{a[1e2%\S鋦ώg9tD-^f\'s}VQ8ӆ-MdM6Ab 6|Z!SPij Ɵk7|{V1hә[g޴OMy7#qE: #0asCbS둡7ӫϛgU̖Ż_kfHRLj/%syK:?W0KՓA׮<[T̆ԋVEhQg^^ k@}4BFٌ#܁]*' wpEO0م܎wk@<<sgI @Q񍶛獑oh><56q6!onлg1 c~7mVvR|ΉO-R횣N9ܲҟOͽxvF!a˱LF.A'U`!/ʧ.,ChNS_?[YWNx`!Ye@-o{v ڣ8Z?hGO<*F] faDx^ 2=]'ɿ] &_3%?LV"fyc,`CR/*Ei|6#vwGՏ`\zR;MJZp>9!PiUakn <{%fV7PCӶDm+7VӉ4.2 eR-eiѹmѩT^Ff5QGָ8#5!<`6:VOy5ڡG9%mS7fQV n^nBq<|[Tm#*oe7 bw3PDyF&~ZDѰ .Qw^Oạa֖ R8!쉖4<` ¯{8}s=i"GJdQ~lqUpT;]Ui2ihrCCsG - XɆeꂂdalzr $5[៍/tTp<"~Cxw枌Pq ͵O̭]LW֧O怔K_ܓid%ҌK905Zz'bɌN0_6`x "_a%Q_>IQm|%|Ty_.xjZĨD=mڶy)ƱY$IkxgM={E3 $"`$M x\oX<+  ]N_Fk.8לl%I> _s34*+Ո!Kkf?~9GhL K1LLTz3N> rty-#Sɬ~VXUoӋpǷo%OS+)]WYK7.,W::W= v 柦 #P\ꂆ.mmg$tD$cq#-x`5L}z=nN5ȌEw" "sa]sZSIL0/{ILdM ,?A[PRu<?{Yu({DH,[O dNnjo? fdυ3vN`A MIʔ++7aP.9 s|d\l^d\F5bݭοn9PsP䤔!+Ĉ4RVTe6&.xҵ\Gm&PQ1RF);Mbp[wne}VՠN-WC%A>HM<&nbG> stream xڍtTS[-JׄޤޑNHH!tHWzMzAEҥ#(M z1j{y8 M`hG: d=-)DH89MXwvNs8 F/ l,FXKȀ%e@ @ ( pv @yp@ P;`"Xsb=d}}} H/!4YWE`]cF!HфH8S_8; GyBQ08ULt8/_`! BHrp@]W (/ @Gw@]&3z y!(+ P04 GaH~{\7 9!lBxzõT`p&lp, @RA]0vq3z='`GX7<8;}"?qf_g1?0u5̵5?Nee(qTPD․w"CO#PNh@~qw>HG!sqԅ<0$oW'#uow~DA@꿡𿴫!BprPB9(-ˈRGa,/e76w nBoNbP77 G.8NA.a&".@0? nѸ8iMe@XB܀Ck` e#W^7}g8%@Ce#\k#N}W]qrGDmORInGr`u;N}eϓɃbG77o:&M\]K <>AٌWhHU ϐ>U̹τ2w*՗L]R;M,G'c UCFOxt,vS*Y,ô}rDadnVQ9&QVD>=Se"&jw wP 3+ŝmls_PB.7^As;/XH[΢@bL=PտiTg|~Qk9i~V)q|$sJL)QxuzMeȧ&GỴ=w A@J.@3NA+ϲ`S"ں"9~yԘQ]ٕ] lu/)=VI88k^`}L6nA_ČHVӻcMu;-*HYʿn#L͎q4/W."%gV)].e.(Vcx3kPpSG;ZiQz—*2%4x4FX\KYYk*}Y{s1vi,nL`uR{?;A%2 \jv ɮex8=9SQ6QZ5i;VB:~ewL\ocUL}=w}WT6zrgLV–WL4}-:)ۈ:[s UIGa5ymO-rUMz& HhKgj`ԓQ5VÛN1}=En Fe)p6?(x}H8wH)t` ~8|w.]|9Nz]52zڡww6yen(Tؓ|/K2q`paNi`^R{Z^[T -cF: ҝ>;j„+*&h*cց C̷\#?SO{[I " 80te,_mz+ՔlʑGdY[!uc sORz3lEKT vˆҪ o揫uO[-r!4n"gJ79&T1WF9Ax{40(]2^kW[k Sxe@krt6X< 5w&.A{Tٍ6ܥq~9O.Ԫ?QwK?fS|xVq[nz%i5zY!P̼q>)8z~:51B4#0 GGNde5ȡLNp'J/{7!|Uf45+2w[yF> Z'MԐHweI 4̰(XkFE?*$`K?JY{`~c9|m;JF\dntE:jd+bɖ1:Z4QbK~>0Cцurl_}wƒ=/鴘C 0;U0fåRy }{%zFړ ^6{ V0}/bg1;N$ŏmkί[T v0qaN$"bi`:x\6VzK#)^?og=>SRy|>,\LS'F"A#9)rxv>._Љ9" !FO 6?c+"̛eS5$(ґuS*Uu:wO^3ǜ&udDvT\EcB+[2iRX>+덝^d!=!R//dD/H$8 o0(h[D6IE ֱrY[<ޕc7V]#:ҁ7ɮ(G(ī>]ۻo;WC*M[( 7]][NH^X~~I>b'X  P /Gr^&Ծl J@Kz8 5ZiVtM2{H)Kr쟅s !;HQ>ޚF%dJ1a&;I\jۉ]2Kl8v,Z 0܏{2 #TEfL\@'#NnO9'/[E7ϻY"i ԛMY!];|zE3ZEk2~]}ٚ.&s4{?ͦ!@~;kc3Gʹj;x4Bg6 Zs/-`Uʦ'Xxٔq=QFHƫ(n9RTKD3:pC~I?"Zє0:gxg?1]r&:=vҪ);1nA2߅!7=y–vWx,:/SϒwٰQxFTmӯfgQ*&Gl$A>@ހ;[IsPƬ|Zb'f`H9FX/,ʷL^?gNn 8X7=WM#زRp ;:%VY MZkqٙ.NgCOoy@w&]z3?, '5Xe ZNV{1osmy )j+=a)?ZGM4 5fȾ%'~"d}_PY''s)eFSV؛/K,d1%}]"|8bUbb~6@ W%;tiwe}8JM`^̧)OMZE6<ta7HJM9G3(K$$xt*schi=gD[{+˟1$޶ AԧwmVWGcr,N_T"vnDk[P{yLxD)6OfDtR 큆ɞbQ# |ݱ-DmmRRDF&c$0ϺVcGOO\gjM2wZ>ba0~\|zb;ֶ,4("*Mr#G5n2>-m`oi4ӫ|7`Q!MBjN]QE4dM{;ưu=[mIi=M]giy¢h}upsW'7%2ŧx,&1W!r*X;8EuW*2Rk=mLrWZX4G[pB@#l E-1Coj@-y1*PmP*((Ū{}᫥/:< [Þ⤶hWx"tF1흂|=ql~C^m۩!&𕙟Rk;G-e:`WxU/AeU-{4ur-9.Ou]2iZ"eeA&CSoo+*>|}v6 lYYH&؛4L@q){;8*"1H֭-JqSbƔ($ݕ[eT. zK@ vm]Mϟ2Kf8<W6W^4 Ʒ~T|v8sXЃlAXqnlLyfߡ%umg:?_iB]+w4. t% ׉7B;ȬD1]7qIB9RLS>hlM8]k&M&VܡÏөVKо-~q}XKXfcVJ6B  I佶6ط)ZZ>DwPL3[ JoĐJu7 3UO^C wJW߹dO5(؀,'wW\hU-m;LW9 }}CMbF.S,F/lK._G_a63eq՚ai7v>n\F\Q;vwyˎCٕ࿜dZ;W_Kh 9Nx\8ӽS~02Ro?"= endstream endobj 26 0 obj << /Producer (pdfTeX-1.40.16) /Creator (TeX) /CreationDate (D:20161110230320+01'00') /ModDate (D:20161110230320+01'00') /Trapped /False /PTEX.Fullbanner (This is pdfTeX, Version 3.14159265-2.6-1.40.16 (TeX Live 2015) kpathsea version 6.2.1) >> endobj 6 0 obj << /Type /ObjStm /N 18 /First 134 /Length 1091 /Filter /FlateDecode >> stream xVMoFW̱Eaèd[85,#u!@SL$պ3KʒK;;3o#<8m@p:U0™B w,HAks&V`N dq-A TIsq&٬(p| hSt xvZ-z,Q1+JV骞50e$h8 _:^y[Tez1D|&ߒ+K'm)9YI=X9vJSz{ھ֎GᩏNAۓ/Vn4Gժ$@Ji8 )Pd(˼4;ΟP`֫_E$aWuOBt߅= HSިAGOJnH3.$= ;8"!ҝD 7=z0<1BEAh{limr^':4ժC=':.ìFSG@3d5&A;o kPX¸{Y"kHU;菍D_Cއ< G N MxjוtIN$XUȳF1g6zvrk1p@Sv *{[1Xf 8kjl%=e0o[CdEhoe+Nhyeuy\ܜF)tKF u!^qTX^ځ|y7J&ᜅ&e[՝>gs1eDpXdtwb5}dI!ůBA _l>~I(w$huf"E>ip`u1CVc,c9 l ljְn5.0Hޥ$9=F~Gqa$K2*k65)Bӻ/Fj{ aGN`;ܑҽdԷ`} eM߷W|$>nZBůB>e;g~.fCC #l-l+6лr[b a_tYClE(a j"VsfЫylf endstream endobj 27 0 obj << /Type /XRef /Index [0 28] /Size 28 /W [1 2 1] /Root 25 0 R /Info 26 0 R /ID [<8369981844BFC4885ECA1BE4BDD06248> <8369981844BFC4885ECA1BE4BDD06248>] /Length 85 /Filter /FlateDecode >> stream x@@DѪfPD"8DcGu )%2oJ@Me,Դ r9L\#V endstream endobj startxref 28856 %%EOF leidenalg-0.11.0/doc/source/figures/resolution_profile.png0000664000175000017510000002316015101156755023220 0ustar nileshnileshPNG  IHDRejzg%sBIT|d pHYs  ~ IDATxytTMBe2,A,,lN& ""vÖ=j%t`k"V :Xh1r"[JB%J2_GɄ$wq3;{s}|Zm@HE@(0 2 x(kFםwީJ{СCէOqr 9>^t 9ʎ=|ݻWԉ'o8%@dozꥸ8IҨQi&=zT>O*..V/y˲,Y5⎎]yڼy*++e۶֯_D_$-YD#G ۶y5뗿ekhM!8WsԶ|ΤG~?M\[`^[P6x`vmJIIрd۶M\͛7O}QYYLd?'%4I!8WsԶyNg`wҼmke7GseY͒<\999 uZvvL)h2 @(0 2 @(0 2 @(0 2 @(0 2 PW`pPW@l; p%e P`Be P`Bep4)%%EJIIQN`+##C TEEeϲ<])>>^.\.L3fܹsU^^YTB̲x)ekjn o޽{GWVV$)++KV VF Z(? &HJKKv%I]tѡCUN>իWkر &*JKK_.IrѲuܹxL:t$iܸqڿz˗+66vqA(tƇ Bk1[bn2 @(0@P)q,o\.,ULݗ@p)n}  @(0 2 @(0 2 @(0 2 @(0@T …%YVh=\.,U@l۶C]D],˒!˒<0ISs ӗ PE:F/+..ԕ@ð @3š2VP`Be P`CYEEƎ~)11Q˕eff2x(nI۶m|}*77W7pvءaÆiΜ9N`4G7=vRRRk׮ 6vDG۷o]h"6,Fo{n]~>}DDj֭*(($nЎ;4l0͙3ȶm=3ڵ5}w`۶|>_cʒ$eeeiժUY6@bٶm:X;wTzz*++U]]v5^z)..Ne{ԩSrT^^?'..Neee,5<h,Kr%* ?.\ ڵK<7|AlڴI]vÇ,˪qϕ4_hM\3%q {h5^^op,99Y2dn*IJJJG}tѝ͞=[۷׳>++ۭk۶mc Z,Fn[)k۶﫫ɓ'uqI҉'vZ%%%iĈz$IK,ȑ#/l%7M7Qee~m-ZHrK/--ըQdYtw*##CԸqϫgϞZ|y@KpZvZٶL{jp ML_@%MSsK B\2LIIuwdN4p@͜9Sqqqg e7p$i„ ^zI_~\..^ @T8-[4zk%XL_"8%Fuul_XXeTT64@T3hĉ:}lVtt{98qB3fF^ -^XQ@hݗ֮]+۶5eЂ ᦩ$B\2y2 P)))m0e۶,Raa:ڵۑBhXpݗ#Iۦ%5o@sq{OCUNԶm[ĨcǎΌ5竹C`В%KԫW/}Zp~60|>%$$Jmڴ=ܣW_}5:<]vꫯ4`͚5K]vUuuu0jG^x|>-\PڹsQ@K@60YSsK͛7kڻwNj)j 8R֯_?7QZZ"##nw;|8pzjٳGǏWYYҴtREE·HL>e;vխ[7nb̟?_W_}}vv~a)66V=WЊ eÆ ̙3{?XN?[oi̘1,\הmܸ3sN:O$9rD.Kg`||?\ǎ'о}g饗^Ұaôl2_$-YD#Glh  в,۷:ڰa~kڽ{ƏrhٲejӦM)2- &s|ԩS/IO/ 0}|׿%I֭f͚xa$`(۹sf͚_u饗2hUVVʲ,Iݻxa$>喝XYYYڰa}`6tÇ[mSΝQw_jK-1222vڀǜ@(P559}W_R/;v7@ e-Ҽyt!%&&CYǎu}@pp'O~`Sӗs1} 1Kڳg&LNP8 &slMYwu>%''+22i0B@8Rַo_}'lc p.F`2HÇ7pBW_}N3g֠/C5es &s|;ڰa\.,m[eѝ:G|>"""T]]}IHLH-0]7@-2 P皲}(>>^C Q|| FP_B6mRBB߯5kv0jCYTT |>ٶoQ lԹN:ַI&sκKQ5\g؈ͩλ//t饗***4i$]~ݗCq'(prΜ9T6m4eM>]kt-`({7j{W_~!C(%%EIII={$iϞ=:t;CUUUY6@Rg({g;v(557~5ںu}zw~XEEEs=l_%sMYyy93g*77CܹEwtI]zu-Dڼyrrr.8"ǚ2XS9r_^~eUVVjݺuZn>|Q|>K.ջwo*"L:x`@kpM٢E4vX۷OӸqO7mݺU*((ۛT0@kpgyFj߾$i֬Y\TG;v=zT>O*..V\NNg#sQ86[{)KJJҖ-[-G}jӦ:uSN)33S<,YѣGo߯]kbMRHYUU4qD 2Dcƌ$\RYYY j?WVV|>|>nvtMׯƏG}T)))2eJ@kPHYjj %Iڸq$ )2P[6@(P96}yz4}Fw e:~8#UAР5e%TL_|O:Gzj` `*Fp>4 `*B%P`Be P`Be P`Be P`P@KrI*ƒ% Ymۡ..e@X@Ss ӗ P`Be P`Be PV\\aÆ)11QIIIZ`$\JHHPff***,x>D%%%JNNǕ|-^X]vf̘s窼\ٗ=} tdIRկ_?+??_YYY,Z25e{￯CTn[ҙvС``Ǐnվ}{YUnJv&N#GJnD;w999=<zz=GKҤItk޼ycيSvv6 @elӦM땔$˲dYx >>^E)aŤNcp6o[-‰ZßI߁kKՒ-mv;]b֬Y?ϒe˖@ ,Ye4FkJj:{ڷo}qqw^EA駟j޽ꫯK/iĈ(!)… !ϧ)S_~(!YS-.kڴi;nݺPؽ{Nqƅɓ'u]w{ˣG?򗿄ȸq|PXl\.nf?^/Rllʔ)r߿ }&?|20M@].R\\,?222`! e'O֚5kjK.uA=#馛ז?\#^_zbI ,}[ߒqgbN8Qӊ+~zg5_>#i.b/FR^^|A ><`!٧.z&fAAAs~?aKЂ5?. @ WK/?ZݗQSPfvo<@s)N^_B&L뮻NEEE+xbEFFꩧRFF5~x pm/-vXĨ5eP`Be P`BeT%%%iȑ:vXz_ꭷj:L3gΜP eծ];w IDAT꣏>ҢE^êUϞ=[Æ z|O\gekՁiJNNٳ%I'O-ܢ_/$iJMMՀ4uT>}V:tb Mo2dRSSui̙:uRSS5q wa,۷m۶ck֬m۶׮]kO6Ͷm|-b_WX?n۶}1ѣ駟ڶmۓ&Mϟo۶m{<{˖-mv˳'Ol۶mu]+v}}^uUEl۶~ڞ:uj /ݺuSN\sr۶m2۶m۲,;//l۶'Nh+#ضmϷuf_~o۶mnWUUٶmBuMgϮQ#""?Jm+++K~ǺλKj}-#eu6mV~;|>eff׉'$IÇK.ф LJHH޽{gI.]*S.]hǎ|Zrx.xgCۭϺutQ:uJV7MUTTr)&&F۷oVXYY)˲te˻*//O$khUWWѣuM8Qk?IJHHԩ%&&k׮] (0,0,0) -- (1,0,0) node[anchor=north east]{$x$}; %\draw[thick,->] (0,0,0) -- (0,1,0) node[anchor=north west]{$y$}; %\draw[thick,->] (0,0,0) -- (0,0,1) node[anchor=south]{$z$}; \draw ($(outer box.north east)+(0,7pt)$) rectangle ($(outer box.south west)+(0,-5pt)$); \node[anchor=south] at (outer box.north) {Layer 0 (Interslice)}; \end{scope} \begin{scope}[xshift=9cm,local bounding box=outer box] \coordinate (t1) at (2,-3); \coordinate (t2) at (4,-6); %Network t=0 \node[nc0] (a0) at (-0,0) {}; \node[nc0] (b0) at (-1,0) {}; \node[nc0] (c0) at (-0,1) {}; \node[nc0] (d0) at (-1,1) {}; \node[nc1] (e0) at (-2,1) {}; \node[nc1] (f0) at (-1,2) {}; \node[nc1] (g0) at (-2,2) {}; \draw[-] (d0) -- (b0) -- (a0) -- (c0) -- (d0) -- (e0) -- (g0) -- (f0) -- (d0); %Network t=1 \node[n2c0] (a1) at ($(t1) + (-0,0)$) {}; \node[n2c0] (b1) at ($(t1) + (-1,0)$) {}; \node[n2c0] (c1) at ($(t1) + (-0,1)$) {}; \node[n2c0] (d1) at ($(t1) + (-1,1)$) {}; \node[n2c0] (e1) at ($(t1) + (-2,1)$) {}; \node[n2c1] (f1) at ($(t1) + (-1,2)$) {}; \node[n2c1] (g1) at ($(t1) + (-2,2)$) {}; %\draw[-] (d1) -- (b1) -- (a1) -- (c1) -- (d1) -- % (e1) -- (g1) -- (f1) -- (d1) (b1) -- (e1); %Network t=2 \node[n2c0] (a2) at ($(t2) + (-0,0)$) {}; \node[n2c0] (b2) at ($(t2) + (-1,0)$) {}; \node[n2c0] (c2) at ($(t2) + (-0,1)$) {}; \node[n2c1] (d2) at ($(t2) + (-1,1)$) {}; \node[n2c1] (e2) at ($(t2) + (-2,1)$) {}; \node[n2c1] (f2) at ($(t2) + (-1,2)$) {}; %\node[nc0] (g2) at (2,4,2) {g}; %\draw[-] (d2) -- (b2) -- (a2) -- (c2) -- (d2) -- (e2) % (f2) -- (d2); %%interslice edge 0 - 1 \draw[-,red,opacity=0] (a0) to[bend left =60] (a1) (b0) to[bend right=45] (b1) (c0) to[bend left =45] (c1) (d0) .. controls ($(t1) + (-2.5,1.5)$) .. (d1) (e0) to[bend right=45] (e1) (f0) to[bend left =45] (f1) (g0) to[bend right=60] (g1); %%interslice edge 1 - 2 \draw[-,red,opacity=0] (a1) to[bend left =60] (a2) (b1) to[bend right=45] (b2) (c1) to[bend left =45] (c2) (d1) to[bend right=20] (d2) (e1) to[bend right=45] (e2) (f1) to[bend left =60] (f2); %\draw[thick,->] (0,0,0) -- (1,0,0) node[anchor=north east]{$x$}; %\draw[thick,->] (0,0,0) -- (0,1,0) node[anchor=north west]{$y$}; %\draw[thick,->] (0,0,0) -- (0,0,1) node[anchor=south]{$z$}; \draw ($(outer box.north east)+(0,7pt)$) rectangle ($(outer box.south west)+(0,-5pt)$); \node[anchor=south] at (outer box.north) {Layer 1 ($t = 1$)}; \end{scope} \begin{scope}[yshift=-10cm,local bounding box=outer box] \coordinate (t1) at (2,-3); \coordinate (t2) at (4,-6); %Network t=0 \node[n2c0] (a0) at (-0,0) {}; \node[n2c0] (b0) at (-1,0) {}; \node[n2c0] (c0) at (-0,1) {}; \node[n2c0] (d0) at (-1,1) {}; \node[n2c1] (e0) at (-2,1) {}; \node[n2c1] (f0) at (-1,2) {}; \node[n2c1] (g0) at (-2,2) {}; %\draw[-] (d0) -- (b0) -- (a0) -- (c0) -- (d0) -- % (e0) -- (g0) -- (f0) -- (d0); %Network t=1 \node[nc0] (a1) at ($(t1) + (-0,0)$) {}; \node[nc0] (b1) at ($(t1) + (-1,0)$) {}; \node[nc0] (c1) at ($(t1) + (-0,1)$) {}; \node[nc0] (d1) at ($(t1) + (-1,1)$) {}; \node[nc0] (e1) at ($(t1) + (-2,1)$) {}; \node[nc1] (f1) at ($(t1) + (-1,2)$) {}; \node[nc1] (g1) at ($(t1) + (-2,2)$) {}; \draw[-] (d1) -- (b1) -- (a1) -- (c1) -- (d1) -- (e1) -- (g1) -- (f1) -- (d1) (b1) -- (e1); %Network t=2 \node[n2c0] (a2) at ($(t2) + (-0,0)$) {}; \node[n2c0] (b2) at ($(t2) + (-1,0)$) {}; \node[n2c0] (c2) at ($(t2) + (-0,1)$) {}; \node[n2c1] (d2) at ($(t2) + (-1,1)$) {}; \node[n2c1] (e2) at ($(t2) + (-2,1)$) {}; \node[n2c1] (f2) at ($(t2) + (-1,2)$) {}; %\node[nc0] (g2) at (2,4,2) {g}; %\draw[-] (d2) -- (b2) -- (a2) -- (c2) -- (d2) -- (e2) % (f2) -- (d2); %interslice edge 0 - 1 \draw[-,red,opacity=0] (a0) to[bend left =60] (a1) (b0) to[bend right=45] (b1) (c0) to[bend left =45] (c1) (d0) .. controls ($(t1) + (-2.5,1.5)$) .. (d1) (e0) to[bend right=45] (e1) (f0) to[bend left =45] (f1) (g0) to[bend right=60] (g1); %interslice edge 1 - 2 \draw[-,red,opacity=0] (a1) to[bend left =60] (a2) (b1) to[bend right=45] (b2) (c1) to[bend left =45] (c2) (d1) to[bend right=20] (d2) (e1) to[bend right=45] (e2) (f1) to[bend left =60] (f2); %\draw[thick,->] (0,0,0) -- (1,0,0) node[anchor=north east]{$x$}; %\draw[thick,->] (0,0,0) -- (0,1,0) node[anchor=north west]{$y$}; %\draw[thick,->] (0,0,0) -- (0,0,1) node[anchor=south]{$z$}; \draw ($(outer box.north east)+(0,7pt)$) rectangle ($(outer box.south west)+(0,-5pt)$); \node[anchor=south] at (outer box.north) {Layer 2 ($t = 2$)}; \end{scope} \begin{scope}[xshift=9cm,yshift=-10cm,local bounding box=outer box] \coordinate (t1) at (2,-3); \coordinate (t2) at (4,-6); %Network t=0 \node[n2c0] (a0) at (-0,0) {}; \node[n2c0] (b0) at (-1,0) {}; \node[n2c0] (c0) at (-0,1) {}; \node[n2c0] (d0) at (-1,1) {}; \node[n2c1] (e0) at (-2,1) {}; \node[n2c1] (f0) at (-1,2) {}; \node[n2c1] (g0) at (-2,2) {}; %\draw[-] (d0) -- (b0) -- (a0) -- (c0) -- (d0) -- % (e0) -- (g0) -- (f0) -- (d0); %Network t=1 \node[n2c0] (a1) at ($(t1) + (-0,0)$) {}; \node[n2c0] (b1) at ($(t1) + (-1,0)$) {}; \node[n2c0] (c1) at ($(t1) + (-0,1)$) {}; \node[n2c0] (d1) at ($(t1) + (-1,1)$) {}; \node[n2c0] (e1) at ($(t1) + (-2,1)$) {}; \node[n2c1] (f1) at ($(t1) + (-1,2)$) {}; \node[n2c1] (g1) at ($(t1) + (-2,2)$) {}; %\draw[-] (d1) -- (b1) -- (a1) -- (c1) -- (d1) -- % (e1) -- (g1) -- (f1) -- (d1) (b1) -- (e1); %Network t=2 \node[nc0] (a2) at ($(t2) + (-0,0)$) {}; \node[nc0] (b2) at ($(t2) + (-1,0)$) {}; \node[nc0] (c2) at ($(t2) + (-0,1)$) {}; \node[nc1] (d2) at ($(t2) + (-1,1)$) {}; \node[nc1] (e2) at ($(t2) + (-2,1)$) {}; \node[nc1] (f2) at ($(t2) + (-1,2)$) {}; %\node[nc0] (g2) at (2,4,2) {g}; \draw[-] (d2) -- (b2) -- (a2) -- (c2) -- (d2) -- (e2) (f2) -- (d2); %interslice edge 0 - 1 \draw[-,red,opacity=0] (a0) to[bend left =60] (a1) (b0) to[bend right=45] (b1) (c0) to[bend left =45] (c1) (d0) .. controls ($(t1) + (-2.5,1.5)$) .. (d1) (e0) to[bend right=45] (e1) (f0) to[bend left =45] (f1) (g0) to[bend right=60] (g1); %interslice edge 1 - 2 \draw[-,red,opacity=0] (a1) to[bend left =60] (a2) (b1) to[bend right=45] (b2) (c1) to[bend left =45] (c2) (d1) to[bend right=20] (d2) (e1) to[bend right=45] (e2) (f1) to[bend left =60] (f2); %\draw[thick,->] (0,0,0) -- (1,0,0) node[anchor=north east]{$x$}; %\draw[thick,->] (0,0,0) -- (0,1,0) node[anchor=north west]{$y$}; %\draw[thick,->] (0,0,0) -- (0,0,1) node[anchor=south]{$z$}; \draw ($(outer box.north east)+(0,7pt)$) rectangle ($(outer box.south west)+(0,-5pt)$); \node[anchor=south] at (outer box.north) {Layer 3 ($t = 3$)}; \end{scope} \end{tikzpicture} \end{document} leidenalg-0.11.0/doc/source/figures/layers_separate.synctex.gz0000664000175000017510000001136015101156755024007 0ustar nileshnilesh]]Ʊ}ׯ<;+v{ނ$p|h%Jûd'ߦHլuk<@5<9S,l~ϯ_?c$bU./mXl?۟~7r/rXny:b.7Ǻhū{S->A[>͏MD\@ݗqU6mQIto~V|6\xܦuWzU_z{\|jA,AZU\-_"m_4wb/rlmCr_-V"xZozM)拺-[1O2MBJ:\ajթm_$> USW*vub]"m[lIN &}y\V])\6ǭZp. ,HV{uw̋~mKUa\n7-v@wG˪.j:˩ :dǰȫ)=]]>pG^#=VՎyp>1톝e/Wi+%۩:Yߊol؎oQTgcV{}ViUt|sMբe^%JWګzwT۩KWec'fUAF^'m76QmYWJ'tOJDZ\~a^ū],y&^ՌHEӔ UˢިŊr%Q _)~FDp?{ߨOq_N}oT*?MbHq:nSRV<?l+vO=f?Oܝfwx|u<IL'=fCģȨ[gHY:h"u<egd oOW.}>)S7?)ͼOW^q[~F迼vuNeؙku+j`Qu_ڪk<"lAHjgW_խc68Ltͩ\Şx͔ܵ^%yǐ=^z?<~˽UA?|EB^vk;\<ݗpqG1.h0ėбE$b [qFYŒy9 48.+)SWߢѻ CJvjԿSnA? e?aptcֿ`w\&]"qN,&ߝN;J4-6#ve)=gwqğsW-P^(ׇJ8w"KPѺS4։3x3qjJTcTx)["G?KKtT-@J\@;Ec5D9wB (&g4Qgw;7rQ z(ͥHp4ީ{^[$"wk x(gjʋp4^s^>G?KCpT#y,rѶ}[8 J5ל̏ğQXLbDy>E 9w"K, $qD$ yrġ18i4N&gDGҐ.G9JZzZocDE&3m|H9;` (qhijʎ4dsv.Q!z8(K'gDGҐ.G6&[<#uwhHD;pzQ^2<Q~(wFae3 +QXŠ}_4{( sV[H)<&g4 2ZS8>FeQ$aYgi2,:H,1GTa %$IxZQr[ !02IҘ-%- QB`Icd<(h9wT-LdFH39䞣ģQB`v֓1[2vM[ !0ُ1[2}ϿS<棙lDEQS .{€~ ^- gF$q'A05 xOV %,Hz 35gY-x[!WˀYpYCp'o80jz8 6v{Z/a0ԫe,8!8gՂe 0jplpj[FaH628өw@k3ׄ=ԭ 6"q tjtQ޴?sMXV8Lz`s -wpg0ukk; i-~[ay\0,肣i-~暰p5Wٍެ%q$u^߹ /a-SNmQو8DZ8 q3#8a'n Ki"s}-X-vsKH{ ]%0Ʒx=`\ #<c#5n~p(26gq$Uq*C~^>,OZPf[>a-"/D2 [4Ή` nBÅ#Ұ+0I;#i $/e4~}y f+fSXK[/?Moˢ?zqx弩ͱ.vZߛⰨmiq:VOU.vwqei8V'fY{/L leidenalg-0.11.0/doc/source/figures/layers_separate.png0000664000175000017510000012011115101156755022452 0ustar nileshnileshPNG  IHDRFSl $iCCPiccxڕgPY<@BPC*%Z(ҫ@PEl+4EQ@U)VD((bA7"Wy?g=8X'& 영ALi)OO7z? xo"DDX3+=e/1=<+]fRK|cלo,]z ){T8بlOrTzV $G&DPJGf/GnrAltL:5204gk!FgY߽zس {{t@wOm|:3<@t *K`  |d\ "U4& NNp\mp '@&+ <AX H Rt # YC AP4e@v*: :]nB#h >Ll¾Z8Ns|x7\' |+xa ʈ.FD!d3R#H+ҍ!!2|DaP4D9P|T*j3U:@P"4-A[y@t4: ].G7 { 0fgL&S9i\ b1X,Vz`ðl%v;#pF8G\0. +5.py8^oG7K n~ A`86Bp0JxK$UD/b,q+xx8FHI\R)ttL&kmtnr*MLO'!EZClH5OQp((9rʌ8^\C+&YZMPC"QXYKՠ:P##ԫqBSqi|vZm@IR%%%%%/H Cc$0J8RRZm# ۤ?0edet L[MBjFt'}mngG StYZ[6[Nb\հjJ*̪Jhʹ>l-Q ynjah;qNp^ M}ϡᙣccieg^k\\6\}\\i ܺaw}W'<}O=Yza<^xzz|4-}Oo ( n $ n ]fkYk\'.a݅aτCBC*6,b:*4r2*4j**z_tMLyL,7*Ms\m\GńD\bh$jR|Robrv`NJA0"uH*hL֦uӗ> ͌c֙ՙdKd'eoްkdcэ=ʹr6q6m6o٢%VǷo- 4 [w8h)+Y؟vUKaD"b~ yqwӒC{0{}e̲²wYn\^{p 〰­RrOBULp]u[|ͮjU-t8:z##G^47emjm,j|,&f%eDȉ'Ovֵ1ڊNS^˃Ӯ{ΰϴU?[Nk/:6t:c:]A]\t[v/H^(HqRΥ)gD_Yj^\ݸxj oZÃ< >x8(ћǙlE>ZLYZ M<2G //'&O;N}īW3JYZٿl&,]Vwzf=gO|??WA}>Mg-`*>k}et1qq?.r)ԕ cHRMz&u0`:pQ<{PLTE7~~tr%tRNSf3"wUDݨߛPiv]bKGDH pHYs,,sRtIME $9@IDATx\(@kUzr~yDF;4*g׮7XF65Z'hx`QFk{ڦ~-FŝNωr5Wh{$fcOyL-Ǯm*7ZFMs]Re;9:4e0HHh5IIc.u|5jꢗeg}obZܟn]n ~˽WTGa<Ѣ$.z}CM>P{JWoMk4X~vzXjԴo&Q+Y:`DOW.iR^Uԣ(79FV E-t[ p5=>^Hho{njkeT_׼zTko[J?EtDo} y`rgr5s1¦|$X>=4b 2,)1ן$ r_%pzgl&7\ާݶNFcetFSC.hoT>tcuRwɼl_bC3~A%wo?ք|R{}܂nMQkV&4zp>}/[_MgМUL5q4iϿ-QoЗ)mwkiTy}v:r_S=g֭ uu/yR~0ot+߿'\ZDn 4;hbPQ?#9o]?F&{&90)swt_ί<4n;[=]{`h?Hyun@ŭ?Kte܋~vri^@yO7 ߷}_oy(lB34:aw-_4zMviff3٨P =^ۨNW&/w5zghnۋ149CO%yAJQ9k'4:P\~+ʃ<Ú0Qߞc;s73U R*qGalV*Yo4uiuf Wq6?fe\FI&ѱSZ+pH?U)î/_gb݆4'~Bkڽ5=isLÍwzkc,p߮!7dq D}oq4M~*`滋4~<`, ^x~z`O?w]:9^zu#}Ф؍j$צ*xF14 4 4 4 4 4 4 4 4 aPBhWQQgB(QpP@^p(8h(xW(8j.4 !p(g(Q4 AhF!(8BQp@4 !F!hBC(Q4 Ahrx6J:ET"? [U{ۧJY2iKi+i AYYoT$FU ]{ ` 4 9F]iIҢ/%N?V/D{L=2ء"GQ+kSkk~dQg*OE֩V{&`-];McuHuZ>c4Izh͐h:ugkhӾGJO>HR]ڕވ.aF#ѮZo@?|A5תFż BX"qf NtJz`#պh=*fSTk4ZĈ9䛷ޟ Xm^1չ*N3{Vhm4U4]̿NLԨjy1"sj >B$SvvAmh94Q@:/ZNGKMtYa5v-'fzQ5:_rDKDDI#$6 JQQ2tYTy|KVzp!΋'0LxjmMxzLNMxj.3{>cj O55ҋVOco}uxNc6ȱ9FL53`b{/;ckŠŠ~qP:X ڍumM$园IhjtTz۶&۪A4)G֚b<Yn~̩cPUFzm6=l k4/w9-zOa5ry[8D$n9DMxm[=9;QX~]riTT^H#|bF wFIx8s5e+L0)eXjIEUҮ?G֨x+NS5mjDZ5jnGe/k4c_e5M@sXQK:AШw,רUNWXh;k46M#JFcݚzIyɑc~`kEbjSG4!RM O^gz~!݂FG@1[<Noh;hhШw,ШNA#QXQ/{<4:!ht_ ^uG)`鉨cF3a?o^FsE8/ht7Agz8KhtП7A? -'sIjoQpq5ZJtCRBӕTu˭Ȩ>nECv8hrXfj>+;U_==譺ѣ7%%$}F!%u>Qmsrz"*7~<ʴ⟋lRmVYl J%vzDn9FXv3sm3bnYo i 8vkeFO:Jw~TZwz1JoḐF*ViT* RiX|Ģh[]hwKB#ۻZY󭯼TU:Xx*h1n8;t%^_:+?R_qjx?<Ūϯw1Q[rLhNu1fn?t^J ӂ&W$[r4*Ԩ1jWf%SkWϱmVhb6vmgA3Lx-V9F4rJAbQuk:Ub6i-i}$q^+5l&UєFJ5%^{qLCWEc1JB0g@Ι"9F[]ZtͰ5Z7 + CX}VhxM6M+k&S'NWYO =!(fˆ=aEaȆD$Z;Ft-,`F{_{lͿo6T8cuuI:Qcj4ݨFj nMkG'_gFo,6.]UETFQt"tF[ņLc< Oo՗lN}kt1ncֺ򺃷`}|,KOrFeL{1-XJcbN: ԧG(1$/,wj԰##7iڌQm뇯Fj kYvP۲xKPgi< rT%,m@+zmk 7*݃9O}팣jHopySK>7r65)JG4waF7l$ySpX7|"ҟ!u\=e:SVjtt=}KTlj|]룤BfTuoҬ*-hw9ӮSOa+5hG9GO&]u>JWNeG~Q`WދuRcL+ACkTCϒj+k}MYu>ڦ֨cXQ7o1cQG8X.Bev1Kh;iv}cLr[ШwӨY$Jٴ4:uG~cL R燩u>445#bFtHc UcFG@ޱHQAEHMFcFC~iI~ht4HRjz2ʘ=*Q8 >?)AޱL;yߏS7FG@ޱL"#}[甆OkШw,hpkޣh;: QנQXQ5I8ȏ9A#QX"g=4ht4Kvn? FcFSJGjۓ'@DVFG@ޱTY/Mpk͡a A#QXSUtړ乩A1CM]Jq U}8DDY9j7h;hppĢG|h;hq;:t FCF1]st6ht45*y !꫔44궺(1'FCF1]hk`-FG@ޱF7o8@#QQcLCVFG@!QY.n{1pKQI} zǪULIxUxf*{h;Vi4~KWG0FcF; ?JnڜwШwh??,%OFG@ޱJ=^SxGc<:z:&VGS=)ht44t|C8uܘ<h;Vj||lU(1DנQX( oӛ)j04:eu$FcF ;>FG@ޱVoF\lPx zf1hth뤼FC!"=v8ht4j">FG@ޱo Ght44.8H;h;iv1Bkh;jcLh;V~(M;4LٰHkB 5jWuϔ]@w*g'v{`A? Ze[rTe۞xTOV4N1GK,jJF {qFS{IP"-JR/=e[7*S8;`C?~mOiUDuio2ظn?lxsjĻS̤XvCzنjuusJ=^Qfnx~djrG'o&O=onsӐrnX?.hcdF%$ۛ I~.םo]xoڣ;?VXQ'3[4цHa!_~M]8hvryZLt6+58hw3mI,sʞԲTL<ޮ=Q1haA4FǃޢZm}UnHp(];进uaݔ)Q J6,H}\5=v5*/.EWgi9;X L&r-ҩ{}^ؑ%Lݽã4Q4zɨi7PP-3ސQsM7뗶ћ{P4z%󶌢sVk4>~?X<}^reF0*Wk|[.ﰪB]Fzt|a`P_k5:vDfoti^U^4Wj修6ƤWo}ZQy(^ :mSK%j¶"Qμ9o2ir/´(}0"=Ssܲ.n3dFǎ4Erl=ǖmUPSIzܾC"o*Q6ޏE24C(FjD8/?T-gE >F>^Rwb) >ÂF!4% #lcjXdA*Z\-ꁜu=?jYԷFwv^HGf98>F!,itIu\Y>`tcgCF!lMxZުtx<>{7A[M\?0Ƶg3hBk'ABXӨr䂦<]R8BXh:}4IP(AQp56qH$)hDA2>(9 aOѺB_}>JI994 !͝N zhB~MSYT14 !Fϛ>4`GsPll4 ! A ,#U-F!dh1TDwVI'4hB@F#-ygVLR(fdbGJY1P_Luz=BXhn#|K|Ek::51ܤM=EF[ vZw"=Q"&Gw(}6o^Cwk?eQ#Jg_+D_Lz4ϝhBńLIMzVxǝhBF2IfU?A{[ANe%:KVM}Oh;43(DVF&oW'tP);fT;F@Ӟf:yNb䛉AɄ2sT֗V&>Z:q=k><59eo;V5ߕRmzz@1λ'edg2`{ɡi9]S4\\85:ogiu}/Cs:PШc]>< G;EFՖ_OR=42nB.DF7Бݕs1SgތEi(]NB5J^r4⠸wtEUCLoR5qHl-uMl}x 1m hdx#q2.z(NgɯG1ᩱ?7x\kt`~Zq+QK:vQe&Lc |c̨WY]$V.+SF'X:蹴IWqv2Gd[6D[5I#?~>h+GE|y5\kԴf. Ċ{̌[zڽF<}_]̞L_0Fyp>RiQty?\nL;4F!Ox*gO(T091e"T>t3p(v2: >Lo!(8 MEUBGXŔ9GE0{cF!x TXk4 !5ş#I]>FF r&Y}3D}ChqdwӀ;h_BY*R/A'&6:hBmEkYkD.,{@Qa ֿ,F! ; OwA_֨T6/Qo7T=r->,ƚBԭQpׇ]D{LF!?RnLKq*aSxz>ZoSr䞀VU`}uGB1o4u:|Ů'&KSzQ!ELz 7LBz4 !*(暭?NM_oأQW銱c{4k^/v}4 ! lM}Ϻl}vߗGq%]hB@FWrѱSDr}hBܣj̘*>0HGQ4{+I@^i';)| C(hn}8+B,k{,F!h2TӰ1-F!h'aoߍ!QiW֘W}{ CF!dha_m}֣h5Q4 ! D?tcP*6Z=G(S2TPp=F!hyњݛ(r}TO)g?X8_)%8O9+hB@F;RQ4 ! WkC*=Em>+hB@F ~.ȻcơɱgB25@TGARa[GBheڮG೚QL[Bhꟼr6: 4AkhB@F~{&N/H,F!j vZsf4 A W?QkZ#S)#F!kԴJ'J\mQ'%m/Jڈ=DGOޘTI%GZ/"[GTF!kI%UG5Ye=F!B}vFuD`](WXBI죽}b:=Qh4*blR[IN3/F!fZz)7E8)(* 8F-[?_IwQ &_2~ޠt_(4:~=IwUK:IxQMU?oOS~TQ9ύmt55<7iQՃh4z>/ЇNPo50ii^>1Ӻ/ѼIw=h95ϛNvJ,R8湥Be}Q+~*ԦyOһ43~S"6чv-׶KOQ(E l@[%1"[Q)a 43D43/ͯ(J4ҳ]̧B6y@/SbgipfTG]̽%Lw{e|s:Qz8TG,v7ffu-tzϠr{WnfSl悥tA.?G~mj4ƃ b ,Ph┖oF!~i娫uh>}*Ah?mG]Wyt@hǟ]mxwAGOycs6&?8F?hӏ3i4fL%4 !FΙhujom#(.F_QռyMM)_hBc|!_hBcыTZ۴#hBcFR1'@(mLUV"0ON$?hq|fUEZw4 !p|Z^JGF!S6Ph)Q4 !p|4ţ *Bx~'[K-tA>h>.I_>vo 4 !F/.%?Ӻ_Ը]?4 !F{LRHdTG(/5=\$i݉-FhO] >#j̥Ei*uNTZ4 !F{[Ѩ |֨ |hB_:h3/F!j"7W4 !F˝L9蹅$B٭F!|h3,dr hBGfN/<ʝֿ4 !FX O)EZt:4 !F%FGʹoT{}hBS׷γ5Y{uhێBs^˰ev2E;Bxѣ6SkXoU#۱~4 !F`*K={yBџ‵Qd͛BxQPsü~4 !Ff8ūoAhkL=Q47ݾKK(H%{2'FQ`4ZunnQ"=hB ]t J۪GjYL*թ7=΢Q5VZTx~_(}ZٮIo J?:ʵت//͏5|F!h>zWEm?q6;֞-hB_Miyi=}mT=K$l4 !FT65KQ4;Ehƕ4~uTуF!h3G q/so쇏F!|:b(헷U6u^EkTZ2>6Ce4 !F"+g6Ur.bm1(@T;L숏nj*r~z=@5"M~PN]~ hBx獚Y}x(vw~2Ch"w@htȯۭB1ے,KeUJ)u;j1&4zL5ynuGvsOU (}al&[!Q{)<Ƙ!k[{G5<*c^Tj+:OfNU%viƘ1k<u&ϛY۬iLs7+6fdB+Rqq.K5#R9)ؖ[K=1Qdj2կ6劝EkA'uv:E$5)3K/F=Ͽ7Y۲.O7FvXNVG[Q9cZ} jWua4z<R)^drLuS:yȗ?*SGp2W2;_`3f5Y0VUK4H,2/{R%Qf)4z<Խ`K'4zXޘwNɈu净t9=R?sո]N6FǣFk[}@yZi0:/~揌-wm*/'g.z-19km?Iݴ|SF.Vjk^G$ymSGhQw3Rk6vww7SIrq{E]F<G[zhds:5R_8lhayҺSbԋB:O׬-3m=="Iu?eTO:sQ]z(>xy}tz+u4ieFI[L5FsW/V%yMۉ=(Rnd}5qhV8o4 !Fy\LBAQGe&q y=S(͎g^(]Dyi2@8꓈Fw{"D / FhF n~jdXmQG%4FQ4Y"?tE)4 !FY\P&a\`LۉBQtbsR =Q4j?F$J-2@hNUXlVeP4 !FUKdأ(w02>[0hFwMdőN)eAT (mvؖCHut=w#O ici B=ZJ?Vۙ%c@40(:zuҤ-4 !FwϖȻR&4 !FxѶvփQ4Zؚs`w8F BQsvG~#VH-F!h[+9l:~FAh4({ BџNkVMP EahVOwg-(8ڍHߜ C4:jΒ}uO #|d%U>14 @iϛ*vk,6Q4:f0wZ`6YGZo@ht>rZF~nhF!|l\jk-lݺ?@IU-TTn BQofʷu`(! &4tCeϑm< BѝHet:v*6F!^=TONw5яv H{3W}?ߢ SuYQ4[uT`oG-}hB $Fgﬦ݇6F!BҨSyxOP-̅gnBџ Q{Q4JuthB ,;LŖG~?D}bhF )()QGM!Qp]u4 !F9/|fxAht|ɢk4 !Fm{tq˩u<#K(]@D;\04ʢ=ߜwV֣(]V3}hB.`FTNH6L+#(]ʈ n΍JVF!hVLmm-r{kѢQ05Z{|Y|^Q05u_zt#;[NuZS?~W_[RiOt.F!螇dVxs(@-;dٚwW}^hB Tvh|љjU稨#莣L^9F!螣LG$tQp5([6:%F9[ ck^wqglLOQp9c?jCkG*lU[z݌yQpDs%!kKO~|wW(_ xZE4"gIe1Jr[&QpCs<@PN"޲wLA[Ght ^bFwn;;'4:~y"^tjSǞF QpS/04 3:xjo yhܠChtAb㝮]z t{^QpC9b=2~VYj}0usCT'04}e4h԰P$ RZVws Y&F}~[QSKlZӲHy"O(8_l< U6\Le—>ݮQpFsXϲ4۟S47t4ʴP4 ,fy|vS}o}԰b,@=Y:̣ :@gٓw*Ͱ!ChB^,R.F!\i' @7b }c6hBX2\(˷ DhNS}OhB!=h` 4*ڦa4*A.4 !FG(Eht y4-{UQ4:J͇F+nB rZ?N<<0S(G姻ZwmFhtT9WN:2N4 &dwCY: B7ӣI򎩢6Q4osiQШ\ q=QШ`OR JF>h`4*NF%EhB.)_y#YD^.Fgt>UX 0Ek5c-ei04 !Fg:ޫqv@sr(EQ4 !FgڰoVג/hBmr:-V(]BtxD2̲V(]D>HT:,kգQ4Xx7/kգQ4uen`MZhBnnu4ٿ3vQBm|:ܹ7vQBэsT;OZҪGht=U?W/S[qIBZ^6*l%z4 !FS:U?IhyizH7hBn_e%z#!F!&δo,MwmFht#ʴݪ٣YwʢQ4ݥeG~yQ4:}(A]!4 !Fa.Ѳ-{(4˥F!(̥^@)/謿3[Q1Ш%u>|nyfCBQKJ.g_?$gf @("M͂ =؄F!Ш-`ѨM۾S_Zjԟկ0DhSۺP\l߃ nz&4 !F-wWV4z^/@hoi&֤2BQ{ Fzv1&4 !F-oDe1Tm=C]sǘ(0yXwjyd|Eڤv}Gu{,ns67.s#f1QПa7.yn kF6iL:?'RlQ϶:6XRS4W}m 3̼IӍ U[f4jm"i2_uJc_ZzOg}%t:1&4 =Ikg/Ts-%Y0y~2y)FLfޟwAS`kH_Jҙ>aB;]t.Fa.FP5[K)ѧ(]* J^^*{4}{S]gYzȻ+;#kzۡv (ޒHJu4ɭdvcF̷^N܎Յ=ZGJ-i4AR5xX]1evؙ{;}Fɽ\gFTγbaNy\y^:fF[1jeiߘ֪҅}4 ?6[Y}Z"7\:W-} s[SQKSg(yck^5饾K:!+Fz]6k~:%^ZeJB^IN[Q=QjKb>/)_Ph ܯ_iyuS\WXFuW+y~[˥[4 Mul n:t/Q}7LV4 @nm rh(uD,z;8hBB{g#}@hmfuZQ4;[]F!Ш;Xz@bAh%53g+4 !FavChVѨFa 1@ 4 p*~p**4 p*nF!wP.gz @_!xY;( Yc4 !FC{mhB~0=F!跈r:P0Q4=gBhQ4 !FIuw _BѯVEh6yBQp@;F!(! @5o@XZيF!nz缙JBht/TwJhF! ^G^44 aFwCiayUQ4uڰXhBHqԆ}y?F!a_vQ4/lϜ|F! Gr(uyQ4 AhF!(8BQp@4 !F%uڣQ4*JثBQQԝpAhT:u!@hTEܵAhTuu>o?=ȫhB o:hB% ?F!(,wB4 !FaCΠQ4 g|B iSh BQv 8@i:-F^@G E$F!1E1+hB,OI ; Gh@`AhPD{F!(8B"iVhBHm2: *R- @QeY-qFQb\-RZI?.3K(籽VY}ChDIޝI _4*թʵn6h%g4LWNmKGb =FNVIK4ԼKO], j}' hu:y;;WszĎĚAoD=HNY$}Xaىch;#QlI#N H}|3RШ_*7K3Iuɯ*]: #Lz}RљNϏF:UukZ 5MkpQ4 ZhtE4ZŲݵK RAF]AF nkk73 FkU'O\MpڹLj-˾uZ؁uLh4Seק5ϫJ[r9<7'jvo Y:UCI6A[dzv|}^h?rL<+_I}.v6Ӈnf"Y^ ,uv"l-,8b\<۷wKYZryxPQ|i_ oUϟlIަ˟k7–Z#5j8tk+|z"bx]UZr~sm٫Do\[Fkճϕ7 Z;iI}o}[fzMuc@F0o>k#ϛt/N ֢y3O'h]eh)up\v4G5rmP}|b^re$ωfפf-h:^]34q`ShqLu%dMܛe|gjݗ5똂Fy݆n}Wcy~NOiSG뵏K~]nusC];|Ңߴ#Fai“%㮩7OuH5/FNmynßVM[t.m.b6 OT}W/y>'.ɻk=wkQh(_yqm6׻hƾ%gh4 h9{ojLsr9ՅkQL|ӗ _y~Km=|V#t=qm4k~ߛ餉ѨS5ߍU KkyZԵq%?mgiu>UԻcTc Λ䷬G&ϮG'ɞԅF}4dhy~y?Ns,j7Յ7Fv6F2x^筼ۏ?>@nlh jt<'7>C΂ULﮍF2GOM1T[,-YYSsa獾/N\'9eM9ywm;e6z9Ae?Z ]dT}fXu ѶQ7f@y]B#yN&/?|>iT]/;GT&)o=ױ>t[ݵ5ߺ. ʱ$y%v6% Tھ .uv-T?VE-}5yoλhWˈyBJl|N_BaTSyޏ<fZokmIzJ)7u{#"5>ύ,u7U}jnJ F% ʒ􌘛k7?ϑ5Z%1"M{`u{b5F3{Xq]zV]̿ƨTGaF;]܋t9lN17~[;f h9ȅΤS%*AlU;szaFAzv=xCkh4ШGhA.T6(f68FGG-:t^1Vh4:{YɅJǏLh>Rvn#yZU:k7t|if'Ee&jK 蹃X&ϻK}!mN*/3QTTU[TZoZ\08F/he_d)MKL+ʹQX4j28/%VZA h|5ϩ-x]nYR7NJ%8F~ikug՚bx+o\rpK$KR,h\"I]b@I xHRX+(D$^F%%) 4 ..IWQp$uIK$KR,h\"I]b@I xHRX+(D$^F%%) 4 ..IWQp$uIK$KR,h\"I]b@I xHRX+(D$^F%%) 4 ..IWQp$uIK$KR,h\"I]b@I xHRX+(D$^F%%) 4 ..IWQp$uIK$KR,h\"I]b@I xHRX+(D$^F%%) 4 ..IWQp$uIK$KR,h\"I]b@I xHRX+(D$^F%%) 4 ..IWQp$uIK$KR,h\"I]b@I xHRX+(D$^F%%) 4 ..IWQp$uIK$KR,h\"I]b@I xHRX+(D$^F%%) 4 ..IWQp$uIK$KR,h\"I]b@I xJU[Ua3Le^GK,Uf3HX$uHyb3jSmX*=ie-=;{O]_l,,M.y"u)1g[VmKXQn,Q:UMSH,hWMLnZIŖB:IJmUAS9 XʼK{V2:.N=ʖչ"w?Mj>8;geEΣDӞhw A]xԒSZ^k&lpa)G֨$\yckV*K5\OX &1)wjM̧nXy1T~m!tvY\'Jtn#Y)it3]F$7EyǒQZm}Y/HRX+h] z'3mu&Hkq_2.IWџ"6\#-~qqKuqsRz$uIS:.h@M,S0g#I]bV?E՞gtybOGEyg"I]bW=I|$KR,~km8$D$KR,ht!ht%) 44I x]]$uI..B$^FF!I]b@ A.IWхEHRX+B"$KR,ht!ht%) 44I x]]$uI..B$^FF!I]b@ A.IWхEHRX+B"$KR,ht!ht%) 44I x]]$uI..B$^FF!I]b@ A.IWхEHRX+B"$KR,ht!ht%) 44I x]]$uI..B$^FF!I]b@ A.IWхEHRX+B"$KR,ht!ht%) 44I x]]$uI..B$^FF!I]b@ A.IWхEHRX+B"$KR,ht!ht%) 44I x]]$uI..B$^FF!I]b@ A.IWхEHRX+B"$KR,ht!ht%) 44I x]]$uI..B$^FF!I]b@ A.IWxѢjS٨Rk+{cTN봭=>"HRX+h;VĴRbAv^>%) _5zhNY_'M%z:e]ۋlLECOj$KR,~j* 7WH^ƷOMBWE^B*I]bRQjt`oM<(1jQSiܟZB$^Fgt􌘫OF:UQX+|h?=m3[F鍈uͦ.IXQeiq!J򲭬-mΚ55dWzcb(11=$I]bHSST}b|Fsݾ djGKou͘qv.%){i?[`j\RQu^7UyDA&jوNwkOKnc~ jpq"q6, u3ZdY^$b c==3KL}gY ?&:ԩk6}ߪ_ҷ%%)kt|C,yVQޠoU:7T/96S{mHRXlߔ( :.3wG7O^x %NhRފA$bfHvnP ^4o{댏6*I-?NOd%)ktFfέFB5R_2Rt,p:Rh+z[sQ%F8o4>o`hX>o(-$y~nFhULy3#Eb #{\V1ՖW1UښΫXt6ظ^iר57P$uI"5xﱨ<1c D$bLüZybw-;<#?3ܼ߾LF7L,^jeEl*O_zVMKʷ&,uIK/g1O_zX?_{p֑.IWsv&E2hyηzv6rm Z*c"SA$^FnΩ*#K]bV`_ŋ=sH{Q;**I x}Gˏl-$^Fx$^Fmx$^F-x$^F{$^F7{$^Fz$^F7z$^Fy$^F7y$^Fx$^F7x$^F{$^FW{$^Fz$^FWz$^Fy$^FWy$^Fx$^FWx$^F{$^F{$^Fz$^Fz$^Fz$^Fy$^F{x$^F(=I^Q4z{ x2ףh&) 4zcGaMR,h<ޛX+YE7IWs%) 4:.FԚ cf^)ѫݑ.IWy/iHvSQHS!*I xEN"&=?%*?*I]b@s05Gu+1N<$KR,©Tr{:m=Fj7*5\yHRXDk^GA9dV9IDAT:'Q1g_$uIE4Nt;ZZjb6OҾ%4:Fc;"hzE"I]b]qb /\T[*k"mQW^$"H6R'SdZ{ߠ [Y;*I]bL[!NEƞUΠzy ^ƳoC$"5oc{H4:7]  :A1e:$ZϓԟNV$KR,1)8HG+KXa\V.htî!$K!gI&I]bL{3^=_E-+4:dINٷ$.IHK(P4G뎝^ʣF׎ E$"#FʩK6fmуFPNUW<& }.I1VzkSU?<4:lLz_/R=f$?ٳw$uIE6}=MbQyYz4:Ekk61D$"i,/2E}W@(/E:Tb)h:ߚ%) 4:#t8^IL{ߦu$KR,ht&zEkl\Pu߲ˣDKR,ht65޽EW$KR,(*F;FJNLI nw FT뻏E$1MUŮz4IޱRX`5kҬq zFItɫL І$uIVbL zFj %)XKQ:bdNG$k1qnAQh;Q Ei5QX'vhTj,4 1 {^e{v_.IZݎkF=#~K3$KR,aET7OzFusk>$uIVl̢ ZTBQ]7OXT$)!nW~@* $oW d{꫶It:+q(D$C( Ҩպn%^kc}UTnk-Qp$uI% T_}MU^uZJSo+1럧}tG(D$3F)%?:?=ǽ "4/EՖˇҨ6̓=%)`:X7&=#obInvOݗ:G-1EL'$uI%~JoL}|Ћ٣]1)Q, 61KUSTw՗U=KU$ H,ҩ/`t遑'JkfҩEu m}ziJRXB!~^]wlu=Vb#4F>nFWZr]zRK5\#k^Ջe/I]b V'-uHc CG563ݓ\WYL}3:'V:*I]b A:>Qdj}~׫g .0Z+uǧ$qt-n9IRXBayZ 0][ceͨFO,6Bsoӟ[N6T$k&$uI%Ljt==9Hm6 6 y2TfM}_/}f0R6̤=F3HRXBajeq7ksh~7Znݮ}h.CHRXBy~*IN5Jm2i$uI%&W1;X[߈5c 7ȪTϓ1iw$/[vxŌJOO/e'$uI%FPslkg|i~XFA %)P&9n"^v&^e1(߃ $KR,!m>JOnM|A6<sA$JeLκ6>dФ0uۤ?^Ʊ#PUr2(HB$`{zΩGz$uI%8w]Z<"HrtadU/gS']h\"I]b+|g=ΠQp$uIF!$KR,`4 ! I]b+QI XBHRX hB@$V@%).I($uIF!$KR,`4 ! I]b+QI XBHRX hB@$V@%).I($uIF!$KR,`4 ! I]b+QI XBHRX hB@$V@%).I($uIF!$KR,`4 ! I]b+QI XBHRX hB@$V@%).I($uIF!$KR,`4 ! I]b+QI XBHRX hB@$V@%).I($uIF!$KR,`4 ! I]b+QI XBHRX hB@$V@%).I($uIF!$KR,`4 ! I]b+QI XBHRX hB@$V@%)Auf],-4 ..IV$O!͓%Se_U7(D$Ize&Ml^eYK^VEI l"jnd("NkU%_FI lNuz1ޙn,R5{}4^WF%%)@8 (Jn LsXTNxK$KR,R?,f\)1Ju5Ѩ*Gw=snjWKRX`=N2"SF:Ѻ&CL$I]bՌJ11/QPZԔZov*NQbjK%)X9hlnĻ=zFur\z(]:X W$u3FkK,t7VyzFq,7=p$uItyT5$:-F4^Lw6&I oX;Qht9|?(R l7%)X3sW6MзfF}I%hFA0d[Fü5 e,I]bT:=94#%qQX//I]bdӓ2U?F=18ӖXtp4T$t1bjQVLx Rwhڙt WsRIRX`5鸙ch7 :5U%)XMs2nXfׯQPE?E'I ^e Dk hG2UY>+Ur+I]bIڶyDh\"I]b O{ߟ[=x4_"f4 ..In3Yh44 ..I&*cx}=*.o1bZ-F%%)Fџ(VMSIGeGQp$uIķcM%x&mf HRX`;Q 3NJ/ryQp$uIK$KR,h\"I]b@I xHRX+(D$^krԅFh"G]ha4w_#(GQ4 @ hF!J(BnW%tEXtdate:create2016-11-10T23:04:35+01:00 2*%tEXtdate:modify2016-11-10T23:04:35+01:00}tEXtpdf:VersionPDF-1.5 \ 9IENDB`leidenalg-0.11.0/doc/source/figures/layers_separate.pdf0000664000175000017510000006337515101156755022461 0ustar nileshnilesh%PDF-1.5 % 6 0 obj << /Length 8634 /Filter /FlateDecode >> stream xڽ]ˎ%qWrfQ\eX oƞz@`= quKY׀4U7$'/x_~i秣/%mKqs|O/o?/lڢ/vײl\[OWDO=xO'S=.]_)ơ3r֓?}YWWGhmw%@nd粌߷ͻ-W >j8p_8;awa$lfOfЁ/ 7lOA^OM!Elu8@b\4xyP}>%t 1$G#樿Y85-ufdU< 9Br|L{=8Gz4?bkk{ +j5T3ږk i4[r;J+BiPV8{ y,-C,-ry:96umFi9*M-ΎR M5-Gͫ:y%NwyiQK!1|}rԲh>Q^Os*pm /yږ|sSkJĴ8ofTFGFn/x]N LaʹI1<%Z!hUMYZ- wlRQ8݆SV@_GkZuk*` ] TO=\ 0Uh 6pZjhT;#4+Fsy6cBO!w:!p`. ͕ 흪 UPZ"#Cq dy9 T:A0(T:Rc)4`hͪX8EJED{j$4E>t&#sMtr~Čލ%~cFe 2 u-"4D cMSzRJvr1QކRfiC2-9Բpjm1fNJk%KUkU- %j9`cpȀogdxy.Sg+f>:9JcZj -:jCo"¨2guF2\Ǜ(9ƫo9'&qL\7hmB6J|\RY*m 42h|JRuu-[-$ :|1 D$dR}=4O5iJī; ]sӫ|3#H¾/k@>!k%C x5 QA_觚$%E]L>#ΓеY_鷅m _izj80vW40qoK֐j[fH\7+N.o-GYub&f8-L0WXV A| Pe|XSA+5z|&n4y&#{rix4{lSyHVnu˩%MJ3Ъx<\0ʜ[$u,(٘ydF{P}2ga_9]5Ɨ_3:=&O;Zb%~  1N߾~{G??}~_ȋzP eu[*Ad]uI&;_}^߁RlZv6x|9a.3빵WlnvlIm;jD(u|80S%L ~~.{+x%nRJ :3빭*9j5ؒJýx܁ V-Nz9 xc\6co >s.=ʓj Z #&+b3vD/H1xG9ZBт>Qm Gg!qf&{bFOs.VR&,PVQ0h]ALmUM 7Rhl|(Tn>GivATҩ G/`F1J9ZN摞1Rdf־˼kr. .槾.h\G~Qq]=K.M5wk;>Og]1hKzs,Ͷ(*Rm秒eu]zWt[)I,@ fKjDTjۋz 3xʺSz$\o+0gH0]IbF!>˒AQGf2GY3j ZZm^S!K豲Hk{g)]ujUA5y0Q,We#4 J :Km}QdZGuOPϧĽq$˜xeQuF D_J4pUBBH Ր](t 1wV8O–W]0+m/s?[BKeqBZ" Uaa 1kzziH|9i=/xy W'@3}U*U !f\3*W#,WGE'\G6(suoKs{' ^] #Af|AרLZ gxY Isd4E'>wCNeÓ<} 1=/w+"ק"&ջlw?Z\Âd216 z1 c=Rzvp_9=IWw C[hes!ռvh4δ;s^Â^jB!hcrm@Bf6*<}UG \l Cbb@d l,81xP}S=8hsz٬İilW%)4 |6<Іn ^zD O>yyre0+&rYLdɁA,& |Jц ఘl}='!.#γy "X0N.PC0Q W&4 s^2_hOp@c'>+}og Yp!/O??(_^AG.R7ne_]`ɰi!ҽ/:p|Dĥк,(.s$hTD}}NL S8JKw1OkrNd5yM&״;Si ;| X9?=QU&0gk9  (mڞsFs'y&%.g QXfjKO]#[x~qą[( 2S槚.\/3`14yec-&o$cY` ?}ˤ/yH-Uy|3UŞ yahgbEFLh,bĜA^,eb3KgS`s`(Ę;=Pc, UY\."= J|Qf=mԍyN{YTa ]!:v7&(UlwɺpPM|i<ǔ'JʧL1_Je?hat*Sau7Ufgr.M\ɏ]&+OM|'P2|ӵxzMǬ8/Q9MV:=vbw߿߹+Cu ̫̓V?PRR|c^O, ׋`.e1wXTN]RcV[e4(9⷗҄)m"%`dc< :cN F)x-}h3aX_B-ZS܏JZ9j:Ous%_/[]9j\+ * gzQ,ءkvZg1RX66 ,sZX4hVρ]ve!u%Y 5mJ+bqv@ih f~Q%-Etx48C^jΜy@jvMR[gz ͤS6'FbbA Y|Qw= beUyqH^OQ4EMGPYuQɈwPJ]PG'j?9n%kG>#WCڜ!C¨m媹gD4ͦW"WJS$hZm+W x)Ȋ6]6[5ʓȊy`TPl~#.4xhEs)X2j]T+&]e|CX`|}\6$P5aɭj)NĔ_-;cSa:hZskx:7QFk݄$u0`)?9ghh urhRnjLfFw.#ΓеjK6h`Xcj/ZYdpՓ-"`:p1lNN3&3"^}^=G 3E'␼M_ʷkaMe=1J'}i:ܿ';˛\dh&dV2l]h} {;Ve&оb.s^X(딻*9a+}Bn-aifyzM7Q*"@ Fڂ8Ȃ\.PhwVhOcUd$˅ɱXYԬwjA5XKӨ+s9bړCWqGs;Њ5= ~DX]Zhgd*1 Hj<+kmGgeXYZ_Ǩ4Ԉ彣/cst%ݏ5WV:@sW~Beg^Bs,_'<i3*WZxf@sP}μu;uճ[6ewk]m`YVFyKY%³JmsJs^I`–fms٪s _z[Ec|t|f$;+[g;A8ˋ@)Q9P|7ؗ9Etm VϞ?EpMM$qXL)N-v:(ПC>,0*Ϯ SG6vJu5x^SupB#f*1*]:ΐFcwtPn*>Gb+ D"2s6qE5 fc*{Θc)z@6j:uHUt"PDg,/! 3$QCpf#Y7k ٿ <g%,D5` VBz5 <ҰCpfCw~ޗ ˋYaa_WTlV zt.kX0hV` A]X3fa/FX]g' 3}r 3ɇ/\l ElCjybWҰW#p&Cկy7m+4@? 44W+#vEЃp9c^j~ .z[U/Ff 2c1b *o>JLZj-/zSG.T)E3ГTVђYlaHDKաq:XNKwYӲO+GBY~ʋ\ņ =Mm es5~BL0`o{(LΨ8}Ou|~&4& 2dF晫Qb⫽CD-=ޚ3:?3]s`: hWXЫ5aj(kZdB ^ a3vUdGث8ե}Y.#s6Qf V0FRt~Aip6ZTrnu-,-լvaYӲϜ~GV>9}[W>Ha9kO\zm mJV j&̊ T};Θ3í pZ _\,,5a6ʆ]jNC]쬓A,B=Roڌ{U[sƼ4 >j]ڻjIr@rGѮWՀ\/sU^8jByiثA8|Y6#гɽ;Թh$pƷVq8:+o1,A͸Y+r$o. x 9cjjN$,TT@OU^1Gc+FHtVbX0"hWp#PqxnΘ|6#г0/Qҋ]/֭(κ+[H0"hW`4* 8_֞'!?T~Y6#Q!Y%n ˷Fnp6BX0~yCφ..Eնr\1/y5gj?TPoJG x"1W](E\{R!ڈ_:=+6R%W;lSo !9qYO3Y..oiB޼1KM|o0 e@&2ג7/3KnQdɋΜKzucEdΡOCt1`Mo{.ɇ|s_,)}hKkjssXڻmG{(7Zަ.4Zo>Tzy9*߾k/{/Ө^\ }s'{ :w_9n!}mK^™뷏AwΕM9Ӭ? VCoK*󔨙KsjEM)qEKPh)JjvCX&aſcyBp)(=˴.|Q{w忙q/9"94hwRnt U 8V-W È<'>Q89`K]>V] E%n=.W)$ ro:wRX.1&w,γte/s6jb4/_"̅[W-C $ŇR5d=Е64Nwk["#oR<$'VMzO Jo]籹ҥ(x#)  endstream endobj 13 0 obj << /Length1 1662 /Length2 8008 /Length3 0 /Length 9082 /Filter /FlateDecode >> stream xڍPl6HIw K7HJw , %  )R "(% ~ΰ{s30kqlJ0((@ Ypmd6{!0`ަB5`P O','"@0q b `8&<O-;OLLp+b 4@G}E[ @f #|Mpy`\o =v_#4AFd;B:`opop؂Op_rC$Iu8>+G0B{ ΃Ap@P_D vA\@6?Zdu nqCy_3Js̊P;y+cO?w_޿. !P;_cy@!`8&60 E;c'/ n07@= @;0v[b~o0ˏŽ`P?WPAW[q||n~! @LL "$om6/ ҄  `ss,WoGJ..$ W_{z"@v \ *2B\9H\ ӆ l˟v_apȯ~l(p%0_k/$ yx|1Zxy0}~@=םxU~D^xmA|r!w MBEK}N^ GeSUs x= gM /"a)z bN}J;Մ^TRys~F_lI4B|wTG|2.gmWA>=Y[Űy竐T\bwqF0"LCgmbƚl&BK`r n* pӧR!esڶcML==U Fnc! rqf ~כ&pT]%\IDAJ3#I+F8Й|Yx7R'@41?6lH٣Soà){K;[PJjR $'iYJmōϡ*"gד"Q8V*qIa (^$-ϥ,87|Z]ǠD/JQHTvZ6_h]eCbL`!1Ѝ8V,2h8C:.BU).1\^cώ٭?+Bpת%߿K4!y6d8ɱ_iA/B:@vDŽtk 3en6數 Z^;3erNXRq=RK؛h'Һ 7B]c9;QACRy}Qb45q9m-wϞ0#RgQC3%udE΄r)e^sT =7fKcH$WFR;lYWt Z[딌SeKhS%!?3ybbuR' cyT>r[(Qc3*u9JkM=Q1<@~G;f|WYKrp7tsRKA_~f[MH]sUk}fk%2- p ρj#ņӱ)iGHc2|wV}x~‘JݓCVͪEQ&Ĩs7 ?#OJt܉RtPkL~SVM{&*R\c 7ð1$+j5ל┊)lWBlCGhC_"=){vuy$gՁ-[2z$/IIʦXgлq׾yz𪍊\5\IDe癱l x;uRtHJD;mrOJ iKw">82P~I!MG< y'Gi ɒV"cYH&Lmz(ňLvyz]{:AU\EAfȲ +sǸth&~o2׾K~X6)9Ŝ F o@pŭBփhjOۻ}ԉdZjLY\G~>& t)ZO騦W7OP/h:)&iB(6/XJ|?^ҭߙT6em7{JYTQeՒSm} kfR]xUǷBT{Htİ fIٝi\{*_\!|S{~LEݝHnDÀ&5Gጇ=8{ej'd6ˁ甾b&y' h۲oƃ ϵWX94({a24EؓVN + Av6)>k*:Jݩa~ 1 L5E$ʋuOs9c'V#в\\ߤvlX{2w)%DT0 $OBӯngmmE'Wi5u^+'l8 r,w I,99|?fyHHv&1Q&NÊUr"9v ,߰"Rk TWh$ :;ݡϴ,{,B+hXu\md/Gu^Lg[rj #-έ#8. QLT$qfcO2F74+Ǚr닃Ӂ旝8w󞈧3\ 7Cx_`E2[7YT ! ߲:-9E4O?cC.C$&'}eTKq?< ?B(dN)[KZ86ϣuf>A=dLhhk*Ƒżi(*i=OPbpw:|pό5Ȣp?c:e-s]={Xt[@7CZʡL[%N05U)/U^ob'S(sH☾gyick4 4H)uo;@HT4QAGdL}A;UR1O,㛗LOڂ맖kr[ .[+[Qq]|]ʱd 66g QcZ!Hu՚B1?Եu{{b]vUO*hgg>_7l.KkK@ YX41=Hꈝ X\?lDK~ĀvPJE1}J{3U^12׮od$Rȥ+!@hr !^ ʔq+|RGH&e.3߰XLY.4h ݁<{!w[!"q_™/*\C<6W9~n--9#}T$5 Lրg9)nqA>:}NE~ zr!$)K=Xu( (N56tB0x{_ʬD` Q.9H?91hN+吭2N0;Ni~bU~GLE|C#5>\K)8[SgǤgeȥ\“h N#)ܶS')Igȴ0AD7UK3vO,_89ǠttQ Bɻ$9R=4Jg+J{ 2K(^'Һ_'`l / ½/6=TBQzH/瘽 sLՆXIS}QG#ݵ-E_wj^A @i/%w=*dzq(;u)8q" K"FC Aco=Ţ8GIoLҴ_*2Sd,) @$J*f rr*'M(uWk &O Zq` ځn?)@ųщ)nkp <(-oL^WiuDO-wuxgմ5bM$pօ!~-YzJ( EamkEh]m\Ե'a[d$O9uL,R֗m)dFvM5NFO9FYbgjz4zo%}CU~EÞKZ7~ }V/B{}|%w݈y r!ϧ)%&X~#4mǑJU#Psi')p{)6%I?+}%a&;Z藛;nČwO n5%//ңۗ>Gw[xȯ^,.~`ޖ>s^s>%_ե ^Z^S&@֭&E7N0*1o|bG]A+uA]C׽V-zwU"ԦW*,OfD$_7-20":էM֍{*0PI_5ԓF zڞDoIDʉХ1k9cӯ|oSgwuDXXtNDqЁtel15Tc%ru= !s%wCQO̭T_ɂ8Y}]<C0/'GHZ$8#ݮ79䜺#KcH7aA~ jCt4.?Чu9DI˚> k7 LC!RVըp/_9Ԛ2;Fr_`,=Y>NZo7]=\e~ٌăIvqz[`ʼvƾ]w?M(,c_G1DYLJFy)A?N`=2ܲi#gs"]][['ŎRL>mh4F#̑ztjæ٧Hbv??;DB\L ?<ǔ`,R0O_|q2hCg{BC.qpQ"Bmy\zd^]}+K$_O糾hU98YP԰`jR$ ˭5a{6X/,[NϛKN"k(BvLJlծx@Ks>Bz6'[Etoy"i\Q^1rXa \ĸa"6s+*vAS[ pzb}zEȏ 豂dBUk@#Ó}ƅ4|27I\sfOE<ah|a[aQ-,uGDѸ$?\chI1D?MfHFth0=g}hEÄ$O7Mq\wb79)f TB䎪7 S>ad#O]ong:BW}>*dQY9Jⰺ.D4}UQ:)=zl(]MrF2;#tJ4"gRBk٪Z͘2@y)Q0#[慗`: ]k^W #[)n`093Q^ym&/-~zoN*&9S)2<]4h`A61t%0GYDדE$rTK͙Wy9Fd/;jkU"+H]䰛v͋M[y;o:[1oޞP{M[+9lEk"?D7L|mJ!o˹fDkHs3E gZc}lRQa4J_Xjc{bo,dvRʹ |&g#-k2G4(l(C/[,G]窞B[S d7?OU}eX [^ڎalC%f!eΙ^{e>V щoCڈp#]~^>l,xBt+WdOeG7_~zɭ S/8)ۄnLc<,uOH&VϴN!klLڵ}BQőőjJŷ0> stream xڍwX>! Ұ"Rsc ctHHI JIH#!!t7}c칯n=-AB`a PI U 0$ 6Db;l0,F5ܝ`1 XR,%EA 鿀h P恴j 5([ A;`q`ii)@g 0WsBp$wXOOOa0cO: nkhlnma@ Gp1([+ku]?Z, ;ݟѿ!Qap8FvH'P% a(_@!`68a@> M kH_ipUB;;#PX7"C5 PW3#@ tp""Jz ~;̸)|\.@; ?qy X;?^`- (?qfk0H/}` ; l('OYD]XKT௙**>BR@!Q  JIHHUG١ۨz\:h{@$.3+Nn 'NBpu⤠ PF"ݝ۫$Z,Az!lXnKqNHB@  qQ Sѿh_r07wԸӥ-7"(4 Cc ` w`p2}kBchlciOŞ;D?SLEz^[`[TC_k@ƚKzEף]eBe{vÔUx&Il2y<=(Ry[(7/Oih-|:u %Z 7 6m{y2cB>LIԼSw ,6Iu3 K"vCYj+<4U{k-j_, 31{kTA!o̸MI RR^ƀX\7@wEو{<8xؒ#7BO/]1—QWwPPrga"5bJ3yGēH53G&gR 5yjʖB-yo]2bs 3IBc$wxmGyF/mFkÚMoC{X-IJE=j̱50NFzz?p̩M.cc}ieAAgIOEP)cu,%pfV6r@xАqEA(x)f:amѐZFKA"HM(V`64kۇ/]HJ7In:~oOl1fl74_q%:P~EÊ4w1|Mݕ̃u9o@UžUgawu0|P ]OЛc7=D|)WWWm &8')XcbK~l(u;!03K@mFFr/L:[ ,'Wf= kR*ӿ #-5>-Xaj6qu|a1EGt Qh643/* =mcKkNh0I$Y9dݻMneo=τ(R$KԺ٨˦,}.,kn!1Э"8Ltzjx}0 ɭv%[է}2~&3a~5KSrA߸⼲޽"ShꥇK_/~1`!10&ʝ\3Oe[G=DO}V>D-2C$8x@2֝/d3K6vvkAq4OG_T zF%(Zw6HM1]}F×8@0^zeqq/D/q=3(eUemZ-*U|UGRO4g,>'Qʹ{YWʪf&ҟ*ٲ-}!nCU8zE!҆&2whM^%Umrw?zBd:x2(\CV,^X9}Ij6Wyk2@RvL_2/qD~9Jʫ,bXFᧅRbZE{~t)p݇i IB 2`@[睟 jYɬ{ia4)O# C*ΙnT&}(Bga$+p{g3Aj=!#~a Ԑ}$C+k{ݚ jir>(hx,=PV)6:Jt \'63<Ɵw{{<Qt]Z̯ }BD%$ח3 ,E+(mUJB$=^-eFB{^ID C &A/u\h/mL DNsrpk?b:5󕫷˙2`_Ce ˳lC&Ԟ[8ԮϞ1kY?/;D '>?n4=wAvsfrnxwjH gH?'>PVot<:H!Qlz%l2f춢^5dۙDg=Q554gvz{v}EXZ}`K鲴tע1>| .(idFE}VP!%;4nS~/=w'P+@)՗byh\T3r3W@4EO;N;ߍQW#']ccS˾x 4pnUϔdŵ{'Ih1%M qA|qį_?*̚0&_1XP8ǢbM@C)ñꢜ٦ƀeQ3wGY3 *2?y`N^^.tCsPAN'}-Dٟo^us+㽱 \ǼYUL\xMsv*p1ͥ BPyYkB.k"BK9S`JNɡx{xq9H##6I,iJ=siqwv}k9KCʀbX NT r7nӋ"(O;Y3nijr}襾dm_$<ͻp f OMA[\xq#B&=B-d2F>2]6a?rTEST(@5/ul埓tXBӇ#3R$[F/9m8w>P_^& v\3NOWo<jD 'Yx&Oe2AvBxg=:j Z%x:W.# ;yT>lo^y4BAľ:G+y텞ǒV<ƏSTDނe\Jm]3q`e&T-1Zl6W.Zf8#|Mo'' wd'Y"WbO@S >>Y?T]՘f{ҩ#p{sNMDWOfE1WZmtJlfO#ϣ>fniSI`xZ<>ҧeSlfe`a=p2j*]]jPV_^ C_m=944C$rF|,J7Fra5̈́Pg<я!LBŸnOjsbVT‚gy8ycBΈ< w=H|Ֆ~櫈kE;(\d(7%_LrG^CAP{sO={c˝#f\ixpKW@$^YH,/mC1T{I's25Rfʜ#Y='eNj7v9̐#G7nl+0p;ix"mCB-{  ko҄mTrY>%pSiWQ/=~+Ӷ䋁r9 >7\`>pB J-{it#i?#kmb.5)O5BffHO֒ ZF,h:WI2W0`U'w%MOH)`D|u1s+R|5ѡ},YQU͡]ir)^||ջ}eCb6afҕW65JNKRAϲbxֵO &C37#zLAWIOBG2lJz2( o\'D*LWI|}B4ٵɬ%6㎃^tSkT/7~(y#ҋ"ɦڗZ~:dүz k#'iJ?\cd'uf[٪0cLGZT!V]OL Ռ<_}7/6$L|ݟ$ ;.QC#fj+J֕HXqJW n4ʼ[.O]4EL{BY]\@YƓ=,qm>+ö*9bg wI\ H04lg̺h4s'J\WOvCi;`Ob&J6 %Od#:6%F{uLZk e 7Z:^ُ=cU4}T|-}t&GpK1a_:l2ZGieD_A&"w}CbBfX;_ڒ8P1մK+?% (\ ǒ\ 1'.1&0=xarZ }d~FKl6L|Mb"p~o~a9ݜZijU pي@sid)W ˈjrDJ=Ͼi+7b.<>Or>y Evy߄$E>^8 X]HX`cSƯk$T mJB\f~*t4ٜHٟLܯ*U'v%ĈieQS2{}ʅwy~BL"soƫWs7.; ^ϼŗz26uAȂ@+2L ֓vF>r] yT m{h἟LYCLDTP]Nƚ.~H{mZcpVzwgL'~t!j-uy/4K%+y BBRO+E?ڏ_#Zܲ ;:2 ◑\-1]CuH( yh-Zw&> endobj 9 0 obj << /Type /ObjStm /N 13 /First 86 /Length 743 /Filter /FlateDecode >> stream xڥTn0 }Wqbɲ X.K$ yP]51řn~Ib`"sHڤ  p&O$(S`PC:ک`.Є~f)hFd3@.DdXmRFdj.LpanK=`AѐpSYK-#1뽞cTl )Yk-jk͘֍\n0!}!b3 tcy}Ojw+/qY7ֱNr-&gsvczwӳn7n߻Om ߤ͡W(-_ endstream endobj 19 0 obj << /Type /XRef /Index [0 20] /Size 20 /W [1 2 1] /Root 17 0 R /Info 18 0 R /ID [<81B0B7D05021895A491A9F9BEF85DFB9> <81B0B7D05021895A491A9F9BEF85DFB9>] /Length 67 /Filter /FlateDecode >> stream xǻ @Q. _1EOcKA C*|>gt]ȎX|7 endstream endobj startxref 26048 %%EOF leidenalg-0.11.0/doc/source/figures/karate_modularity.png0000664000175000017510000022346715101156755023031 0ustar nileshnileshPNG  IHDRXXfsBIT|d pHYs+ IDATx{|ufmffis(D$RD%-OrKJ%a 3;ف{{|ۮq?ws}im}}o"B!b̍B!DU#B!¤B!PXB! K!BaR` !B(L ,!BI%B0Kc' ([dd$жm[5vZB!*`&; aZ""ys56#/"",XR\=S&My;U!eK2|Krˁ@R. -qii !XB8< ջ,|&m{79!K~X{B W5׺5;H%B"Ȗpp+gP6)!:Fty6"vnDa[IL!N%-BX\̄{r]c !ЙXBQQ&V{!gC &B'R` aD׃2P&BH%DFFblJmC &BR` !B(L ,!ˋ1ځ~K qpw`ꤩ7{vɕ+W?3B"&,**ѣƦk7mDNNSN5pB!&KB0OOO7oγ>K^,&MDnn.}3BQ)0qԪUKkg͚ŵkسgBQ)0q999ԬYS}];ƟǬBG ,!L&,?=e%ٓK*[!XB4}X#FYfYF/:K%’ƍ6m3:KԩSIJJbΝz}BT'R` aR?>!!!8p@B@ ,!L ,e˖gN>m !DU&&̐g}ƺut)UXB0C`=oaBQH% 3tK_~aرdffBQH% 3FK(BN ,!L:Xfffau`yxx`~(, j֐UЎC Q`GXZ(**"//+++`H Þ@ !ЍXս..rJtbdoMWNuYTf,G^[aX ,5e?Kg g[Bh@ ,QԶMp\_w| A[8!N>$+P;X-СC:u={>=F70w4Ƣ(q%[X`AM4lήv2}!XJW1ce? .2-@/y3OE^>lذoǏ-ѣ={?,3gp?VVXԶYzNgg[KYb !B aV/?éTu'f_}#]>9s搗G~ wU,/_Nbb"G)wM6sQN͜Gܵ83t333xw8x >,/iP_BK:XkժoxÛp!=܎M`7k_榔| gθjXΝcnݚ5k֔[\W_1u⏩ ' `Ȑ!{"BCR`jƦw֍'Oy+8~8!R+VUfV^ͺuؾ};#GVا~oQIIIXZZbooȵz"(({Ѽysv֭2.0 R`j > 88] 7Hm "'ɉ7bggW=qqq_n4. |e sΩ/`ɒ%_^xA?ܽUVe3xpePa*^+nܸq P`ծ81F̧1\?x9o_|w_.3Fll,cƌ̌[ϟ?Ojj*}} wؑXӧ;vLg !>H%=uX#99͛75'Cus{_4 thLuE~׵ہ770c .]ɓ5c͚5̚5둏WD0tPWB)DnK7|z)77ܷyF^.^z' պha;R/s]vލy:tGGG|}}xbb"VVVԯ_KaԫWdzvZsBH%MK||L6 333>sd%Ov v~,hWkH٘Bn3f fbƌZQT<*ꢢ?6~x?εk״K!t!6&O5k֬Q<}X+\Ak$ 2@s7h۶yl۶޽{ӨQG>Wр{I, !K ,!Ю8q" 4`ʕ+99SEnѤZսTˠk!XB}0n86mZzGS$Vs<E{׭[ǔ)S,.Mqrrٙg> rJQ!%@͚5&$$DGE˖-Yd"賃[o)TpCߖ?̄ J|||<666ԫWO,]TBR` _lP~???-Zs.`B *8Rֶ *,U` 6d5S!%v y'ٳ'oNq*(Rj smo"""'xk4pW߿MF%04)-[ĝ;wt3x`̜9sY+ C ԾvMJ*mBƍ3|p]&%I%D ԯ_?F믿`u\ ~o.z)ҥKiD0h (,|tɓ'g5+K*zɋ/Ȕ)S4W274 }:ݫ8lmm[V)e%0)(A9t”)S4iF鳃>8W 4o̓}Tڽ{ҦM7o^u.5йsgB(E ,!J[.+cǎ̞=_|Q{Y`5lؐ#b9E.ǡݺuSTf]AZZZ.!J.pB -**"//O==m~HSצjݣ @ll,vvvڪu}YIOO͛e^#KB}KR身{yذaC )`ԶMHZP&i8lif4߄&r?>>cǎ4 tT[&ݻw)#G HNN_~jd [[ B!>H%D)%::Zϩ[.[n+<̬d~zNZfm۶U;Q? S`,^%K(\!K2̙3ۗw|+ {MFضmA%zXz8|Xݺu0a՗XBK.zp_,Y MPB~~>Ǐo߾e^So&v*^N駟xR;E.֋/_ea!D v e?>7IIIy^E˃*YҦ{_T]CI.K.U<!D#02Jvv6#F`ĉy-W`3{lF/1?X"_~X=+V='33 .Э[7)Ϣ` /˗/W;JR蜒UuXs %7gorғˈXz9/؋6mҶ 93e06n\oQ`lٲk*UtyՕ"nݺU3gΐO݋?6rH8pZqDбcGbcc~;QF1/By :t([{nyC ,#ٱ ,--]̐!'ص6׮eRP_~\>9{ְCAVVVb畷_~͛tyPႳ#q-[ʕ+IKK+7޵kpvv6檚,L<{ǬWe1e̙2(XF0fLO~ijUcB`dX,4^y?Kh6"e.-²|l۶?Sgq:u%~wZhA-JVy,C,h6UY-99:@F*Ə槟ԺY3!&f}6Oىr ,gi޽h mUJYzuW}OΝiРF/aRSS9y28o DUX|4N1c_}ǯ)))Ys!~k<PfMZnG}Ĉ#_~׿ ܼyǏyCX]Kޫ3g/(P$/p99̽D&DFF7ϵ۾={Q& LNK?ɓٳG)))ܼy:h"]ta׮]j]'`rJY1tsy0cS|Q5 V,:2%K^eNԁ YH_jBY*`?;wu.˃*׮]I{JNJUu䄳3gϞ辧~H.^̄!d`<AS IDAT54XWTR`@~~>۷ڴ)_4c0ʻ˕+W*VADΟ?OVVjc=~3FeB/G7;???#B ,BXm¥K7$/Sn]4iBXX^iKe7ߔyMBBjjß2i$BCC  ӧm4?:ŋd32e(v(goڌ=[H*<7յ{U+Bz7uy*tڶmKAAgʣ͖ *7V3B㙞33c\Q}Hemڴ!:lc3 i4m ,qrrbŊ:-ر]K&LP>|W6p֢2ˀ1ce(͊TنWWw5k#rDT+** 5kưa۷/oZ4X6˄m0ZQ:Mzz:7o:Yjllñx'{{{‚?f/EhݺwvVTR`ظq0hb-ںaFwЮ]׭[BCCIKKSƢ4;w&((HWˣ 8y#aׯ_<^EJJ jTZeooϜ9sXhq2!R)ٳxxx`gggtD5#|XܛmxXΝ?U4!AAOM^CJոqcڴiCjj*W^A>k @޽y}O]}>sϽH˖-+eiiw%""7nlzS=b1."""]6M4ysݻwFL<7>@rssѣZڵ+'Odذaj]_-'SN8;;OWnnn꼳{/̯Jhh({@._Նkx{ѿcqvW!T{nq_fff)n2\\uQvmի/\p777իt⿺uO?hLK^ԩS'jԨK/Ė-[?f}]ӵk M+re033+y?9JҥrrrbĈyQEQ5A5Nq*6n^R bcc)**SQF&KFBǎX-r17nLVHII!::|FAAnR, ,o3vXۇF~qtt^NŅ &l2sЕsXIxo{kq`50+O`4o*maaGt죥WbeeUjVCJ+)n5h޼9S7nx䗍;nRd`ΎmRXXHXX:9Jv^E^gQÖ-[̝;˫>]0+AcB]vv6!!!xzzH0)R`Qi+%X%9;;IRR׮]@ՙa***y={-M ,BM"ώoϯU$VUvMi޼SLR`I~~>.]׷􁍖-[F.\\\ ..ׯ++yu2RKXk֬ydgw^tTVZ… ~uݛ3gp} -A@ݳʕ+ԪU c"D2WV U^^^ԭ[P̪ˋ۷osmc) yԯ__É-uI֖#GХK6mJNNNoR`4 ;AD`BB.^HF4ZX2lll'66 Ӿ}{rsstYYYynUv Jt^deeɓ'ѣZݣX\rE tKcZP&G[CWL ^Wy ˋ:u;!"T Wwwwbccq yI9,%:XXǎֶEsssN:+꣬9,uȤIp:*KǎUkg?pȺB+;wpuڷoBmR`: fmmM˖-ܹs$''r͛7jE>81ON{R8]dd$ 4ANEI*pWiԨYYYddd ЪU+lll !%%)R.'N燵unܸc4w^f͚Et*D.I\HNT{)+"{g6gNuoFL\V:u*Ǐתsx-8իyW֭K,tRMFΝ9s& xڱcÇL}z*Uwx9E*BCCqvvJTz2֬Yֵ.iS111\|777~Wm۶իdeeQvmbR`#77 +bffFӦM$::zU˳6lHzS{uʽ&88{ѳgO񰬬,:=n?q _m`[*LKK+^x"aaaԮ]ڴi~k׫W/ƌ.;s [l!##^z3gp1zVy1qDkЯ,N=7ÞF}qss#77H>._LÆ 7v*BXt%5 733yZ6[[[ڴiCbb"ΝͭUaiKV@@̬֬YsgH,,6݌m-޽{My{{S^r3g-ZTaYҡCزe uᥗ^s|^utBgG3lݺG:VVV4lؐ[nU/Q6  ,]h۶-\rcd0nנmKF뗫L0MNNNwެ_駟ꫯҽ{ +cmm?\?3#G,Xu=R\ 4 5T~srr"++LW#ŕ2 MUudqwwQFDEEg BfAAC%iS`ү_rSz T)9<^{5 P… ټyss| ',,kGUX|:TY =Ū۷oG۶m0v:,+) :uJR7dnns玱S;2&)8Aʽf֭ <bW$7?9Z':uj?gee~ztݻwٽ{7 .T{2lll=nҒƍsMaةa0R`HTTj^`4j҈"7W%ڮA`0`rG N(>$BٙI&;b qj֬ɓ'1c-v֍p޽U>u$rrrHOOڵkӴiSc"űcHNN6v*&K ,vyΎ5jTod 5k#+3$mjuɓ'5GVNNO.wZMEK۱?(!mą &!! YK$00ǏѣG4ieP RK+^Tn~u4ǧI֭3];E"h6 ¼y_U`CqƏlRZn䕅jkԨQj]i+ Mk4ҪU+<=utAL55j`gg]粳޽{Xvm]6jѣ|7ԨQ^zݻϙ6m?Nyz{{͛7iҤFhРw%--? ##7nжm[4\ §sgOqMs>}|}Y ScsުL ƍԩSG*뛄갴"""̲sXt8EDI\~!Ctb] ?~#VOXd% tXRV-4h@&Mhٲ%;v+++233 ԩSܿ3gb :u@͛SOj*rEeBM ,K$$$S+yByu (zp-7Ӧ/T:\ՖKS^!/j=Ll*4ҤU>gJtĩfoi5ѽSvmxW?hԨ=zQFŋ5jqqq;vL\K۷ ]",Yf\vMgí[ΦENE(~A.v߻iH}[*) ܹso^M6%>>JyҸqcHII!::|b1KKK:t@``Zk*'NPV-Uٓ7FsM '({nޠaÆ?ONN_|ݻw'11~{www֭KF//BCCyYp!7oԸQqss5QfM4ڬ~)2?OW^ReSa,Ĥt;yV*#) @T.͛7ޞpV4Y&T{./_.uq1c ֮]ٳqُL?Vp-fX\=r; 0x`NxtdVZU|$Пٳ]vgggZhA࣏>b˗%55UbժUlqww'&&F])<<PHXX7gq~ l'%Ҫtҳ8,--qtt9Vu*Th۶-a42K-tA{?ݻwsu sm5L>.ہF qcq\9tzuwM֭9v;wdڵDGGxb^z%\\\8q'OEZjѣG ÇJOO'**E||2՛'L[tT>l֪@ ,=SbJK*5k,/^QFbYB]È#ʽN۷qF6n /CtêU=z4zÌ 3?oĉlٲ"&MmSn::uȌu}`"ʢ]vر",ѝDzz:FAWPh(O)Q(Z"Kzd:.~k׎|4vJRwKVY˃oW_:ϲ_bʕ,]T,}=;w2}tKƍlj'ضm֭QU+++ׯK<ѣ9qpY"##˝ҶbXrssqJ:/ֲD!:@k;JV6ң$pqqQ$^Zptt$ma...xxxMvX9,u:Xe=mݻLj#8r;w,Hulْ˗/?~ӧOl2V^Hw͚5Ԯ]I&:N:˦M4N:Ӈ˗/ӪU+ڷo\sY[[T`թSkkk(>[T^PK@FaaӫXzesXU^^^ԭ[P⩈-͚5… ^WQ+11Dڶmo޼ə3gxgɓ 8 *򒆪/W^ܸq͛7d7o^&L`ݛ sL:'NT4%133GGGM6v)))r]Fbb"wI\]]IJJ2ؖ-EEE0}{P`MU.R`鑮F KgooOҥKjYgH,V*kyPm6nϘ1cꫯkiiIFF=߿yKחo+T޽{s̙2ﲴnݺ4nܘ͛Ӹq⣀󉋋ܹs\tuv{7Ravv6Ν󁙴:/ Bv'‚+Kexˍ7Ljн<uJ[<{,:/ߩL6m۶1{lJaҥ<899駟2egQfM^yŖkժŲe˘;wje 999\^^^twwׯsݺu;w<2Uvmԩ҈CB.*޽{G՗m-&OV R#qKce˖r9>>>\~܁:XnݻyծT***={ry6mT1< 㭷bٴk׎,jҤI̘1nݺq WWW{dѲ"777Zn]p>:֭‚)S(R`h3Urؽãs{Ow9hc Us5s5XXXY{~׼2{6Ӈ cἷt)?=J@n4vꡇxMMߵM`]Bgٿ.߱e,/QQ:5TAW‚ ,,,@1gZVzz:(}w}*--e̙\tꫯjP>}UV~V߼Uy柪<3,ZaÆ勺= oר*T`D"Ϗz<==URk׮榷rNIQ C!!}9<˗YxMX?}:O ۭL-0>/_,2`DBSB"Q'wprssz$ȻVxsBCA)GDcccũSG5䄓$&&q&J ,eu֩<9'o6eeeٳpSzv;>>f Mw KULKΓO .U?T>0TU~ǘ1'2(//'-- kkk'ʳXV[;W>6l`ҥ}:ܴigϞG޽[KKKٱcvb9r+++Еyf+/^͛5^7`ڴiT2g.]9:D"ɉ7oU$(//WlD$%%5hiin_?Z1 _=6 ٹs'̡98X[o|-ܣчB|p,sx"¿H XZZҧOr ^^^8::#F`֭5MRQL&èI}}=9.]bȑ>}=XG1x챑Zpqq!$$2t]Nhh(QQQd&[kp?y$'NT|Ic{BBFx@-q+Xd :u8T}Y 2/i&k`…ꊻ;QQQK+Η#HpppS兙+**ׯuY<:tP 6l2~WƎ2݀A` LUUG5~U̙yGhP7ooo<<suu ٙjJKK%::OOOK"kkk?W R<'&&XϧLΟ~bb;r~+h /P^^NDD`# %0B_EFŒ2"" VwՕ`JKK~2XzgΟ?X,&,, :u*eeeX[[vG3T?3:_|Æ S;SHƹsذa۶mSM6dɒ6ϱ 443g(}\ Z ˉo߾XZZ z/?W޽8pbvT*%++=w3DC .LuNJJ L2cǎ1Ű3Pm K``GvA{BC!<\CaFD">>>Lv0Y=z qe ,UG̙3 ի<_v֭?xdll_Muu5˖-SZ???O'|ymuP1SXXHVVDa4 ,7f}wP8cO?qwpD]LF\\ 3kOII0 vCׯ"WRTTMGgbccÃ4iHVƆ 8t<36WTTi& FQQ୷ޢgϞZ_"MXd cȑ\vMfϞMNNOnI&믿*DmjjL&lHu[8::RVV۷ ECҒi*3ʊHWlIw#GEQQ 谺% B_YZZr]Nff&IIIͭ[())զTMqssjNs#x$ .\^VCS:Çsi֮]5?c~63Qm5L}w{Cs5x0pÆ lllDTUUQPP@ZZ111\zxnܸAff&RVV0664ּyK.nʬYVNLL Æ ۛR=B0]~+WOӧYpaʪg͙m6ܹ%Q&LHH|||Tn%CƎ%B[5pWw믿h8~Z; ѨDFFHYC<_RmL")W[[T*U1SSS$b-vS666ߟ]FϞ=SzHDhh>*HLL wwwضmZIg~P (ذa'OT|}ر6} A` P+9'N{ٰg-S?M} \˺u S{bb4ќzتF%'̂k&2`rrr fΒ9T6 . m9'OdݺuXw0qGԼYSSfc SUUΝ;? t%3Xr9Ov FP\ge`fz?UgKKKquu???&88???\\\033RIJJ"22055w888GgSfCs&'. $$T㩯祗^rJNgbAڹs'nG-Ys串݄BeqppPUE^*(~g]` lJ*x1 AؖmϜ9#!%׮]#((HgMܟ~s~Naa&  #F+ =x75kavQD"fffJ#ij#hU3s^l 4_vO-f|eO=T; r_̙37/Ri9/`„ =T^^^XYYؙJ榔ְ[nq" 67 YYY:iynz@deetGGGwۗ틗"J),,$##8c"ܹP~]: ~~~dffRSSѡ/̔)S0aM{饗8z(ׯ7 ŋpET_VVUUU[+++[=Skmر˗/gݺuz;Ѽ 6A`iIBB^^^zjX܎V񓓒a@7u#3#n޼)Ђ)ƍ~`ʕ|Mk^Q_ËFb/aŦKPLL;ki>7nT'ø{AAAZxSÇqrrbԨQ*okk7ѿ~GG?s033cȐ!MEP"]l țtD IDATA&ܺլZ lmmֶ?;;._#otׯ_G6666|w|Ǽ|]nvvv<pTϨPjoƮÓcC;ΰNm,¶s)=z4',_ӧ#<PKLLT'Ғ+W5Z[[K~~~fu***?П"Bme䀩) .JKKIOOW|-'k, 049(٭TW^~cĉlٲ???6Rg ?°V.ƀl{8k-O=ׯp]`nn999:{]y裏Ծn̘1XKbccã>òz=z}R]esvv6TVV2dȐN%-CsQ[[KEETVV* owK-o ? ЪQkcd2Y\SZZ333EC38rw^̢E?6S=VIMJݝx4~h?(Eܼy=zdFOkHܹsR122һ166nBeff*eJ3em2[[[j_Ümpk-~8ӧM$6Upss#11rʾ۷ogذa UM:t(/2UUU ,և9jTT׿&ˋײ5Lк ""W^yEbذ:f L&kR⫨Z/effZd7&55-&lCvU-Fa:gf>9X[[ӣGl*++B:t(EEEL౰0 ddd`ee'vm1dTCXP)J|6669PIIIח{s5k@E_ZŌ*CuoFWWWjkk)..4BBMȎ;T:yNU***n޼I||| QV^^(;q!p~yS\]]zt`k ںu`Qf**ĉ̛7O$پD- @l,mƜujXW0JB]j\366V077כHKK3sL&Nȑ#G8I{)U!('bd /c UUU 6sΩU466n^ZZJii)III+ mG<[A]]]&K Tu( zpuܹvoL4IP/1cƴl ɉ~{W{ݍoⱢ">|xD_z0 R]nn.vs]]] EV 777,,,:gTRRBJJ ܹsC㏓EeooOvD6SfMW繓}PcjE.j1O/ɾMmm-fff\pQFq)MDYOOO*++)))!++& .%bffF^^zw[D"UY=̛LLann { 2L?m#&&|MBBΝc׮]baaA>}Z|]|9K,1vh/{99u,Bܾ}[B`Xd>NRTɩӸ# ?d7}z")ᩘNtt4nsvh XLFFD".\رc9t۩ܐdSXXHjjj>Y$˕0SɦC4d?{AFF^^^*]ke#/6X5}c]H***{>~qz9ǭ[[Ѥ/ K166*[d]gRPRRBjj*={ё]vqeR)SLaܸq-? [V_),,D1.L4]v 899JKKIHHhKA+Xcƌ=tͨ!L` ye899qƍ&ϿXv-g֙1cX|9/b/"Ǐ7VX~}a)))Z'<q} Izzi/iiiMf.\ ''3f%RRR>NJttܷӖk;tD N>`laa# KM:cfx]xooo4SSSnnc6nȥK4Z[,--qwwO>`ffF~~>W\!55۷oS[[wC`˪-<&ϪW1>e<Ǿ5䐓(all?nڵkm2C2 G歷j˗{{n=FyXn] 'NиUA_l8v GSSdh7Ԕd:A`)᧟~[{<" k׊x_˖4joSLGP^^NJJ -y(//gj7kܮ ggg)//W:O?'՝nVVVxxxP]]MII ܸqCIUə_wJWՄGs& ұbs6޽{+wwwWd7;;;cggGjj*ѣ#jժUroBCCp#Fhx`` ~~~Zb0773gpȑV)//'""O>DL&GaMƗuuj0S"ŋxz eWՌg͛Y:c+V4jj}oe߾s89iS7olu4O]]]fffbLW͛=//3f oӧk>|Xm^^^˿oVZT*պ,K$ ...PWWGqq1?~}*JwC_D"aQ9 hpsҾ4 &LLL //>^c…H$nݺ^ gϞU*!5o޼Nc+kliИLf͚ž}3ge55<1z4#"lV6Ąsnn\Oz8}@TZ%Çrq__XN;ҧ7 :O^"dKjj*qqqDGG"$$>}퍫+q  l".]ܹs… Wbkk`1cmmM~~뛙޽{.D8880`#|SDB]]]SPP _[\]]  ##l]لǏcnnٳ'rƎӧ[}ёx[ lױ37_t˗s͛? FF>{۶ ;b^ <,ab(;;xB\!woWN$(HkƎ[jpu d܄mB"==;wE!Ui(۷;w2f!B4+ӓWتx~7Yf vbǠ ; M5=zl2yɑH$TWWcaa+ƍիK\\:희wO€Wڒ꽗/_СCy'4GW+ʢ)*9t'O̜9]v5Θ3sPZZJxx8#fa xw.1,ॗQK\5!)3g9=k7WVV"TJXX]ӗTTT9\| Nb̙3Gr Ƚ|}}[=W_elݺ~Z'qI`` ?ScfffJmm-ؕ Ύ2tTTT4izo7KK(dҒuDNWXD"AAA-e2&M"33'O Gsޜŋ#H:8D.666xzzҿ|||DdeeMzz:ŝ#Hn;>>>$'''Hlٲ+VzNsR}зo_rss),,l} TZFn̵kװT%>,#F`…|DŽp}utXK.Da8:B]]SQQQ~)vCjr9~ Db"a䐑AJJ saĈ|gēH}}=A֞U0C .dݬYegCVsW _RRBaa!(J%,HԔ6{MϘӯ_?n޼Ibb"~~~Z}[XX`kksPHme~CBBZ]Xyݺu|wיWuuu<ì^mۆ%?xG&(:Q Bʸv-J{{{߼yC=o}d/}_~4d}h~zŋ &@whm2x Yz" !FbcРA899QUUEBB PQQ!PĚajjf211ˋ7nv=$!!ݦmg=ݝb}kܮ=,֞={xppphň#ؾ};!!!>|l,Yѡ NXspdG'==&b`͘'gH-)>i)鿧?g=k7nʕ+xꩧK T/̚5~uYd{b賭-={dڌ ]FFFF}l@niiRߘ-䨴% @*rud2ʱeggw^^|EwP$ƺ^9s!C(HO.]IJe˸x"...={?MgLXכQ^"^Sw&pvYjր }5}Z;{%G,>ĉyu888`lḽ[t~/y[^s_G5\`* ڝzJOOnàUe„ L>K5[tQ&T;;;0`o/--233)--^!bANm克 J)))0 peP%b#{%ӓjkkպo߾^3f(2ѣEEEU VVVxxxзo_z)DEEJaaayPY,hhzOOOoSI6ˉ}.W\̲B,v6U]s*cqyn޼ӸF<Β%Kxb1dVbʔ)-΀A`+W2y=O/3Y{1ocb!O||! !(TYQ`Y,hhT޷ocǎ8*9у~X,&??TJZZR 3'vvvy3S؝Ǜ hpcwD;FU3/GGGhHiiiZÇ'22Re/1:X$,,L*;)l?))Ie;jDee%{aժU ={'ѣ+I9{6DiCw.d2_NUUM'(}ܹs$@Px&q FѦ9.̍bff+H$)..$ڰ I!6+1{xؘ޽{cffFtt5hSHP75`\\\8uT:SPޫ@uUÎ̡CwtGw;;Ո?ɱ={2m4ΝneeŨQt]˃T`G r}a;wuA+‚?#FhdE)w[5033www$ Rdn7+Eֽ.4䰠L3}%;;[+o+] ÂΟںu+gVk>W<-jѧ'|pmӊ>9s&iL͢d***ZyCe(L~-kݔ^RR•+W7nmJа#ŋL8QP7Xr7[YYQSS'HDVVѤ+2]y٧|E~;b055%&&Fݝ~~~,h1f{Ĥ 555lݺŋu.:t([laM+..fѢEgQpРAW\\\T29ZkXH>}kkkLLL(,,j?ӧeN tՖU9nnnҷo_(,,$**7np->~}//_8#UmB@@ uX`mmM^Rzf- nUDDK,Jv'vU0, ))T&Oubua]]"k5p@lllT/u7ȕ|;Ν:9|0SNd-]-SN`u2aceddҒX,??? UUUf4|kOКH$"66VadffꤧO, >,_|hJ~~>ڦ?=z )Spte ? ^yGٿ"jnXo&::ZUs-`cUh'=w T͉յ4Ǐgҥ:B`5.v ٓ، Behu5OV\777zMZZڛ(tU*Ԥ ੧z-&٫"1b|DDDyf?|ǂt{uejkk5DL6 Wqq1&&&j56vqeeejTU3;çi@X N/91HK ^\AonW&à[СCXBfQ`c8,h BE x7R H/5PWWQ/giܩ=]UEsヒZ)K,DBob#\$">䆩maee%yyyZݳ1&!!ALȑ#dZ}i+sss cӦM 2D9{ j}n+<Ȱaz衇8y&ʸ[Vjj* 4;;G0bccؔ_w%,|۷]5k}V; A2a eyGEym}w쀈 آQԘDcQc\1hQao&b  `C@@DEC? 0>Z,ךy9{pw."iwP($""4`vHIIiMu'C-YYݹvZϞ=\_O>p fϞM`` mJspAAA,ZHMRUQ/^D-رc]tzccc9vZΞ=T=" pM i@%fff UJi۶-U+;U8x`^*Z[[[LMMhe7zD;}4ޜ:u 555ׄp!V\)9M&`޽ &T.N6d+&& S㳰`믿ɓ')"Ha̫6TQnjwov+hۚ#ʡC@~~>i޼33_EL޾`8p#G5TQqm۶q-+ٳg,_m۶}NS9X"M6`ۡCڴi͛7+//{67|_DEEo#㮒i֬z"'' uNL+P#7nlڴ 777444d^{ "xٳ's̩7xG:Mĉ|G5a Q8ѐ҃ЬY3+7ˣ{umQv{)/&))]ۗz[[ǭZD6 ƍhjj}v X`͛7gs)Zh|}ٙHHDHD/_|A+CC"d5Y{Uz쩐P}`ƒumNr9\\\խkSj eٳi޼9\mmmPUW@:WhӦ F曵k+ O͛'U[CCm۶QXXX1]v,_OOO.]ĤI>366W^S177WXSRF UhWQ$z9s͛7W_ɉaÆm[}&|D"M(dЫYB!g'YMz)))rQ`eggs]iRQR{333rssNTG}DvXJZJ{PRȈ:%seڴiߟ[XX/b Ξ=˔)SyRSSCCCBBBE +XqdL^źw/^Eϟ?'==]ӎ; *E}v7oĉ岫>ddNP4E-fP?L*yM.YWe߿?qqqr}*))!11Rݺٳg$&&ҽ{w4Zqqq$$$Tצ 5*x5\=%200@ Rٸq#۷o/5+K.ĉL6k׮})$''pP}P}JYnr~e˖U{ɸmW}eU;*9FV\Lԑ#i]wӣO>zQ޽{ҹsgZ M:= ZǏo߾|Ri,K٨m6?~mmmYx1f̙rPccc!"HBuEJQV˘(T$Ղ ѣsέv+WpUmdg%KYX( hLmU)v$&&ҵkWh0nܸ6VQ0hI[_Hu}cw@rV}qJqsscƌߟ wUٳgnnnuV˖-֭X7K)Q'''7/ݿcc*ƍcڴiR}1 eǎrFwϘ>NHF`9s;;;:uTg2l0Μ9#Ӻvrrr>tMM:GϞ=հji)m91cHU쫦Mê,M(o{Mѷo__͛+ƍqssۛ p=ԤGdff 8ה",Q,{ۓ0`^^^ׯڽ/ٳgw9CO%)=Q;X9X z9+44T%Qà%ɓhQ,eJ54lݺHļy*O>x{{3m4qdK֎~q1ʨnhJ]_hC1|:Mo7(.\FVPWWÇR)+M/F/&Ν;8q"FFFr}100(ٳgRˎ5smKC8)QoI nݒF`UիŪmV@S^RSà%ѧO>sLRṝwb3؆9s~ͷ߲4g)o&lZѾg{n][64н2 ۻҔa){waٲe,^XR֭[cccCLLTM333LMM60>cZoEEEl۶H4H*n߹s'ׯ_Y&Mb۶m4kL!{;ϟ?gHH(m۶1m4_*#,xՋU}D`M6Uݻwgҥ?@N{>DSUVˠ\1EiE$؏'3;)/떼DRu9;;0j(k[jJ,tG%MXY V)Ǐ֭[Rl񥿰???\]]ŏ/|r5% IDAT?>_| k窣9Xٳ൉IIIxay||<ҪU=!%[kS*3 u B,Xd[ "vf&lq~MѐR̙ٳ۷T݃Æ øҥKjѢ⹙mڴʊh+]4!HJIIח>@^վOҥLQcV;m%CltV}I E4aM>D]]{{{UԪTѫ0hIXXZpy&Ȱpeѩc'r !)jjj{\SSMMMtxGGGnݺ#G䯿̛7˗WfeeEll,%__G׮])))!,,Lbޞt9;;-Q,ʺ/MN0S$I.]ӐÃwy5@_ f066F`%''b':uꄑQI+!!XlllT)B*j72ͅ-Š'??r%CooodR79q{fƌ/{I۶m)iӄեK.ATTTՑ̳gϰ`xzzҿ{pɓ'5kc<\8㧦Wՙ,>2Uײ U!M&aIٳqsso߾ܹsGucƌt҅ɓ'믿f͚MjjjutPHDDDhӄ]v]vJ|^Yѫ3gĴiIGDGG׻쎢DGG{n&MoͪUeѢE={G2uT~ݺoMأ<2ˠhH?~𷄚7߿Ge;X `mmM6moS@`UNMX~)_) +\\ +`QҘ"XӇ[nm۶j _eܸq;w;2a֬Y#, >>BJ^N#h۶-/;oܸ!N-*ʢX7oޤ(|ѣG̙32޸qG湟;wɛoɜ9sرcZZZsi<ĉi޼yu?x]]ϝ˟Nɴ8XU+vWϣGҥ "wޥyXYYյ)N] ^J'68DKKB sttt())\Ca˖-D"d.8q"ϟM6[_R Ś₟_gI"077g8uARRGO?eСlڴ+W`ooaݼ{R1cY'_f(;vڵk@KK:V]vEOOIw&Fqq1gΜa̘1umJA W4@mX T`5:Wӧ7odǎr:u*W^EOO{*ښ5kFϞ=ѣ.]RcÆ _^U駟bggG|||9yix7:6y[neʔ)|G?~PuƤI8v۷oDs&͝ ?}Ҽ93M| ޢ fuV{%޽{]"QWmIKKZX)J6JAwQi{jii!R؛l6kX$"P{Ȉ1(͛QW{31c$>>tfUqS޽;:t௿bذa ݰ2m4FÆ Sh:Ƅ Xhr,ѣGs̙ Ƶk׸r W\###RRRՕ#F`jjs58ݻw 环/{>[7G!CpppPe[xyy)4ټqvvfժU:uNzzF9^KV DGGӬY3uV 7~?η~[f4IKK#++Ki#7P[-M,-INNFOO}}}g%QWk( 7! gёaE0'OL||}kJrr2YYYdff*Kg}NA1vX<K.y?dڵl%+++W]vEWWHDfNUm޳gOzɌRhqtt2"8:ϟdŊ Pȑ0h_7.Pȓ'O044l0iц]>VVcE} xC.uB!<ˣӥ_giiUʦMؾ};sa˖-r K ,XYزe Ceܹk󉏏m۶Mff&Ud\>̪UXvK_:TnJ`>|xh Gڵ+$$$+_}Ut`˻`&sNEJ?dDF>^*FDDо}{ڵkW7=|||d;({~ˏ?Vx\[[-ZЦM֭m]]]ϟ?'$$PbbbHLL$++$+}6Nª5kcܼySX#)..WWW= PACCCBBB 2555nܸ!պݻwΝ;=.i&ڷoرceWYqE~ ͛ Ӈ={F׮]ٱc;w>hZIIIk}r֭6C*V/ɛwٻ@__B7YMӧOs֭F#WdggHΝߤ/'q(|! v^RȎ|߼>>\pMEߊM&M7"?0?JLE)O>ŋ*J XZZ4+5*s;sgb)me)MMM5kVDA$'veddf檨 &l ̙3]^^^OuژWԔo4Ν;޽{qssեk׮jjjhjjVZ^W^g6o,VZ[jĈqF_΄ ʥ455ܹ3ܿ_keb͛7gܸq޽[Xzz: bՕ:WO<ҎO/ԈsÙ3gꫯx8x ;vdɒ% 8ӧO`dd͛ٳg}ʹ@Jb)͸?I (믿ΩSD$T(7o5_rMJKPsR ;g?3bYSvuuuEh׮fII kם;wr5J*Q8R 0zhq{ll,[l!((.])h +Wp ~J8p pBN:UesӦM1cƌ 0f.]R,zqvvM6\|___ :U&+hPŋ5j^{v&k짧NFFni\p___V\Yצ4*򈉉K.5ӧO}6EEE899UZ([ӕGvv6*ƪ Gp]Xn%&Ν;W,vZʦME__777 MJJ"55kkkN^áC~:YocZjUAɒ{Ǖ+W())ٙ!C`hh(\;U+i05X$&&6 ^vY [-'"((*o&Ǚ2eJ](; &ڱ---*ϼڍ@II FFFHLL}}}JJJHJJ }}}4*={ƍ|'1o޼*733#22Rԁ!C_255eРA;ooov؁ZEڵ [[jߋB j_WPPڵ\;=zT.窤%...^BΟ??.Çgݺu JĻ>`"Xc̙899յ)Rgcm}}iR7u9s&w]h IIzz:YYYXYY{\$!HKK#;;---ZZZt4vI@@hjV7,39vÇ3NHhh(F~>|8[lkkkVB__.]pVX!~N$qqttDSSnݺU!Ą4qߵkQ*qWWW,,,_E4jPPRz7S9WJDU^XZZSnJTIdffgRSSˣXB}c?ɉ!CvJStЁO:u bbb*8tڕuq=yܹXRĤ擻;C ?y~.^;|SWzn.])(*ؕ+^h}TǏpqqa„ xzz5kVT!? ڰabo 'PyܹLJ}յݔNAKK uuu X\DCJJ @fWUџׯ_O>Ν;̟?_udffQn3W\;Xjjjbnݚ 憃񄅅ku SWgʨQV\ 7@ ~^Rw9k׭cʕ 0CVo޼)v qvvftڕh.\QWWՕիWWHPܹstQ !aoo99QA-&FC')0`ggW/'76= )H$nRʚ_444022 Y111<+8^ fÆ ڵ3ge*\cmm]A Ȑ!lܸ>3֯_;SL͛[CCChѢ:k׮o߾5.vv"}ü<ްn/T"z쉳3޴iӆhΟ?/=|pVZ$2nNtt4BF/)m?=7o*Ѻ$e5GBBJY=zUxQJG(V(,,uucܹYTxŋddds: Ɖ'022ȑ#<}E[Ɣ)S ev֭ٳglݺcccrssر#o9805<2 ' -WOÓ'O5U >kkkOU,uȑ#48J f߿_,e"m;hs4O:믿ֵ)M6mLMMDѵ2)MV`֦yJJJ9]塭]KRpppڵk|ܹs¬?ccc233IOO׶ GǏ.*]]]V\Ɋ+h߾=SN)S!s0xΖKfOo>Ο?OII XBTukȐ!J̨l۶pr#Ftf(^i q5koU}Ztٳ3aM8455y}k/UF/)U޽+We˖ ~pp0z"==O[7nT fjjȈ~L?Csr{۷r/^_%E‹/]),,dĈ6-DumۆH$bΜ9umT" +8^]jjjJ={Ϟ=\|{p fAKqő +W3.{f emWu\+Ub^9X-z'VVV>|;tTÇQu#ڷoOHH-[l0iB4MFUKK --rH$*ŘA^^t`IHtBJJ \x;;;g(K`IISo+88\jMBBddd_`jthiiQXXX5)¦j ądb3Ąw+vvvbcz>UoQ-c}}˒%KzDANRΙ?p5?nhU is%#Gj^JJ k׮%>>:MKyT/Q pڷoA=z++z1d2Gy 20 !;x9ŋFxx84oޜ[wcĄ1O"-zC ʫk:uOZZZ_>#Gʙ*!C+E5 r<{ ]]]jɲCFF)))\_/X*/?kh 3>[bbbh߾DxÉ؎ PT# Hb̙,]ϟ駟㣐eߟ^ٲS9M:uk\ìYO>SY*Νߟ~MQ!%aЕSZVՋ/PI_?"dRRR ### ׯ#ol>o@$aee%'Or?0WEf~>ź{{{:wvvv<|ooo崶rJOO/'H 3kӦM1cƌ;Ϗ5k0d.\3lb̟?3fзoߺ6E AK&--jU ku9fff<GDёjhowQ2Ha ׯ<~Xblժ͛7'??O>;;;LMM尺zRRRĩīWҷo_ DvjJ8X9993K.aaaxzzbdd… i۶mǂ 8~x]B°P .CNNV{Çiߙ  133*MNN&--Ν;%G qräcGٽ{LS"$9gΜ!00P⨸X9v `gg-aaa! r GKKKrr\R'֊+w:( <==C)ф#//O5 dL=}CCrGRRRHNNןI*܎F[[ۋ`Ltgua!I a`S FM>I&I=L>66}}rZ߿ʵjՊQF"""'))IV,bTV/&&Fݺ>hE;X̛7'N(u͛7s1<<`=z+Vw^vk֬ё 6ZYp!ƍcȐ!um Quĕ'**-[Ҽyjw]vRJJJ UBWWcoo_&.."ZlI֭)))axyyѿ*Ϝ>}:222HOOںs_~%GzTرcTIII]YYYz.ij^%== D޽VcV 0m4'QQQYMMM.\X *7iiiL2MQ$"##177W!"[4::&/**"99dLLL033VD577ǏӱcJѪ~M?>YYYI̛7W&Z f͚U 0YJk"""$uҀrM 䡰ǏsߟW)*.[^8S߾զe@dž Z˚5kx! . ;vPXXܹsJB%;? 労+#))ZKJJ YYYcnn.U811L:w\udfddCШm{.s`ƍİzjcU)r%~j.%==ɓ's9Ȋ.cc ]8Br[5)0ٽi/p r--nV YZjRݺu+`…;SX3f ۶muum %"fc'33T:uTXk2呞NJJ fff2tӫڊS"`hrr2%%%1b#F/ʦ˗/j*vލyB|>|XKQUb RRr[[;\ʪLKNf€z_jz`GO`a ]+ֿBZN<'|gϮ!T4߼zSlZPj͚5̬ڙe&%Xʫ)4bcc Vۺu+{!55ӧh"ڶm[~(s֬Y#:eQ!+1(844Tp%$$>5>:qg)c|(T$PuܹS9 *Y n޼ɚ5k֭VLBox1bD]P }P}_#99 ںEҤKQXXHLL jy7`2w۷##*}~gr1e^[SIL1iӦBlŋd z)N%JU{-IH>Za8:jXۚ2lXMf,^bccYf ,\P7Inn.oV-*>aЕwIV322HNNsssQ###חZ^4 ȨuukΞ=l:$̙3l۶M5~-...9RKٺu+jjj> )FuuuNWfWiܭ[ĩĒq* `͛'}f͈Ȑs,SA ҥT&q>,^@t7PȚ5ku .dРAIg}VצATà!""mJٯOQW.˝tI\||<%%%`ddTeuW7߰w^4e{=͛ݻ+%ܹ͛'O?ѣGy RSS:]])))vܹsGl 2SSS89s)(dnni\evlZ4a۶bLn޽0o&ڵ۷pB&L )*TyXjJ/ ԇA2>8U ]ܹuJ(444Vb@A(46m:(**b7ܽOYBL4^{M}ꚼ u]X[[ckk BX133ˬCb ZG<˨@'eK㋑EZ壏`"M.^\v-.BE޽KfTǓ0AP(Z555͕h\:tYNǏ144I4eff&t~4iw)d\˗`mmMttT{Vźu2dH-[rT(Lqq1gΜa̘1umZDOO###ڔZGGG' +WndE,Z3gΜ#==]i6 B&??꣪"!!D#sݻw@CCC@۶mz[vvv3UV[oHN8{,GV^֭[[J7x "˗KG]+W_ BZU˳KŅM i|s6-[1iJ&]6mlgrےMtt4hjjҫW/:t蠴ٰRD"kH88hT4])))iu8*^bllLqqL5IXKlgÈDE@uZJ((ΟGD7OHH+++:waeS8INN{ؘ|^x!,pyUtdbhhȑ#z]UӇdzdW_122MD(ijj899ɵV*89u$}S4QET@ӌbU`-_~PLDx=z8577BCCQWW _ "SɄbnn^Nɓ'rw`;###=Z5vVGNxf޼yRQFagg땲_}eJqW;~2**@---Zl)V-o HrR !znp?Nfpppu520++S! qpp(WL@-veuhѢET֪U+rrrĉ)mԩS)..Jٯ>ƍLbAu_^/_}Dumfd" !("؈%kWPA]Q 5Ʈ$5Uh5.ƮJG:3;)93s[yyPsdV""L4nN!CԾ֭[˗*χw"5Ι OdGZW.߉O?]ׯεcZZZR'==O>kʐ@ B&M~nJ_}ZniӦN]ϟ8\xkV -aȑ?E]vET3r 6¨ ڣAR1m`ݻwO8yYP(SefII ߿СCCfTRõf5ZsTĉ1|tڿ(f߿ZGa+WN/nlkgjU1}hMTt~!bK!yDw⫚HH$L̮*+pB#kէpvvC9^x//zSz&K,cРA{nlvީS'ܺu ֭kEUر+W4 Q={b}~. Qϟk|G}U{ݵ.+Wa޼/^/^"%%2XW3`GxRSS.]4Q 533Cv=T^^8::j]& x}ؽzKP@*j6*{n$''O?U{ݚ?1c48Cv v-5ix՝^]|-,S1TXAAiNq$ᢤ>BNNNx>zJIy &_qHdxWCP011Qڑvvv ~A%MV@@r%MW@KiѣBBBSr714,@VA°ZX E xWGO?1\)hTadhZRS((WTݻu1.r2`R cakkzxڶm'HJJJ333ѬY3΋i`̘18wΟ?kuɶ  56|p`̘1/6oތ{{j*YXX`o{.q[7nImsrr)ݪgNC123_8B@ۃDpzkLmή=D %PR -,,D׮]U WΆ#]SѣCЮ4^m5~Vv0c ,X@5I.]лwoA27o2\f{*0{9 ׯGAff͝L<}zspX,K1dHW4i;*r\-CU~彖o( ],M6y}A޽f i<1n׮KýXbŘ1C3LK}0{~m Q/ ;;)#뚛?Gppǽ{VZ2$&&901YYY*ԤRPP,--~XXXhu?: O &`1vX'"+B)5DGGW]oH IDAT@ >wn+!Jyyyg4")).I_,bDڎFVV+xlshVEE.EӦ!'OŜ9! G!>>s/^ht`hcann֠[m¼<,,,{֖lo *1EXQQBSSS˗Zm2EXm6L0W\Q{]vŘ1cj*F1To#<<Æ b 2heO?+W喠WwV-Z0윜bjU}zd2pU355-RSS&GGG?GmTСCѦMlٲz ,[G1c.;;;@"p]D"d2̙[[uǏΌ>[Pŋo *1j^η.҆ `jj9sɓ'g&CAmVx)//opKP" >>CA%;XoƓ'Oњl,6mL~|~Ax8]W X߿X6mp] 1@;2 EEEBK<|-/^ %% UF!wBT YXX 00]tѸDwutgΜҥKq vZ۷OZƀ%^e}/_^kT6Q\\ Z[z{{!*++777V֯ +`|R `)YXXGtt4֮]wv؁+Vhܝ3d/pQ <KyM޽{h֬Y&99 +={"99=z3NMWKihӦ {=TTT4'N 88Xo Xw; K!N_/޶mSeҶǏm۶hٲS m֬#C PX5KW`b5o\mBPWWW$%%iU:BBBj*э~̙3w  %};L6 @ՑD;B,WК-IIIR+((nBii)=zںb@vpul۶ {i666شi&M'O@"SN\B]]s$ qqqbvv6+C֠KJJ`aaQoUVF],]t?<,[y{{#,, .QeF u2yaÆ o@HV-Aoo7)//Ç!J-:{fuv^))'9rj9裏0h 9999~o߾ T!Ex :]["x!9}K5h \v cƌQB.;XJĎ;0i$\tυ ;v]q:JJun VhH`W箪E x5y8|Z뻻#%%7Zj3gԩS 0sL:_(`ޠA5xo*Hxi-T6&?? ⍺MJD*uqUXYY!**JpMXѴiS̜9ϬZ .\͛7uXP"4t֍ReЫV‚ AAA͛7՚]pZ G3~"W- X53p;>lfff~Ul̀$tС󕗗h mLAAr9[JtT=4zh=zjl[KW_ӧO3~g&Q":q,Zu yd~:vQ"5/0 O ǩ?O1W!e=U<33)))A-<ё:5UXXrr]J-,mW`ii]UV*,}`);سg>W|…Ɵau졀E=x:G˱ۗAGEE!** ~aUn z{{C" ..&&&hӦMܹƇpiUuAAA8{, SK>,ɓ'ql޼֭޽{au저E7|/ =8 HIK{_U`III[NNNHIIAFFѪU+b?֭[s>{>r׶}1x`XvvvJz;sڵӫ~sN,_-DC@0n]R3%KKK0~o𑑑/^sP(ڴiSu ڥϺVTTT ;;;K xu@bbb.ҤI0k,ի{I5xuQQ.]?*Wcc2%".1!co>߮3?B@6mT50L| X||k&M WmǏ|]ʕ+/d|8.^F -`Qn: 6 &L+.W*Ee%v䠽UaTW@$3kE_@?f#A]3g`|2k}ɓhҤI&ň 5ʗ {{7󙺇KKK!h!""YYYA(2<<<YYᣠ K,¨)CEb <<о}{"HKKBIѢ|0zG<3X[;޾= m۶O?}c-喠P>UV֡J( +%u{yynjh8x+X_.^jMB0j|xMj ~wiLGbۧۘ)׫!:EEW!ed% ry!rrbWh :w# B۷PPLO#57 \\\(C&\`J^gT˗3Pv(`&0^  O0\Q0l܍XXi(*ׯgҥ^􉊊P(DEE.XX,ꂃqرF{xxٳgS7ntp-V]E'c@*@ý迩SW\V+**"1{v6xB-Z50T\,D"TR-MB@Zɓ'Lb1"#:! X=;ĩ`cWG{o^!v-LTz+V| :w\,}TRf x ۷oUmv+""j5ؘrDܹzEX5L q#0P<PSٿO8ƌ5 )jLeALB! bVZZZl SSS> ,_Uaw XZZ"##Zt @RRꣀEXwd1YA7iQ?\[Ν{PZ:Fj# u:XЭ[7`(݁Wb/E"1&}1ݻwGݱ.?~eeu͔V_n ^nݺ1ֳgm\.W9`rk:ݕ]TRn^ۺvrT"(DTV" 4G,BQQtt폋?+ ˡP(_tX[[̌gE.WJ5V] nn̩~ 9 X/`xvrEK*"//[3x;yP篔Zl oooܹsCk}$'s|ߋŘ`i!BSף `],}}k&v]>| 5%, Ymk\p s-c,Q"88t;h۶3Ck7 aeeHls߿?"##7~/ѪUzCw T!b1]`EE!* Ԅ(ߕf2A%UÕRc],C&x[b܎OhE!*5*bq|Te"gd"LJJ;+ksK9+((gϞ -`36K4NNL\ X3L-W ݻwgZ XksA\,wwwh111uQhh簔llmq#|%:8C,8l^b1ZY~2xnnW& XB,&&pvF*++˗/\nf-Y/p)1ط3g"kLݳ('2DF !D ÆFP?Y |75 +{E @$K T X055ej#U/ʕ+Ѻukܾ}i B!憐 .Ge"5F'H* |8,Y4d+33͛77A%UVQQ7oQC&w!D}>ST'3!H䊽{`Ѣ>Vyy9@U,]2`111}}[ye X?@fS"3 w ab⌦Mo,cYmLj0ĭA%AX$ >}ZgϞqX ,Bт7o^Bhh5k$?-.tbMr޽a*2BD"UU >E! HHH@Ϟ=1qD,]mVEE233"U:X|^))5vp!0 >> 9zF-BCTR%`RӦMѳgOruuEzz:***8TGBWg{{{C"pR *++!nVV,,,xӹau]dgg#99IG!DKͅ?wwwGRR7EAmBL Tj L&L&jX^deeŢmBE!Z=`{Taw XCBd(+;;fffԕ_U:#GѣG)`,BRWJ\,urŅAP@777ܺuE(`B X\IN˘_UVVB"bk"999(,, R,BB^^R)Zn]w\wT X999ŰAU篔+WX ƍB#!D >!!!P"-4rssͶW#ai}QPխ IDAT5B%()ycǺѱ;ϥK$5!_(XNNNpwwǃ.!B&Mвez?3X7n75R-L0wıU/5E篔4iaÆɓ۷/"##.!h7d޽CQQqHK`P+⿆>u3]v8z(!hL6W_mBF0X ?‚1T  !8J3~~~x%RRR0p@`,BѐmmvC&:ryv@Eo+ P¨=gq:R)%Q%RT4o\x5rj.HΆ[}Uu/c=S&'0pkXs'ץ `BT^m޹s&&[O@DDctEF}˖ܽr9d ` k2K$gբ]h#AB4؀"Ee0֓W+LL333R_;ۧaEee_bQ&{W_E!hlhu666d(,,dVZA bl= Z2_B*sRS{I5x0=|Vy!D#hmnݺz"Q$[ Uh8E \A X$B!UHg`rdZ|gdh5[H !DMpWb87uc`+`UVVB"Ek֭zq2~ھH](`B;_d2ė/ݫ2<ʂj%!DM|`li+F&35^C(\3>#F@xx8O]g%`EFF¿K5Ow!I쌌 d20@+ tI࣏֭>®]p|ᇈ7̑ `WT .6k!D =G7+sΝۆ޽C$wLL""Wk׮ܹsn:^l}`lc al=,BQ&+%6/}˗!8%& 78 '^8`LL,}yε|}}{n ƍömX[jvJJJ`aa-2?"[FBԠw%6;XsdeX:}s03sEӦ`f7`޼aѢ]sذapLMMѧO9rufҗxzz"W =E)ra E!jЦd+zmXx1֮]()ÇwI6? >W;PL6 .\@BB&L[n`_],}Ϡ;x:Ba`%R Xm:Xln^r&&&իIgggk 333|'Xf ك?)))ZkDܹXZ̰|f"uE!*Ҧ{E7`ɒ%]'bܸq?>VގdB@ :o%JabbK d2 :ӧOr-b}.b{L ,BQK$ V/ N }^)Uyy9|||&M`x4Xs/MMTEBT],TselMu77n@AA/rVKcUVVbccsG"#qsgLW6G(VV8tɤ X"m;X簔`ܹؽ{7rP);XUCuܽ :\ !CV!j&VBB=z Fc֬YyaݺuXj.J!P(055帪J*V+Uυ}|9ffX &M~͚I,?{Θr%BCCq!Dߧ!'^?@E^Թsg޽gϞń 0f̚5`{tX[[Cºj`P(бkoߎݻw>@nݰl2'!TpqqAN^MCy3zUNN:V...ؾ}7LbsPieeە>k#77*Xyyy>X)yyyχ7oK284hBPPP8::2jgls5YAGٳZCPk8w.׮ERi1vGЬY3899iT_nn.ѴiSxyybLݻw#$$@߾}.ɠPBpZ6o 6D9{Ξ0tJY=wǾ}M1ka,[Z">>򂻻^+தc^djj˗cݺu_1o޼ZJyǏ WP|/`e}^^㑟OOOLĉ1bb,BiܕT"믿дiStޝ3lٲ&Ln:TTT 4t_ʅj99C^U?<<< *XU7i$>|N>u9!#''c|mU:XԽjX`` >z ]7QYDQX8#!!(;Mvg,B[+puuErrr߹s'FJ34(+k씖+-0`$1vXܿeeex!% XR_)շMXRR_~䕛7o!-M Z^.h_Q.s(`BH=`o~7XDӭ.sb$RVG2C@@p1( K{!\{!%%ÇgʕHL<{ZEŢ ABꐖ XY5" 25ǏZOxyyt?!(`BH>`8q^^^?4iq &CZګɓqy 2JDB XXXYf@+M0'?(--EϞ=鰻(`BHt^wlق0 C!1"ѽ;SӔ)S;wr,B[pW6o>GFӦץ&MMaw WBH `߿L&Cqq13tfx,B XN@ 0޽0rd?V7SL /^p]ޡE!5"`rlڴ ,NBm۾Þ=1sA>p@>Qo`m ~~?FQ% ӓXE!+ X8w[Wܹ/p tىfzyD"xzƻy bykɈfk% ?Fll,/_՛ɬ>eeea8uT|}}[SSSEyz{q 5u!ۛg_/u]LPɓI#DBauEؠk׮U?-B|g>}:. M6(((@BB XR #oiii=P=z"#F6yd: X? @ `e;&T_JJ ك+Vp]5N> !^>}zmBr0gʔ)ppp UDBWum VGK=K.ҥKaccu)FCyoq%![w"++ 34Au1`K1:SNL&۷.(`BLOm۶]s,C]DDDD 77gIIIx U,B{ۃt@,iii`(͛5k‚RڤI J1} б#t)];׿uE!`qF_H]wŪ߮]Ю];r]ۿe b7< <ò2<--$UTG`wl ?}5r!Vjj*._w}W9ڵk={6ץ^prBWV#TȭO?0__TVVrQ.(`B^ii)222ؚe uFaj~}d*0PƂ!1ݽz*BYj1cot}-|ױPN4]̙c!F U=^mi޽cKOK I2?W(/ }+7 Xx{{3֞={0p@8::-Z@("//Z͛7q}|\b>9˵^W_ϝ@E!1*//Gim… eK!~tpXI?P"BI֭[Kjmҡv>y9Le23@;1Q"5_%&&"11AAAZcwnܸC amH38bg!FXK0®,tapNxEbԘpY8::sZc[/~zK!tgq}@bԘ`i:.[Fnn.YO_%H56&R ?c,Bz%r95?»}vd.cbY&L'ץD";8 R[XΎ!hi۽ő#GdLΜ9R;RH=ޟ=T!GML0͈Q"-m21.233uVҀWVpK5loe,ޣE1Z DAA pUƳEH.a57g2XQ"-m dj,C]駟b̙hݺ5ץxzLx8:#E?8NlK!F)##b666jСCڵ+k =`9r>|8ץ5t OgΠofؠxb5r$ B97-mN4L()))puue9\HJJ¾}pAK!޷/ ht:q%%@W""ML FCW X~ Bź(XOp]ҷwcYNbcq<޹~~X4hakkuE1J2eZINN͛7h"zMyнo߾?KW.]e˖ښRlmmgEC@g!FI-B6dhV޺$(`Bammf͚˗/ݻwg )`=xgΜ.-BBѴ{k.*͐fa… \ANQbt=?cZYM:{&͛ǔR) XNK"`׮]3gUզ];w\BQ"u.פ簢qM̚5R,BQy)`½jGRRFjsssCrr2'?p]!E1*ҽw!!FF'O0Amۆ={ϏR,BQQ~z,^X%%%֡7n !!ӦMB8Gs!FEuVL::~mBKiT*Œ%KpK!E1hӦ ՗ӧO]a ЗmB:wEț(`B*+פkƍ:tZ[ 1t!Fn݂T*E~tXU& \B,BшGHHHoqFV8>wfg:Pb =zv~ puuqe suuŋ/P(.0\A/Qb:P(qFܺuKUF五*k֬ĉu)u!Fŷ5ӧ!J1vXK!E1 O"::˖-*Vzz:oߎǏs] !F,BQEQ ,B+++Cjjj3Lᰲ'|"\z5fϞ K!hb^߿Çsss 6:!O?aر*ՙhݺΟ3oulB,BKHH@>}saa!ߏpRrÇ3:E1x5;Xp& ,Y+VE:6!:XQQQhG(IJXcȐ!W777$&&C֭ѿ>C@bl6b UmY q[<<|gZ$&L U.777$''vee%fΜ۷o>!Ɔ:X 䁔0`%΄0!D\ KK!<#ka}ѥK&XQ"gϘ X9::"''匭yu$&&bԩIE3Ϟ={VCmIDAT샬lb]R,[ 6mbd=Bk!z\!뱋sXaaaY& XӺ?"УGCk7 7l؀#F]v TE!D14C'`1?'2T!&E;!!(.~MKzJDGGbĈܹHۀk VE:X3ud^pCu4Y9sBp5k6mBHHvڅfe]BQuT WW%HJOSRRpI8q^^^9r$ } ;v,6mW_cǎ=z4KBEK..y&jTX[CDZquuٳqĕ+W5k 66VI(//pEP׾̛7tSѴz$&FQYZ8qN:|9FVkj˖-hѢ&O0k,;v!J!zxPFI }mY|B<~8bbbj*>?bڵ L g%4!`l(,|; ddK70?cϻy&Nbqq1 ٳg~~~H$zsƣGl2V ԏ!0-?˖mDTiTVzJ{DC&c{|<̚5O>qFMQ,Ba [GB*QX mde?~!E!,8b5 vvv~W&޾-H'Pۻ&8szȥ $: D)0/],.h:Hk# ZV)J)N^B )Tm(座14-zPB~-!&Tp;S.+JZZP XLP1Q&S0h,(X`1hT%=sOvIj$ȩ?f$ @}(X`A؀9@3Q1pHcc#48H$Dl R(T]cs|\ҵctvjccM8O?cB6[VW?S`9vmn(IvV_߬(6qRbNLfQ3_ḠZ-\)S2y_÷Z=*ߣ`)\.jp8X,? `a,(XQ `F0` XQx7IENDB`leidenalg-0.11.0/doc/source/figures/karate_CPM.png0000664000175000017510000022633115101156755021250 0ustar nileshnileshPNG  IHDRXXfsBIT|d pHYs+ IDATxw|O$BbAB+!vUjl(jծ18)v"f 3fHbFdy 2q?#~u}EkB!PL.c BH%B0IB!& B!$B!P$XB! K!Ba` !B(!w x)5kŅ;,!0IB7l?zRff$$P#9%%aocàCU!D:$„+-6j@4l`Kbl9t U8J!K U/\`vRZן~BI0jӋŽ寿p$K!LE(=Wh\CO>Q4&! Fs*,ɳxpW/_L`B!t",!S0C `$kn^|{PB!t& FtϏ U.6s &B` aDwZ.)B]H%ܸq;KK^Ph5!K!Ba` a$DZZM!.$ˆ\ʖ%HrƥU+VB I0[sEZYQvmVB 4*z bx: ۗk(B]H%:UZ&Yfs+eB5"~-kg-heŖ={I!n$z6%ܹ߮6_pB!4& &bә3؎I>ި=kla!QI%j+kkk6mDNB$XB%K`cP($De #GfذaE! N,!LTV`3ac"% &*WTZhA ={CBK*X*]tޞ~ء!AH%.,ڵkB$XB`x{{ۍB$XBEɓ9}42v(B7` acKeܹlٲ@c"z! &*VT-[… qㆱCBI%,3vX=zdPBQ@^Rٽ{7ݻkk> ܉8WINJ6mՠNNNFZ!2& ^~́{ O 1 ֪kSR%cXA6FޞQr緁-D`>7opQ˗n:v`GӺukUŋ3LXjÆ K*qNIڵk w}oB!4$ qSÆ 9uTΝ;OC"`-\Ke1WlѢE|7)?~ |tmf ۷TX ^݅&B,e`̜9 0`#)Tƍtܙ%Jb .=8q?uӧOl޼Qe|ݩ ˕UŊԱ֭Y6>ϟWk_~3fl2z?^\vU{O۶m•B!Kx5ɓ^7i$ݻgm(CW~׿ҠǽgݻA܏9Ux"kEDpdN]t)C"49Dqܹ>i$͛GBBV1 !$ȓ'NNNkuݩR 3fP$}VE:xP^x;0`@GDD`mmMB^fwiӦqB IM5>ŅS>+XA( \]ePd{P%CDFFqB-IM5O>iӦL0AuY2ONV,* 8)³gԾ'44k׮'{& *ָ9dTXBCKR O<:Ӷm[ڶm˘1c^CD33Z9EUU4J- J*EN:L!RKTtivOEt҅ZYr[W+p`ӧ133-"hӦ ~~~$18ˋ]vcBMI%D*J%XM6/`С߫ Kǎ枦-վ^Uxx8`ZŔ%0IHE>:t( >}V4oN1g$T)wֵwzTX1TI~ؤB(E,!R)X Kʕ+Yn]F_|=L/Nack>Pys6l9M{j 4ڵkx"ϥ%0Iۄ*L2=zdzmrr2z=kU~-,+X_|gj͵Ҷ* sرctzBdD,!> jժ̙3]fx>Rҥ9}7ŊqgŊ >\u"&Tڵ+׮]#$$D!Dz$j͛}V+T… L}'o^z+U\{f‚.[V{ pϟ_īWxAVB$" Nuψ˗/]vi~n u|qSޞff  j`ʔ8~9ZGDDp1FAJFۄOۨQ#~7E'I%DMRT)֯_O4F2R1}:, U+| ߔ-Kr`gƍ̘;r%קz믙XԮ_N*UtN_fۄFחW^)L!PK4(=!-Ee4k콯jCϟ{8~ڵSwp-΅ߟإ0b>ۖ.]"22-Z,ϟU(IH-z}Nٷo 6$990^uZnc33֖e6lXiZe,x&;ia(ڒKt{P ܈3J~ڴinnn9s潯Y[[3nܸ?tŊf͚jp}+gB `̘1C !$XBP gϞÃ/_ϏVZyzM,YM6uMTQbՇ)G ,Ȁ9$XB͍4֗'N0c H&܏7?#e;u4i佞-u:==V N8ma!D 縆L8'OsoެDfy)QQQz=ϟ?jۃyZjxzzRH ݻ7Ϟ51Q둑Ά9$Xr52Ss_(Є թ`̙3SNk.)ƍ\|YO Cx ;ƃuE $$$ Xzr29iFa,iӦqM6lؐ鵺l<~ mƝ IDAT.ׯվٙ&M,CgH6ۄ Agg(wpqqQpE]HeA+VMī$/S`Aʔ)CHH^iKeĉDDDvty8;;k=ß3h .^H`` `Am *иqcq)YK)T1}13lٲ'I \R/O9H Q63f /_dժUi~`HHjJ 31|p0GWWW;#G_իWzL[5C p5?D D>zӆaa;w\Q,oooY|G=z, ?#F0!h~tNjӧOgƌ G$ ehlU`ohVّ$XPjU)ޮxj֬B]5k$44o sss,Y򵰰0^~ܑ#G(\0k֖|iׯ_RJQP!,,,GWmڴϏ$-(k֬fff: {ƳhzFZZ\5UK\6$0wnE֊nC#tϡVR/)TPʐO]S謢KEf͚$&&꽧-#ڌlPXY۪+iK?73c\sHeիWvRX+p-_^S6of_~(Qs=uV4h@ҥzcs4u5 ҏe 6?/= 8ŋ+з`_%KheeQ?V0PښׯKD$ ҅ s6oVp\d>ŐR8x"+U{"k@~ڼrB ffT[Wlmm)PF٩K Vj...L>]/_΀Ҍ+W\rYU57k ;;;6nܨJed0+Yd W^}Oz&&!۶%m-,~F--gkK1c|*Y$XחOu˳%{9p}Tla8Y) ܜӡC<<<7nZ56ۄi M(CprrիW֬a|=u* 5"_|Vd`Xɓi3}:6jnX8ZZ2Vj5o {nC۶m(W~}z?#H8@֭S~ܺuk:u{..\s^ӧOR;,ƌԩS^G)nlp4h9spww7v8"8ۏ1ɉ| ,G@\hS8|wh֮]k 4 6sNFӧO pנ,wwwz|TT4bwDN۶mɛ7/;wT{-%2tT]vDEEۏicOfذa8q 3%@,#)lcߥK,;p+sOW:V*Wĉ,9vve ,k׮|X߁Ǹk׮/_>ʔ)g5bxyy}Y@@qqq4nX(`L:իWzrpp \rEuRW.^3׬N%J bݨMۨ7ofܹs D,#yN,ݵ ˗9g#GRR%TZM4a޽$''ӱcG G[iURW#F}=$~:{M E*V%wЁ}Gɾ{+* ӧy2k+ve|^ E/T,X{;@cȂ؅ʕ+sM2dlڴo/^H<|P5`e`Ԯ] &ЧOK 4J)-ׯ_Zug̚5KtDֳ'OXwT>v ,Z]%'o`UD%7mBܗ9Z5j?ء! `9::r ֵeŴoߞ?___iSK ˗)Z(Jի3}twQUjV]FժU3O?۷߿_XQHL)j'OPW}=U)ҥ =zgϞG@,޽{eS\9>|+8p:w@H>`SJёFS"͚5.OXahzJh9`ųgpq9ݽ{psscɒ%4jB,#JzD+Çl2֬YɓVlL>,c&XotQ繺r93N ۏ6qpϭ y}jBرc9{Gd al`ѹsSN)`ٱtR<<<ر#֭St(wԪU@ESjSxqQm K6m4~nf @J8Mၟܾ}3[079Yg <:|X_Kپ};[n5v(BI,#ʨUR%BCC֭[s?N׮] sr % J4^=y???z ÷i&<<<4znfh`Ջdۄ'ZE#"OE~"## t -I$!!WvF*Uz#K#Gd…,_X>/RrP-BM2)Rݻw= SRY`SLݻwZݯ.wwwΞ=KLLZKLEiP=ˉNʕzCCH2^iذ!-[dƍzfvȳgxk)Q bŊ)RD\BXXX[øe[PlYbcc=\ ׏I/Vŋ(՞^vMղ8>H׮]H2Ɔ0Ӿ}{?Çٳ'/^4s 5(QRz%NqjUҫb%%%qML&M~ȹ&CGr"*i2P`A9Vxxxj*իggc_$2*XF̌;Yfxb|||?8RK & ֱcȟ?sӧiݺ5^ԝAq%t^+=u%,,'OdzmJ ?___dž I8<==9s nݢo߾\zh PlY._:V4^35jh0n8]xg۷N:?SL> ŭ[ nӆ[/^Ɔ'n~VM~zj z7n\rnݺŋqqqe˖9򣣇͛7moooIDg$IXÆ B?+p1sʒ5kL2ʕ+;$ѼysC|e˖l߾ j|qssC^[ڷoѣGC=z`رؑϞ1U{Nrf%t2Dжm >˗)PNNNߓ?f믬YӿJ, @5y vv5[æʕ+1m۶iun͛sL  =$$DϞ=Yf k׮bŊ:D5kp5zō7ׯ_Y͞KX=o,G&$ 3!ܜ{{о}1W-Ĭfڴi888cPP`XfطoZOχ[r6%ϟg4lؐѣG;ի>>>TTI^|I׮],sҽ&((+Vh~(::.5j=ך/%gp/^l]|G5^zJ*u.**^z=CgϞe͚5~ӲeK;wYfjSs%'h၃qqqܸqCOӫW/ڵ%=ʙl2WR\9+ߩbŊj1Ԯ]͛7ɓi4mϏ-[f\?WJn ?wE*n__ϟP})''' *:6663S2sLA֬YC߿?s9$XŊ[ϞtQҒŋC^?z9}K.UlH4>d*4ѻwo݋?Æ 3ؙ@q ڎhPg{p߾}888(:3\Y˕Mݝe˖q-ZĐ!ChԨQɕJ۶mbǎ^ct4iK.(hӦ ~~~:eeeELLL(Qh޼yY ѣwJd{` *:2GW̜9͒%KA`&䜚6 Vbb"GE^t@ӊr<端M6Z7L2___|ڵlْ,Y?iپ}{B @ ޛ)޽%K~op;I ('URsuue*T-[r!cwmBMh`߿6mdxi۶-Ŋh$šd %IJe6lXʏYlnnn|۷3eJ.zmBmY[[t?TR{R+WEJL6(OJ1{*#Ϗ!CSdI7nLɒ%ڵk\|~ݻvoEZs2Equ-[fPBZ|҂W{ γ*Ο?OڵuZlٲDDD`GS6d|}}ٴi~omu! @5`e=xq͋ڱj# Mo<@w.޻Gׯ_X~5jǏٰa}ʕ`,Y *쌣{\/^dL2h8885'O5VZHӧcRxqEO kl/GGzSLҔ5ݚ*T# (QQReŴoߞ]k쐴6 ˗/~zAWRǎ=Y7RˋjR~ossfV[(jkK>}4im۶?DFF`#=ѣ3v|vTP~'rU׶7oޔ1u+W4E^uFΝӧC ?zl x[6+WV# caakK???bbbܹs9ZEmBu3ܱcuԡlٲũ t,???g-bϞ=ZŚ{ѧO*WÇӼJ}2/_&aȓ'̜I6m(\pYp!sΥE+V0c E2bΞ=KHtt4<}ohMI IDATbffF|ښB ș3gpssS&X ˗j:2'O2o>>|Zѯ߿={m6Fʕ+۷/ǏgӦM,]TfDX)R{{~={rq޽{9w7nȰK ͛ÇYxbg2(β/+B,=z +^޼y5 |rOvۇN+1۷xzzramۦsM*U~{_ۼy3;v̙3̚5 ~T]x1cРAiC^z8;;zj-P͛7TZڵkcgg^?ŋ粲)*PVVV?~ʗ/ϥK2. Ǐy15k|<ٳtUxN:E֭)^8GU% Uʕ+i֬ח3fPbt0`Gݝ3g6lǏOZRcffF\r899QzO͛7s?͛7wWti (G4W]Ge„ >};;;=GwwwEϪOW}BY$XzūW(W"WbEVZEZhذ![n5vHԨQw͛tɨC^|Iڕ^ҴiS.\ի3< ڷ'W\/];e&E]L0iӦ1g̙c82(Jk{P7{6l؀߿>}a_x}T`.]9CU$Ц+u{Ft`aaABRj֬# ŋZsW_QZ5FUޙEU0 +k,i.dfZdfV%2}-QQqTQٔeXμ rfuyuuܣ0=}?[޽˚k0zq@G&}?G$* Tt%=Wi&ZN`i3X#hu+AO^\̟i}&L;(v\]]ٸq#xyyo>P]CE=XH$\\\dOz-weee1x`.]ĨQ/UZe;w.K,I&$'' nG1a SCښ/ٳg+|K LII 6TKü<øq8p`UFzf:ǽ;gvB ҘK(\%A&|#j=Lj}Fozo"FFO>%99͛VlTMLh{(AiiL@GׯSܜ椽 4/r huÀ ޳"%._Jʽ{fر ,Me숊ٳgU^X̮]{lD"!;;222332U;VߣIj ȾM6"_EII ˖-ҥKtܙ` 2`HuVj!2Xs>ii`Hj*0tP eܸq!00=zo> }+͍A֪ ===VZEQQQ߿?D&5U"b``=qqq3vX<== r=SSSlmme\m۶޾L?׍7d\YYY/;]J+o D5bßBq!Fő#GݻwuT ,"{uy \:|X^ٱcvvvr1!ϮADKljΜ9;w"NS lbīN` WW@#@:a ֫Ĉ#8r/̈"/%tb\?--3,,,عsg#xYfƌ,_ooo%ߙTD///BBBXt)֭S+VT155Ã3g}] ʗ o޼ٵk d\͚5_YfԩS|ټe3T4|O3Y4ܹs???:ԗ!2X_Cx 6$66ÇpBN< e\]]I~LGa*=CsΘ=zwy#}rmMb\hamMff@+zxxxg,,,ѣNR{MG_`ݺu kkkٿ?mڴɩ5.]޽{)**⣏>=''+VMzz:v믿QFj/Erd*0rH:w̍7nذa$%%\9o6'N;АB !W'66Vm~ut|8SNfÆ ;vׯ+4UN:̟?Ç3n8,_z1%Xk׮eСXXX]͛x{{ӤI8x`9t]̙رcqrr"881cT ͫu<ZnO>UjAU +X(5 ___ƎKӦMn$| &] [f=e8qVÇ+sY ȕ+W0AYaɚ5 +"{~ԯ_gHNN&99$RRR{1r888`oo_◑N:q֮]K^={R;lD"RҮ;#22<bݺug?~]boo_ʕ+lذG1zh{޳HX | ϟk׮Z>VZ[ԩs!33dɓ' 4 ܞY[[Azz:oUx{xYb(BSϦꯣC.K,!((ׯzt(^qq=.\@<== U{B:7i?II-CCe[UX&^Drr2FFF2]Vn]5M$--@JJJ={6ի>UAA%ѳzzVxwow!g͚%!!!|^ fƍb\CfW^MII ǏPdH$ƏO=2T4irww.\ 0lذ2GٳeH(^ʬYؽ{7r_wwwEpם뫯zgZ4=_* !!X"oNlllt? UXqUVXY)a[ OYwT2%͂`Ϟ=+:_΄ŵ7zYx!!?~ŋӷo_nܸ'7ҩS',XPkkB{ELVZ5n}}?yܟrU:qLi;5[H$e_fٳ'gϞUE'q6dr@"sPҮ^򲐛[FXLJJ*'^̄i!00O&sOJL=Xvލ?0444fmRYSM~#..rرc2eJÇ:u*vh׮S+aÆ1awW`ӧ$''ӼyeyTQXXH^mtE&<0xMx֋Vt޽ 6:CW"7n榱)AȩS cͥ= \.aٳS˄ 8;;Wx΋ݻ={VvL__,`vޝ?(5"_Gj>×OZ&>̙3Z r,_=zphƍcnn۷e;-&''3h 6mڤR?]vL}% :%B_UF7xDW^UI8:bhlLΝ[xy{W[ڿ&aggן>}Z. );ɓ2%syў任ەVqqq5bLJC1~x˼nddď?ȴiRҥK|jg5jč7؃aE;v H_68-rrIø1Y7lb)1zh6lؠXbҥF>|XˡW+Whmkvѩ [TzXXX`aa!7 -)Ju'N333+'J!~CBCy-[OKJX0_;=(ťǸ([l_/de^֭/_?dĈݛS`llL x;wVU\USPP~+3uttd]W߿pزe ϟ#۷oX*55xA7m\z+դC=D" 4A^F.;thp]A֪)8;;GAA3)0m4N>M= YfצNʈ#yX[[ŋ&337n`E.{033ܜY)ח%K0gL¬YtK`Jđr!) R7nxٜ:Ճ5 |ۯ0#!!RWې~nժUuo۷g„ 0_2dǏ$' 3O,fm<(( #ogԩxzz2bBҠAnݺUQM`߾}ХKηI&\~mҺukϻAok IDAT*P\BBB066Ce=zz ,5Ju j84isss \7["aܹ8::biiI:udcXYYi{mX|6֭[_1c[/d'pa>=~ {mRv>LRV/fԩtUcAZ*dV<++%KpI֭gϞm۶L2@,YMD:G'իZMZct("##q-2f233$++XKddd_Fp`uʪ@5pTQ N:EϞ= ٙ:Ğ"*r3|BLOgo0o"ٵkc+++42}t~ga֬YL4:u{nݺr~c:lٲ- #77:Tt ,5rJ!DY<{(""""**₫+-t!E2ٙ6m`aaQe׋",66r+-T"XvuoJ6ʕ$)| .0Xb sVVVlN_Ѽyr(EpvvF"t҅>VU7K,a۶m9rD۷4BPW=[=gS7/7;Peܿ[[[o #55$++ dȑe f dd};уd߿/;VVVDrbի{vv\Q&͖eyyyrEًsbKbeeŶH,橊cVc$lvvv޽{jSSSٴiRy Μ9#Ř2e / $֡6mo߾R.\[5ߏg-Ƀ-2ƕu^g?f9tNjqmm6/vW"##{LT憋,CXZw/ʈx^~ ++ dbK(**0SVZx!VVV, \vM1` KAA,%odd$ 6Օ͛ VVc۷%%بCCv,[&HDݺu[.<5++{u1_&E!##~ 7o.aqqqrEYNNLYeR+6/Ew%m=b= 8PޞH*_ooo<ӦM#//O6XŚyiyR>̼yQ4hLxE kYBDzB' ,,L뙤xJ*dff'Fзo_\]]2ꫯ())aqXBaة?s 4 //L-SJS)xzzN^F؟EBBl\Q&-_JJH[i>M'OT(CFaGhU`RaDDmڴQyX:Ν;IZ&|n[lY8訚 /)MMlnǏI蕠b!^\yYF'TÇ+Į. b$&&J|RQ+t҅?PcۑfΜ?#GpqwF۶8ݸXL/p666;;; ##`aa!5IՕ 6(t;Eɑ+233IHHΝ;DYvvR‘#G`ϵvvvj}֨k"v Rb]V3rF*&nXOIL֮[ۺ1ëgשN`HXXJb%..۱w^_TT_quu՚[9sH$\p!CгgO8IOIațo2c"8 50`槟MHHb⿶eݳ"** }}}0P.htf˔"(..PeeeTFt :2=rԯ_wSx+WǧgS۶mԩS曂e$&&7o7ߔ{-==H:vX gH~ VmڵkZ/%''ckkt+gニ_ BpfΜ7|C-'**JښDy>C_>EE\C"3j all̅ ҥ 'OTDTuiذ!dffD")cR%Zۢ"HگM`'Ƚ-k## B͛7+.BBBشiҢErSL! @#KOOw4^[Uι@#Ǔ'OM-:"aaa|ZTLKK+'RSSemڴaʵ(6~-O>%88 .oPN}z\ؘ˗/suF۳ӳLR_,Ç899!pݻW&&&`ooOaa!S,VMXΎqq `ˀM:"yA<|Paӧxeё.]c v\BNNN>U/Ǫx9D;V-tK*Y",wĉ5h ϟgƌ̜9???6mDhh(oӸv%vڵ?kkk`~m6m$ϠX,%2#X>>>Х a/ &<)ڭlll~XhÆ ӘSL>+s>{:UK,J(::\]]U\|ByqY-TAD%_EE_y1Ǐؘ@u7of߾} Wpƍ:v}eRAըQ#YsÇ)(((SJ )k}SP()"pU3׮]]%bڵkܾ}Sj,FσhڴG\'j'Oboo_ylM,J$ آP 8:W^FSӳgOWY"ܽ{%[o94ŋ1cӦM 0au%33ҁO<Ν;J 500Ʀܬ2x`ϟOfTCUP9 ?~\M^R -(G|neETr2.j g] G&M |رCH^cر曌9Z/k*K,aŊ~a֯_ňCNN΋S=F:W\?}kj+55$##CfСC:_95: 4f͚aoo_nӋٳ*988p-֭[Ʃ}׮] >9smbkk JLLH&T͎<}Tk5-2sgn竼`V\Yc|ˤz~GT6Unݺ3zrImkQعs';w>,,FQ~}-GV95B_OAj8L/ʽ宵TED000ezŧ~J 5N|23f`ҤI̚5 ۷o wީr}zjbhܸ1>,3`֭3uTUZ[UqdggŃ(***3PQ}XZjߞ~=U0/h۹34i"P899q2.\ ))I;i݉&++Uٲe 111899i%ҥK9rH򠔶r%#rRTT#*C'ʃiɸ42Kf215j(\\\jpP!矉aϞ=ÇPXX/ŋiҤʳ,,,HKKxÆ 7qbff瓙IjjjRbe6#...DEEj07( e nHDZo/ړ'OٹFdy\M>]3&&ӟ&boZzjFYő#G>}R]?QQɟLLN`)U_*22,7oN>}puuQߚի̜9>QPP/愄(޽{0`Z15nܘk׮tYz5&MB"Э[7.RbIILlIgIquuŋ|nANNPd1r2;իdddT{ʊLMB\tkՕ$rP=zDrr2NNNZ]W\a׮]jr1B|l޼YTPY`M4aЊ}߿[30T-60$b$MV_]}Xs"D".ٳ|L+ŤJK!re̜9ѣG矗ymȑdggt򰵵%22l#n~WfΜD"q\177DBff&ɲRԛG[֖'NZ"!e.9åX߮^ev:882Vmmm"&&33js\hsέ{ ԩSץ5HS$''s8P9لpB-F޼n(^U #_0l3U\.Uq=zp<%WpN XFܹs)Hd%>iTDDNNNeR...J5joܸq~IK %%AQXXA-^{Ȑ!,Xmʒ˃*eܹkN(..&##?ÇӲeKY)ɗGSm\y&...r8)))}:{Yf]۷o'66B'O0rH==jckVjcbbbjlU\\Ceǎ >3gn=OXQ6+ `bgiܠuGz\VqZ4i•pi4xLZZZ9dYE 0771B6p-fΜɰaXvm.]ĬYhܸ1[lQ4<<KKK<scaaAjjj۬͛G`` j "uҦMJJJ'++{'+%VwyL(ٳgr_wss۴m۶kvvvX[[M:uKØн{wnܸqJR|}};vl^zݛ͛7h5Ν;UX~ªåK駟d#oslݽy(P S>ՓY._[ZrSTIv߻M2!i&ԩCIIIk]Yt)aaaOJ;v`ƍxxxU7o:to߾[pZn]μ `ooϨQ&Gff&q{!iذroff&?^pdprrҨionn.z̙3cϞ=#11Z|)**K.[!dddTbɒ%1vlU,^\6QJQQlWbff&RbMUW\X\\IE򈉉Jc`Ɍ9;9Pi"((}}}> ټy3O}?~\}1{lש yà1qD֭[ŋUzFFF+b^NԩCÆ %33x$LlYZZ֘3122B,W؇%ٙ7n`ffViVVVܻwAJiiiWxԀMýiٲ$cǸr\ й3  >|8:u:22T4 Zcƌa,X td&&&`ooOaa!#bє]FFF`hhHnnn=Tnnn iZjEBB8;;~O?\9XZZ*3($Wj;e"""*ŋI׏@Ynfff+118,*qJ #?J!!!WWWAzYٻid:H$ x@Xp0b z2מ:uy1qDAx ܮ( | xvMU#sUbz\333"""YfK6144$??sss={V200qܿl@ &"";;;nٲ֭[WywQyط*tڕ}U1'bXB+qi-[лwok>̡`N;8@ٷo/ڹF *lر` tƏ>Tl, yM8[X?5a#v]3g~GAhW`A+СCiժUEV4%$4jԈ6mȲ>ƍ<|LAFFFH$ή| Umڴ!??wRDog}5R)"<<<066Vd>*;s :tK.1yd.^H9wO~)АzQ%UEW]\x9vH͎?[͚}v8@ΝСC X{Ib yB0ooofΜȔG888憛<~+WǏ)PbHH4(%6l=o ]_``llLݺuILLT:uPTdIX/ ; ٔԄw)n IDATٰa"Ν;ZEUk\X\CHCvIII;Y`OO߻6Áќ߶`ARL O>ٓiӦi0*[.|m۶p=++e0 =?}F <=ƒkW 6$..Nkz4IձAeBEqƴiFn޼I\\YYYK__=== bޕqԯ_ׯvFGGsq&LRY*T4/OK֙sE__9skp `XovmI::HZ`y={of%IGa!+'##CȞS]+)(A֭5ʀPeBE033-Z₱1\z%99Y IHHh\BL _|2\xs燗b  2OLJ_'hhȬ_S‚U^AI}4)))Ĕ*_؋/OQ)Չקym+++y&w%%%LyJÒWsLO^ô)]&vΔ}ZɜOd OqwwĪ14666hgeb:u [[*DFFb``#{Yv-+s|ɒ%4k֬Z *EÇᇸDqEr3 ccvJnA/Ez> m$$$paKJ+7 x+_UM^IX/F~zz쩁&4H$ښMFܻw۷o@QQL`癰 8_p$6?8c)YnNc;1DGG3tPy^ܜpAvMjTL@IKK#<<\Q ҥKh.\(sm Bpp*̮]4ӫfѐ'C ١(y}Y,₡!7oTywJ樊l ?|m6Ĥ]'*u&$((ۗy-## &ȿ@' ڵ#33ScjߏFb֬YUm,27Gΐ$GSS ^GXXX```@ZZZ={jeN 2quue˖Ƶk׸>=_~ lh^3wJϩ_>...<|PVVV+<\,,,x׸,ʽpBli4+ 44T|Wɥ]t "&&^z)uXB077)SӧVom8ٷod-MӤIAߟMS+xFQ,ו狄aggWkT]fҤI«*]",--iԨo=uxJ V\{{{5kƃDR*}X~!vj2T^ANeժUrJ=_~EYW^`RTTRH$b޽[222000Pϴi ̙3tY嵚iCD¹=bk˯@0 }66ޝ|\WP0ػw/fҨ(1MJJ"(_Q籷h;w(N: ^jnڴ)*~gݝɃhCOOs嗎Ne?:+/TAiD`WG{ 8_XUX{8o[Gka/~ɉ#yk:p;D"H:^Ө^m۶YELLL$VSDSPP(g8e/KiJ 4mڔ{)??γ|gM+jcJà+bӦM,^PA{W-% Ybxxxxc9pgbbBV(..&22R!OM U-B6￈D"EGG/R3n8'+VXkOWZ`{"hvֱcԩ~~~ޙ՜]ڕe/Ldl,ec c6YƘ`2d'ɾY6e$ʖBvΣ:=}{k\*<ת-UP,}tqxzz{n<1ZÇ ?2!k&fp_1vT|p7iӦ"Ԓեv$$$uϏҥ 2EwNnnnБ5z/_ҵkWmۆ5G%==NUgjuqlll3_#8?Gppp׉YQ몄"S[nرc\|YagB,-wti.V]>%Pj7԰HvڴoߞlKL6i҄W^I54󓼖c\\\T-r9dpx2ei\ŋ erݫ&Pc,yk ª`]t kkk+SգWΩs9Դ!H`9k vӴiS7nLDDDNٓWʴu4oޜ+W(I5zJӦM;s >}kbʕ2ߧ&Q#={0rHvӄUZp!.\ƍ۷)իWSr^z rVf ;XEu|99a|6}ݻ7^^^tؑ#W:::<30D;vTF~:Ādg*7nݽ5O>eҥxxx|FsB![neƌ =Yf4jԈ$33CCCXV~ѭ[7YjUESfT%aQIwtqܾ}[a&,`tQ.%]PFCӆ|ŗ9sϟӯ_?F5k1wިQ#޼yyXXX7nݺӇ}m$-[P(ok222oˤc:S,E?FQQ(ѰxbN>M``  hsʔGaűh"bbb8tgU4@tG=,TD>H7)u'uBX6mđ#G7o111|x{{Kwh1ԩSN:Nlllq4L*אτ زeK(֭[qrrVZRx"zE , !!AttZ6mIw koh p4k0\555vIVV3f@__ pAe-[O&M$~YӃÆ cҤI :uF[[[& ?(`)J$Q^UK_q2.Z٨ѫ|5 Zs!33;v|Fu`AQK[[l47k΋֙M/>H~0`0 t:<gL6 &Э[7éW+1l0Ν;'[.;w&%%WtaÆ$17F[[oUf\aI fddpM$ޓ [lںĵ'Ofҥ 鶯lڱSҸKc5QPec>9wl`V3H>R@aooOΝqss)4]UM{>ӧOGEEM6ɴĄW^I=X*!oǬu-}t~-+lh[zoܹp&-ڵ+lٲE$YРAe9r$>>>ҿBhтuFJJ Taeb*i˜c,XرcرLUfN_9N}Gǧ/wD+D+Η~|# t9 `ebaa!Pe`-_Çsܹb~+.\--6P0hqL2}}}֮]+֭[`*@:4jԈAH@'&L(]V-<<<)ԤI.]/_f̘1rkЩS'y 3ΕG!3""":0D@:qѳgϲm6oTUK~^+++#m?dQ~O=!{u62pF-ӽju=^zg}V.7UCvظq#:::mRSݣW;31~x4ik Qӄ"Xzzz},3&LƆUЛb \]]9wcǎ+ZB˖-%<<---222dӱcGD1YQtΝ;y{{{?{d"LK߹s'5J&*39rJٗA&}#[͛q'jUWccce%xe*ʕ+ٻw/'O8?΋/hSʅL888`nn^bk8{KUvm222tە7]v%((-[slْիWxbN<Ʉ vW;v$11 S UTT |ϻt邮|6l؀%-naɒ%&11'''ڳ[%h?8snRV`` tҥ\+k2EBCC߿?Zb˖- fjT7P0hq1kkk.\(*1}"nݚkײ`9ɓe֡RUU ]]]B\5䣨(֕+W044I3fСCK=ߟW믿lSem[~7rv|FO/F[w;z݄Zf ;v#GV97Çh3Eh}uw@|VeqqrrbҤIVmۊ"4{ɉPU~}ڵk'͒EÇ܂>޽AϊÇ3a>FDD)smdeg҂IywLpRݻwg޽|W,Y H\_/@ڰaCxD?FFFԫWOagϞz)l} IDAT~rss(1' wEPPPΒ3f ԩSG.{*;Ϟ=#H=VKà &T #F-СCWvظYQ:XhԨIIIE"+eAYx1_ B!{tƒ5B^I 0MަeEUuC;BP5;;;<_XpD5N&~)<(KQiBƍ?l?''???'b XtL>-[eOUM6]PSyjD?sQ,[tDҎ„ի~ıyfVZ;~mޫ*) 6hS**4Zp `v6%1ct4MH,*;XL:)S`mm-Q`>}8r̙3ŋXcUn]TUUEs35jD͉˗Sd$bz fΜɧ~*UTlѢE :9KKK7W> )X9X%51 :=c޽4jԨUPF Ràajb%__rsi'dTcjժ &OEbiiɍ7pww |_0m4.]Z[͉C P_M۶mDFFiӆd[R#G2n8cO%S `Μ9d*TE|6:u000hyrEs*| zzzFL,m۶d6mĄ ՉwT |XٞcBm*/C+wwwTTTR/8y$;wfҤIXBc/n tƍ111!::Z#4!Ŋ&&&TQHLLӧ`kk6R|۷55j[k©Sڵ+ ,ٳg?XǢŮe( IXe`_+iU@OYܼӿ G޲EQf9XQKLܺuK}C ̙3|8::zjR?S~>ڷoOvv6Nmۖ&M#uEEΞ=&L @*3g[;˞={3f _~%Vݻw̟?sq1ƍǎ;US(H^R K%@6֣Gxܟʊ?8*`…]NaV0hRa rrؼr,*_S+.]p <<gԩ899acc#ӿ#W\}o>Jylܻ֬wΝ;sUyB ɏnZs]]]>'Oj_rr23gСCrݿ"HHHѣ̚5޽{uViӦ 'Nۛ={0zh &}>u߭CECNInd|gS--ZڵkE^KIIA]]:<}cǒñcjIYٳg2dHER)aСPy=rs VeOIVU*L.] S3ƍիhkk3zhbccKjStЁ˗/+TVZL6͛7qFkĬY077'>><$|?^'<`ǎ;s """h׮cƌܹѣGchh(}8>=ֻѷׇV A@7rss~2ݫlʁ˗/cddD+ȏbɓܳgNbʕrZ(k#t-v:i |*4hhhP(DE`?ʀaNNNl۶ UU?O4I&qaINN(oߞf͚_ѧOl &пj׮M>}p9r$>ߔ&R3h Ξ=[gq5v+LLLׯ/mjsˇΪkv/:v(}i&&7vvvZӧOBh@INK@OF"G)>>?^WN8… +ڌ*III)l䆕)pCM ;SSFGGGYmI~KK`͈6U! SL[[[6o,#ܼyS4rĉbݛDHMMU؇ٳgtRSIII :ÇcbbŋjoY~=+bbIKKߟh۶-ZZZBԩ#rv ,,,9lu ,--e YHta4ca֯w{?YrIJJJ&** PH6m*ڔ*)tA!}'RKͼ}:Rf}Lƍ133ח={гgOL_~ayPZlYb$a``'O033jݻw?>/_z;{,K,h ӧO\jˊ'OG@@tUUUի ={oצ"}]eU.4s";\UN}}Wbb" ,srdմhтQFU)Ua0W/f"oJ0m[wl222}qttt*,3444>x^|@ q`YٳsNBBBؾ}L)K۶mbx{{쌃hW_}ŶmDXjժG<*xfժU߿_T% ^^^ڵKtܹsI4gpٲeXXX0tPYVT***mۖEXq\]]177gĈ̚5cJg>| \pttֶ[nE[[I&Il"RcffF֭ԯ__T5hРBlemm͍7* X$nZr&LDv3g T™3gqF޾}˗/iժBiGu\v fff" D%,T^~'|"Pt{VFf̘Apvvzodd$&&&舮ΥK8p =bK!y***`<{Yf |pBJܷrJ1cF)))5X|x9͓NE( 333А}ҿ6lXUyfLG|ѩ%`ǎ*eٙ2e 3f̠S'E[\?~L:ujJIbN`"ѮߋҊGd-D8%h7n3ѵ%K`iiɗ_~)v vvvYtCHHH(C?_WY%JNNSSS^z/u9U*T9+--ÇsŊ6E"[Z*, ys֥kإ]bt e9k ?&..NTOvvv4jԈ+WCڵE:UEJ9X ,`ӧM'{7(eǎ;ZhA|ՄAy*""@=& _~ۗ-[JyS%Rǃ=}s222Xnwŋٕ/Sdx) ;coo_ɓ'߿___UTU*uW^̨xxxpΝ[V̈aX {1\Gne({*3a5ϟVͻwҦMrup޿_K(vU4{ߟ۷qԩ8rd<@RZ j{aՒ#s.p(!x%VK1ȍʕ~ͮ\rn݈___rrr߿?6ZpՉJ`yxx  ++N GGG6&QQK.RScm< , >M6)*E $ϿKݺu100(Gˊ+K\Ceݻ8;;LMMeѶEu'gS GFn}85k??/҅OnlMVVȩ233`%eIts!!!Ѷm[̙S+KKJ hC Q_F8@]) W̭[`mJDàkiii$&&0- 2 VZE(hݺ(qPG"L#lW0k;y2zhKi( {sAE[ȈHe Q)uѨQ#+ڔb͍l̙#'DIIX`ݭ]Qlmm4HJAţGh׮]k222+v8pe$;;㕓#6ڥ{ݻ+Wp'a`F{ p ԇsEz)vɿ˧>%;&[~ B!)){*]ׯܹsmXqss#$$9sU77{Vj1=YYY\tKV)5###"##ȨveAqb/W@SSH]ʮJDӦM ~U&UQH&#>>ƈ^ SRRDMM@]./ 0?QG|駼z .]9a3| XP%Uk2 ݻw24)/^FJJ 2Q Pv(AK:999%OdTTT)Rۗ%*];hZ/h߾=\$Cr<8OCBR(cþ}ٳ'=zgϞ ڼy3...j@ ???̙R_h3$ҪejKQhjjR|'4&ԭ[wnN)@1oĉOdu)9X{iO67޽{իW d͚5hт=zЫW/C%R000(qb|YslmmiҤ NR:WerRR<AŒTT[*223ľ4 4|4@|dA m·ڵk3p@.]ʥKpqq!++ŋ3x`\]]G PL٩VE^2x`BPRv(ʍ: VRIBw%ciiI?RY]MXhW\AkٓE#5b_ر#ӦMСC޽sssN<-...=zxDv*U.D<{,DEEѠAo9s&ԫWOzCtggg^zիW9}4 ,Z$ѤIݳ&P!Vzz:ǎer~dd$nnnm67n\&QRyy9X[[W)JD9 Z<$%%N]]UURGTW]888Tu///N:qo2r30m`J' BA0rΎθ`bb"Քp:uTZ˖- fĉm۶kqqqDEEqqV^077uִnsssLLL$8>|(yu1c薕5 Ibnn.tP)))˜9s5[`UQ233à CddDO}Pbbb駟l|޽{D[[HKrMMMK߾.߿O`` $$$PWȭPփxy[]ƺu밴ŋ5@xN<)UZIC9 :XZZZ*LZ8y$^^^5kJ\?w\LLLM̀ppp(<""ggg/_N^ĮYv-M4;m̙DEEqQ!IKK+N `ڴi~z;vl=ԪU֭ JOOsvEGG뒦%$$D$ d!''p+WͣSN썵uwi)7+88yf{uq}Ν[0J\Ѧ(QwWwΦiӦMHH 77ʶ߽{///n޼ɘ1cpttH~߾}\x۷Ν;lذOOXh{F[[_~oٙ[-['2|ڵkWd/_fŊOrr2\pA="H$\\P><)umFFqqqiӦ,Sϟ UUUₙYu.0677Eòe"33ӧۿ/*+Wj*ك!\vMX~9"HځH?^1;;HpjcEիWiذ*,U!+Itٍ:j~qlkjsItNL`?,$ԩS70eʔ2NIuA(ͽ{hݺu{bcc%~ӱcJ$Ç[n888ݻwqvvf͚5t޽ĵ6l^z|猫+˖-^1;v`߾}t$~'2dn߾֭[]۵kWNWͺuڧHRRRĦGDD/^>uaaaٳd-ZDÆ H#ya~ߌ3Y˖-(J1zիWImu֥k׮Jxrss Ϟ=KTT5*bl۶-m۶eԩyWr-Z((I!5C^Ҕ5[|k.mzj"W6-Y•H,ձচ99S ,(8֭[GNNsΕLIݻw <+WT)J0њ6mH^YS.]ȑ#3fK?fΜI۶mŦW_m6.]YlzyW_ag'>1e޾}TΘ1M5 .^رJyIH+_+ƍT@ Ѝ!0ZFc΀:` yfrrruָvɸѓի۷͛Klw=|2L6={K޵kYYYL6Ms ӡC*5"ͮh^zUKGGk׮ȭ[DV^Wb깩dʐM*8tbS%xxQTϏӧOHtnq0f̘Rk*;E꺢hٲ%[̌lȈ+9)RLKW%RА<ɒb\o~ٳ_aÆ(J|۷Sҹ!adggKVEE޽{Gڵز6tYOE5qK+=9sH'55}}׌3={H`𬦦ƁXr%K.?,yٰaK5?zڼ|,p '$&&F%vvҹؔh++4?-[r d8M,nyaКI>н.;::,-8;;e%޷j*qpp(1%XIaÆq%qmX?ӧ=z4wYf$$$-wÇ۷/ޕAV033+0{˗žW *7`Iܻ{1޼IѦUəݾM/,YtM^^gϞeȐ!mrD[[==j=o8$!Z +d_>Β0^x뱱ɓ'a8W,sO޽SuԑʹJKKiܸ1͛7g߾}[81C aժU <`Ep9 *; 6DMM nݿ%VM/K.yy%d%%q Vf(6mJ||<M)W$uy|1 g1+f ]?Fexo@*7o?䄑AAA̟?_!NNNB%҅,\'OrUზRsš[bk-r G (ȑ#5f>!yy;1֓i֥PĠD2=XƪI! +!V4448?<\!yy  ,99s΀`T6дiS-ZάXZZs+GaպY ;+IUJ "oChh(]$\Z@GuurKTTF(yyyR$UJb{=p$؉E5$EPZڢOf7)jv4!6hD3g<}'ɹ}q1l0RS QJWl(*UHIǏOÆ eҤIӨQ#w|r+BBByܼy}IбcGx27Ν;NvV>sG͉TԨQ;2n8E3UbʬgqU+:}4"DN>z"wꕀY,q<8@2L2"/Rq;]bkk˞={7no%&&ҧOlmm?Z^.]н{w&N}[4M<==ɟ??^I lٲeT uƆ… Zd`E=bį,zst^ܫW/fϞMFسg{&O<'NРA B޽у(ְѥKҰaC@MS@L?~Ҋ)BLL *-[p!޽;III^ZLcK]7[_[~]j&XVBN[[k]r`ǎhk^Xb!!Wu!t'ۃ"ܴUunܽ 2;O<4i*U}Lz/y2o޼l?<۷ʊ 6d{`*m,///>|H``;)`tڕCRV-.)ԩSYf ϟy-S4w >} (<0)TH/'hh}6?$]%(:TO"]sRL`AEѣGiڴ)ÇW^^?c ̙۷o,_:ud{ NvIT*͚5㯿zg6 VFݫTBpp0~~~پ%K0n8^?ꜩuO+Y?X̝3wt~ ?x;;8Ύ!&鼎Ƚz%2Bxxл VЙ wyߧz+s% #GPr,=rM4Օ%KPD dƍ|W:ǧM6bsutlب˗/o5kk֭t!˱9uG4Yr~'=X/z[JN<;u@͚76cKuZ~8 pQKKKJ(;i&Y7lljңG ,,MIIaܸqe:wcƌaԩĩɠѴ<<NPGnhxO`J<NQhٳߌjٳg|s FNYFm ^6lhJ3 ]\\ bݺu,ZHg/_3l0I>#ׯq'_>_`ÆxI`I_@[;;5n̳8 t0OQQQܸqCdy AT7c)Zh+e>jjWuI+::>"E?gyڵkk׮j?CɭA8,--QTZHjո{.7niKR͙3 P~}EfZ]v }W?DeaJEKK[ZRagaʕfl EHJhN[rS"湓ںwv8q"ݺupzEDD藧5kPR%fgKT888pBʗ/Ӭ>M,~ѥK6m;vёŋEʭtޮcG_@bR?NBR{Οvin2´ M+7 Eٻy˾9s&v?>E1tPF5zƣGX|YJ$X7޽{:4W6mbСܹSߟwuV)vTNW =z>''e~K9v`ŌC>Q3@O>?Jb^w:vHJJ Z 3UbhFtJ}ϟ?Ց,,u888Hpp03fǏg?~\E(r2XVVVXXXP}[^W*}>ʚcSLK.^7sL~g̙C~3 eRRD ^opɟ??yѺ~Օ={jU>w\̙qM& 0yϞ=̙34nءHRQP!V~hj8 y-a<OlQ{?|q۷}Ql 9vM6TR,[RJix]aZp!#G(``ccΝi-m ܹ3FnݺZvZgU`}K<2à'X-ΛJWcN }$8V[a?2if'l<~Ν;ƴi2n„ [M6eYRS)`_TT 7#k̜9sXz R}K<J0džA_ ʬKuT̓E7䩚KKF10bҺf?`zJ$Xxn߾='OܻwO5`ݛ={ҨQ# ƹs2% 0I/^DR;aɗ/_? DGG+ e3k,={CsN_^O/99/^?~x%/_W}U,m'kVZڵQFm66mWjVn 0IRg̘1j?@HH۶mDo}v 5kFbX|UTL2%ʙzˋGi6+XY~=gϞrYZK,aرZW̙$X$pQa9y6VjUQ ڊ]v3qw>YjtUe[N:ܾ}ڵkk#e+Մ xIHHm۶cYIٲe XXX;ara4hЀʕ+g8lTS[laРA,Zm۾޽{]65kd޼ỷʞ={QFz}NzL&Ņ[n&?~<5ٳ^k.y;IɑAaH9=$$sѻwo i8q"l޼EzLL Æ СC?~6m{v0a'OwIpp0/_իm*@9vX"k3gݻw7PtO,aR_Nll^H"#90#MiӦ k~g~V@@ڵCL2KK0@ь:T\9WE wwwXht… y G * iK^ c)A2Yfaeekhƍ:t(˖-{ H͛79p 4P4߿KKK4ibgk+U6m"%%E>2GS_5͚5m۶<|06lHzn7g` "XL}ժUQ^w>Ӥ5vX\B`` odK ȑ#[]hlOK׃Hf8z(:tBcVR5mڔ%Kн{w3ĭ'2~xVR*-OOO8qF3>+P bl޼9k֭[G߾}6pt% 0.L)lFEEpBƏu1vXhڴ)N:T^翩fÇrt`={ի}}ټy3:uhdURڲf.^Ȕ)S2fΝ&gm(A =9t)boOժPB[ BRàӾigH]emmMJJ III3ggg֮] >prѷF<Ә1c 2voh`ݿ$+=iӆ{+WSرcټy3]v~gvvvZ oooFs*` JI᳆ ̚BBSFF2>ܾ9sط|9o03fйsg\]]վ'**O?VZHV⧟~2dԨQo6XT*V[_ɁX"Z/^Ǐ }L4Mr>sqq3 & Л?wƆG˃LҼW ,MN&,:ÆѩV-#F,ی1СCDEEѹsgYz5-qܻw___^ʡChذ#̱cxڵ3v(omGc5jdۄi+W#Gd~>swwˋ &'8Kݻ[7b=+x4m+WklBЇA|3sL>|8QQQ̙3-[/ hc02yyyqmn߾MddZ+?/^`Ĉo}UV+Wy)2ÐK(.6&fڱ;: tA8XVu]05jCa|m۶iC_L7|C޼y[Mbcc4DwM 46mЪU+|Gǿ4G` ԭK@R \ ; J-0_zu+:u*vaÆ̝;Yf1x`8uI"##Mmam,ML5jPHTJ'b ֋ ÇٳFN$ZbwJu63]Nd&B߇A9s3gзolox ;/^e˖,X'''ħ1c0uTc!m,M_eӓݻwӤIvKeNL .ٷos}u???V\IhhIV*p.\{PB(@[l ^p_ͤIXlV{T*%/0e}Ҷ] @ x"͛7WD\\\9m4 ,H~|mҥ3-Ds PT_Zqq>p@ՄЍ֊70s,GoLj_(TЛ9BBBy&:u2v(zVɟ. ioߞ˗/+W>VRuޝ/u~oߎgcQ9x O>5TZK(&>>+}z<*Szj\]]3fxƍǾ}8p\k )56,%SyzzuVszPzu>̤I ݵkZ`տE p,H5 2,W=[r% PLhh(]5fZ@L- a׮]c۶m :4ϟ?Oݺu(^8͚5M6L6 J=`͜9޽{XhӃ`%JPhQJ*ŶmH%&afYr%W^ё9sн{wMGm?b[mĉZk…|g0e+c2… ?nݺ5u%&&(`z4+Wk׮)1=wm0NE :z& PTΝti2_ЗfUUD&1* $L$@Iϓx!\^ERRR߿?w@^ej%$&&*-j,'56ۃJ*T@rr2J"$$$QM8l0~yq=Ft# P'-,hE I =1[@2(Ʌ!w2Fs1^>ӧ]6yݻ,[=z1XͣSNSmb}3 ^onٲ%fw777n޼ }fj(` YXp14ukb*r/~J sw 3gRhذ! ,Y،`q)EDCҴ޽{XXXPHģV~:)S@T*BB֎$XB/ԩÌ;lg:}@[~Z;z(kv!_mqYJJr`*M+XJ=JI&5Qy8ᨂiO,!PS5jԀ3иFc{>pvE`z**mlT6673'Pԩӄ$XBvP]R)'l 2+u&'w~'q6 SI"EPXY\ FZQ\ ‡8kG` ! BAbzt+Zl1|MC&o޼XZfF3Rti{=Ν;ևcaʓ)ڴ(*7>r* (h9~ג6$XBT>U}9pP2+""-[0p@E5&SnpO+XJk#'f ?Wpi%^}Ȥo'; ɠQ!@tdӨM$%i|-?Lz5 lllGRfK35u%Kboooɓ7nŊɓo~ IDAT`XZZꊫ+];IK!4~z|l|kM|&1H?JVVX;~h:-J0<#Q`h +G|kJݻʴ;dXWi&X7ܹs<~ow9´H%Zg Q7h6%m(kg XzYb]Қ^z:,6F5`*u+X89;+W&667nSrss͛FNdD,!Aٲp 'G'V]ة `(Eh\b𸔨`^*UPJ2-ϟ?Hlllprr2PTl07aD` !¨S]veԷԩO<1Z<&X>dŊ :TL:[*5*X 5j ((TI% t[M޶ufD-BSJK:u zݻwIHH0r"-IBaaaTX?;;;CllQⱶ&%%$GI_>d }i [ܾ}7 M$XB?~[_/]4n2NPhM/^̈#nٳg<~777Furu߿VK M$XBۃrbeo ]ԪW2jvH%:2+00WWW<<<.2WI=:ښm۲yfIL$XBW`is~FLC|||A{v ȧC~®xr@ьR,?.LBxyyeyjոqccbƐ>/~~)}ёz,6S̙3 ʕ+JҥKx{{ӷo_` !`oGBy}I0!iw/`G[ Fd~eܰ5pTKb(QҥKsec$G,!PB^c㗏ӼGsb05n)S<3ێo{|& VNJvk׮3#G%@,!ZfF2DVp>i qU A\p]uT)g%UVl߾ baaAHHH%Z B ?nc-;T0"g. ; |l޼ K!N M8euuX}xs}ޢy,sYnի#iڴTL$XB%u,}n/'m$y&l2"2KrZ*'7n$)9 Ӯmzs1ydLBh!""KիǐлD>P bbyNNN ,h2{0'̿ȡKX|e,Hu` Nu? W,ءRB-[m:u ӧ[ϔ=<ԏI/B*br%x,=πBDboW=9jsIB /_B j]-ϓ*E>Mg2| Ѹ!*͝rdRƪ`$DYfpXr` !0#<\) ֳ8oAE[Te`՝whиqAy nt q4oלo$XBuF4mš5klBlpPnAeW;'>0냼j+>rwH%u۫}>ŋcbwX,Nr ,frS;{uQ>k׮ij 'eS%$`"K!4 }EXwMBNBL_F[9z+_e,<{L.IBCT*X?jkXةhӦ AAA Fhz2ѐSܵN].IBC$X%K޽{$&>u=&Ģ Sxp!|_Ͳeطo}%%MOF[9ܹs$5ORl 5 Bhիam>㿇cF{{[S}[uN;\ٙiӦ1x`0aO>GF *%Gs\bI%ЦzJ>Z}B^`-a'p?`1kk~j!2V˗/nݺﯷ -}ҥK+WJeĨi~tۜGs$XBmS鳂pIJLR*䩚;;ϋ];*ΪȐgCxo{}횭Zb߾}ҠA6mڤ %}SWeʔ(QTI9ɜH,!Ѐ.,WWWn߾pD1c#F‚iqn9^{&tO(b"~E|4Zw۷0tBpp K`Tx}X4nB"#` !t`sX[[S'K,Iҥu~}SNeŊ|7뼮Or9WI>îsU *IBMT@[ӧOgȑzY;2e0tPO#˗XYY ',Xȑem|'DEEq 01}>?X$B5`LDDQN)Zf?&007774hnk#U֮]K&Mbǎ :{{{掚K` +p67 uI%je{0U8-Z[SS>>>?~gϞѺukoXS֭[GӦM`˖- :yuMȦnu? -YR U:d$XB&]+X|Vjc)}RSp|ܹsM61|pϟj ^ 8 օWi-F$XBn߾MrtZG+,,WzJprrbԩ 2???Əϓ'Oi>zXJ*e ͛smo9'3c!Fo̸2X¾= nޗ28:uĆ  _CB\@"ܺuV*U|rvM.]С_~z+2U`` .iӦ[Nf 0ذ0TS,Xӧ5kd:/2',!PR R;wRT)TZԲeKk2Jn3ڸq#-[իY#G*&cŊJzE{x$XB%:cիʠϟz3Fi&Zjŕ+WXjG0gӺuk6l؀7ynn% BA ^{Oc76STZz7oޤB y-[hӦ aaaX1cPP!<;*Vٲeqppٳ!K!˃puuUd=]uVzH,`A _mݺ6mʲe(\ޟbŊQre'z& BdC`崭cPij_m۶vq9-[Ƹqpvv4Zj޼9Ǐ'::!%I"J'X=u4iDXL!V}v8{,-b&XP&''˭[X"VVV:sN:t@pp0L8ŋ+~ծ]'OS*Ti̎ B,<{/^(C3nelƌ:uJLYA۷g̑GszGӧ|~SS֡,(QBv… R ͣdɒZcL={dx{{n:6lh̊T" /^|g`4&4*͓'gƍjݷݔiTZ2O;Oh?|vip̆4 !D&>|HJJ NNNNkƌ,_\gzQ^=6lGu?*)Ǵz) >iϦ8rtL:2e()ٳ'7f̞=֭[;$ ,!ȄWp ?_t)ڵKrgn:uīHܡq;B`6mZHT*;vŋz!I"J6/߳m>Ad\$}غ }&uOI"`AۄӧOgȑz{8wf }C7Ύ-[lr 8\P9Xǎ9|0*T'GK!1T +!dsE/i~oټs3yU>{͛Wݵ$U !D:`YFHLLޟgubz7m [ V~7gϞ۷cH%"JJJbΜ9 6,3 gŌ P &[]G꼌yIO-fSeiiIn(ST BquJ*JsμK{!&&ooo換ϞI{n=~D:(٦$6Em(3 "ǯl Ae?뜬GJ_Bb ׮]3x&۷Lsw}Νˎ;|ZjTV?GDDPD" R~}ܹömhN*} Bqe*Tg̘1#F}eѢEY^#ɕvzATTlА$XB+Xё5jl￧_~/^ءҥK3ŒN! B 7XT)"##IJJs͛M6Ŭ Ҁ$XB?aaaTP 5_ТEԩ |3&\xx8+V`رUV sNN B>WϞ=cݺu/ePs] ٓEjK!G2LK,͌5QFhPr֮]kHrIU믿>͛7ՠkL&MJ_ɓ'ɓK!Dnܸ|2dD*Xr ۷og%W$665k;' BM6QRl7irWO~Xpӧyݺuׯ1*# BAuW*… x bȐ!L:{{{cuޝ8‡-?$Oi_|h L<9jbxI,!DKݻbkf7!#Rʘ̻2 hz'I\=I  F_lf$BzJW9Y`,X@oz2\JTo(( K)},X@Z:[^HfMEL!Ű Ff$Bz/_B b 6mJ5Ғ'O(KNv .^H>}J7įyUq i ` !r=*X,XZ5dÇ3os\3IFːܒ`EEE1|~Gc"0YuX 8;0sL2u` !r-]34ipTgP]VtI%ȵtyP  '2`+fPʔ*CЮ Tú⡷L$XB\޽{T*5wÆ ԨQCoM`mڴ [[[ZnmPUǮŻ0+V%/u?@cmtip>}:4dҒ%Kޞc nb;sz~ۓmU  PNu5vhެ9l0vF# "Wv{p̙ > /f$en V~Xn:Z>s9G?|y˩ͧWNo^ݝ… ;LK+]t={jt۷9q~Wj{Æ ,C5jG`E(p4i܄&H"WfP[ի)Z^޺I%uܹCɗ/:tlP0 RBW\2ݿ;wҫW/M(}WBM,!DN؍kٴlRB;IFv ѨQ#F5S?޽{tءaRKk\t ooL?>}:g6`D3 ӧO:u3(D%z*˗kR^=\\\ Y\\\HNN6v(eѢEC$,!DUUrr2g&88Q'ULcԩSڵ+K6v(B$` !r,SklOvI\\;v4v(B,` !r078{,G6BT1>w`njP0iRB ULz`ɼ+!# zwz(P@-Sb*[&L`-ZءadPa^Yi*X7n$Oӎٳ֞otҰqa뮭*U.>i{%}(nis5W3m3U1vRSSuСknn֡Ck׮K#ߡ?_|ѩ7*vw K=D`Y_~dy~W7K?Զ ۴jhP||ːz駵yfƆ0^X칺XvQzxdKz^. `!U2͟?_/i`#`X _ lw<PVSSTXXgXɓ'U^[@Pu|n}Q5lhК={Μ9ځ@@k׮ձcBRݰrٮ/||TS'NVrq"`? }>7ӫzk[.To6p"`vcK$''G>XZh͚5j `vŧKvC /*z&W^rYL0 X,%55U]2WU2W/L>UPP;vF,H ke,2T,LT9997o܌rV̱XaXXVwwVipYRGێWAך7Y/m|I+''Gw C'؀uemڴI68r֬^W'1zjS裏4o<\.\R;wTOOa 6`QCCC clYei JRLJ7Sn[JKK˃ؠ|\.W^yEsB4X`I)I)z&=2i7 zi֬Y***㏫FYYYzWDf|+ LX`in׺߬=9^&yZ[lOVUTTRW\Tnn~nݪ8^zXן?^ڷo_'p 嵝mCR2S@*+вˌvU]]ܰ ɓڲenymII,Y+s0K}}}jmmUzzz{{ W˖-SQQ^p8dn\~C2GBX}Z[tIH WOºTPmmmڰa}]#  Bb3y:alqG蚨.ܥ;ה)S B-Ĝjڴi?}]W]~=̆R#8ӡ `TXFTEE̙#'Mԥ.IY#5!n;55nj :n9rDr:7l!>1Q66V361߯ ogP|yAն=e 74- `@XF.m${|U4usрp!`@2kzk{%ϨfСݭk׌+IԱcfDFMpL0Vo1c 0D E7F+ۑm[#`@튝+L-\da!`@ΔeRDv  "kϽI']{+|30/"cC+Ǩ9,阣g|`F ﶫg{;a])jb W!`@QOk뎟!5IKՂ?8#?lEh}z5hP̏c+0= 6 h悙*yDO≱N:zy^-\PS ,w0a,X #`F0`L?vt.IENDB`leidenalg-0.11.0/doc/source/conf.py0000664000175000017510000002445615101156755016433 0ustar nileshnilesh# -*- coding: utf-8 -*- # # leidenalg documentation build configuration file, created by # sphinx-quickstart on Fri Oct 21 11:26:44 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os #import sys # #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage', 'sphinx.ext.mathjax', 'sphinx.ext.napoleon', 'sphinx.ext.autosectionlabel' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['.templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'leidenalg' copyright = u'2016, V.A. Traag' author = u'V.A. Traag' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. import importlib.metadata as md # The full version, including alpha/beta/rc tags. release = md.version('leidenalg') # The short X.Y version. version = '.'.join(release.split('.')[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # #html_theme = 'classic' # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # otherwise, readthedocs.org uses their theme by default, so no need to specify it # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. # # html_title = u'leidenalg v0.7.0' # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # # html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['.static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'leidendoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'leidenalg.tex', u'leidenalg Documentation', u'V.A. Traag', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'leidenalg', u'leidenalg Documentation', [author], 1) ] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'leidenalg', u'leidenalg Documentation', author, 'leidenalg', 'One line description of project.', 'Miscellaneous'), ] autoclass_content = 'both' # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False doctest_global_setup = """ import igraph as ig import leidenalg as la """ leidenalg-0.11.0/doc/source/advanced.rst0000664000175000017510000002626315101156755017431 0ustar nileshnileshAdvanced ======== The basic interface explained in the :ref:`Introduction` should provide you enough to start detecting communities. However, perhaps you want to improve the partitions further or want to do some more advanced analysis. In this section, we will explain this in more detail. Optimiser --------- Although the package provides simple access to the function :func:`~leidenalg.find_partition`, there is actually an underlying :class:`~leidenalg.Optimiser` class that is doing the actual work. We can also explicitly construct an :class:`~leidenalg.Optimiser` object: >>> optimiser = la.Optimiser() The function :func:`~leidenalg.find_partition` then does nothing else then calling :func:`~leidenalg.Optimiser.optimise_partition` on the provided partition. .. testsetup:: G = ig.Graph.Erdos_Renyi(100, p=5./100) partition = la.CPMVertexPartition(G) >>> diff = optimiser.optimise_partition(partition) :func:`~leidenalg.Optimiser.optimise_partition` simply tries to improve any provided partition. We can thus try to repeatedly call :func:`~leidenalg.Optimiser.optimise_partition` to keep on improving the current partition: >>> G = ig.Graph.Erdos_Renyi(100, p=5./100) >>> partition = la.ModularityVertexPartition(G) >>> diff = 1 >>> while diff > 0: ... diff = optimiser.optimise_partition(partition) Even if a call to :func:`~leidenalg.Optimiser.optimise_partition` did not improve the current partition, it is still possible that a next call will improve the partition. Of course, if the current partition is already optimal, this will never happen, but it is not possible to decide whether a partition is optimal. This functionality of repeating multiple iterations is actually already built-in. You can simply call >>> diff = optimiser.optimise_partition(partition, n_iterations=10) If ``n_iterations < 0`` the optimiser continues iterating until it encounters an iterations that did not improve the partition. The :func:`~leidenalg.Optimiser.optimise_partition` itself is built on two other basic algorithms: :func:`~leidenalg.Optimiser.move_nodes` and :func:`~leidenalg.Optimiser.merge_nodes`. You can also call these functions yourself. For example: >>> diff = optimiser.move_nodes(partition) or >>> diff = optimiser.merge_nodes(partition) The simpler Louvain algorithm aggregates the partition and repeats the :func:`~leidenalg.Optimiser.move_nodes` on the aggregated partition. We can easily emulate that: >>> partition = la.ModularityVertexPartition(G) >>> while optimiser.move_nodes(partition) > 0: ... partition = partition.aggregate_partition() This summarises the whole Louvain algorithm in just three lines of code. Although this finds the final aggregate partition, it leaves unclear the actual partition on the level of the individual nodes. In order to do that, we need to update the membership based on the aggregate partition, for which we use the function :func:`~leidenalg.VertexPartition.MutableVertexPartition.from_coarse_partition`. >>> partition = la.ModularityVertexPartition(G) >>> partition_agg = partition.aggregate_partition() >>> while optimiser.move_nodes(partition_agg) > 0: ... partition.from_coarse_partition(partition_agg) ... partition_agg = partition_agg.aggregate_partition() Now ``partition_agg`` contains the aggregate partition and ``partition`` contains the actual partition of the original graph ``G``. Of course, ``partition_agg.quality() == partition.quality()`` (save some rounding). Instead of :func:`~leidenalg.Optimiser.move_nodes`, you could also use :func:`~leidenalg.Optimiser.merge_nodes`. These functions depend on choosing particular alternative communities: the documentation of the functions provides more detail. One possibility is that rather than aggregating the partition based on the current partition, you can first refine the partition and then aggregate it. This is what is done in the Leiden algorithm, and can be done using the functions :func:`~leidenalg.Optimiser.move_nodes_constrained` and :func:`~leidenalg.Optimiser.merge_nodes_constrained`. Implementing this, you end up with the following high-level implementation of the Leiden algorithm: >>> # Set initial partition >>> partition = la.ModularityVertexPartition(G) >>> refined_partition = la.ModularityVertexPartition(G) >>> partition_agg = refined_partition.aggregate_partition() >>> >>> while optimiser.move_nodes(partition_agg): ... ... # Get individual membership for partition ... partition.from_coarse_partition(partition_agg, refined_partition.membership) ... ... # Refine partition ... refined_partition = la.ModularityVertexPartition(G) ... optimiser.merge_nodes_constrained(refined_partition, partition) ... ... # Define aggregate partition on refined partition ... partition_agg = refined_partition.aggregate_partition() ... ... # But use membership of actual partition ... aggregate_membership = [None] * len(refined_partition) ... for i in range(G.vcount()): ... aggregate_membership[refined_partition.membership[i]] = partition.membership[i] ... partition_agg.set_membership(aggregate_membership) These functions in turn rely on two key functions of the partition: :func:`~leidenalg.VertexPartition.MutableVertexPartition.diff_move` and :func:`~leidenalg.VertexPartition.MutableVertexPartition.move_node`. The first calculates the difference when moving a node, and the latter actually moves the node, and updates all necessary internal administration. The :func:`~leidenalg.Optimiser.move_nodes` then does something as follows >>> for v in G.vs: ... best_comm = max(range(len(partition)), ... key=lambda c: partition.diff_move(v.index, c)) ... partition.move_node(v.index, best_comm) The actual implementation is more complicated, but this gives the general idea. This package builds on a previous implementation of the Louvain algorithm in `louvain-igraph `_. To illustrate the difference between ``louvain-igraph`` and ``leidenalg``, we ran both algorithms for 10 iterations on a `Youtube network `_ of more than 1 million nodes and almost 3 million edges. .. image:: figures/speed.png The results are quite clear: Leiden is able to achieve a higher modularity in less time. It also points out that it is usually a good idea to run Leiden for at least two iterations; this is also the default setting. Note that even if the Leiden algorithm did not find any improvement in this iteration, it is always possible that it will find some improvement in the next iteration. Resolution profile ------------------ Some methods accept so-called resolution parameters, such as :class:`~leidenalg.CPMVertexPartition` or :class:`~leidenalg.RBConfigurationVertexPartition`. Although some methods may seem to have some 'natural' resolution, in reality this is often quite arbitrary. However, the methods implemented here (which depend in a linear way on resolution parameters) allow for an effective scanning of a full range for the resolution parameter. In particular, these methods somehow can be formulated as :math:`Q = E - \gamma N` where :math:`E` and :math:`N` are some other quantities. In the case for :class:`~leidenalg.CPMVertexPartition` for example, :math:`E = \sum_c m_c` is the number of internal edges and :math:`N = \sum_c \binom{n_c}{2}` is the sum of the internal possible edges. The essential insight for these formulations [1]_ is that if there is an optimal partition for both :math:`\gamma_1` and :math:`\gamma_2` then the partition is also optimal for all :math:`\gamma_1 \leq \gamma \leq \gamma_2`. Such a resolution profile can be constructed using the :class:`~leidenalg.Optimiser` object. >>> G = ig.Graph.Famous('Zachary') >>> optimiser = la.Optimiser() >>> profile = optimiser.resolution_profile(G, la.CPMVertexPartition, ... resolution_range=(0,1)) Plotting the resolution parameter versus the total number of internal edges we thus obtain something as follows: .. image:: figures/resolution_profile.png Now ``profile`` contains a list of partitions of the specified type (:class:`~leidenalg.CPMVertexPartition` in this case) for resolution parameters at which there was a change. In particular, ``profile[i]`` should be better until ``profile[i+1]``, or stated otherwise for any resolution parameter between ``profile[i].resolution_parameter`` and ``profile[i+1].resolution_parameter`` the partition at position ``i`` should be better. Of course, there will be some variations because :func:`~leidenalg.Optimiser.optimise_partition` will find partitions of varying quality. The change points can then also vary for different runs. This function repeatedly calls :func:`~leidenalg.Optimiser.optimise_partition` and can therefore require a lot of time. Especially for resolution parameters right around a change point there may be many possible partitions, thus requiring a lot of runs. Fixed nodes ----------- For some purposes, it might be beneficial to only update part of a partition. For example, perhaps we previously already ran the Leiden algorithm on some dataset, and did some analysis on the resulting partition. If we then gather new data, and in particular new nodes, it might be useful to keep the previous community assignments fixed, while only updating the community assignments for the new nodes. This can be done using the ``is_membership_fixed`` argument of :func:`~leidenalg.Optimiser.find_partition`, see [2]_ for some details. For example, suppose we previously detected ``partition`` for graph ``G``, which was extended to graph ``G2``. Assuming that the previously exiting nodes are identical, we could create a new partition by doing >>> new_membership = list(range(G2.vcount())) ... new_membership[:G.vcount()] = partition.membership We can then only update the community assignments for the new nodes as follows >>> new_partition = la.CPMVertexPartition(G2, new_membership, ... resolution_parameter=partition.resolution_parameter) ... is_membership_fixed = [i < G.vcount() for i in range(G2.vcount())] >>> diff = optimiser.optimise_partition(new_partition, is_membership_fixed=is_membership_fixed) In this example we used :class:`~leidenalg.CPMVertexPartition`. but any other ``VertexPartition`` would work as well. Maximum community size ---------------------- In some cases, you may want to restrict the community sizes. It is possible to indicate this by setting the :attr:`~leidenalg.Optimiser.max_comm_size` parameter so that this constraint is taken into account during optimisation. In addition, it is possible to pass this parameter directly when using :func:`~leidenalg.find_partition`. For example >>> partition = la.find_partition(G, la.ModularityVertexPartition, max_comm_size=10) References ---------- .. [1] Traag, V. A., Krings, G., & Van Dooren, P. (2013). Significant scales in community structure. Scientific Reports, 3, 2930. `10.1038/srep02930 `_ .. [2] Zanini, F., Berghuis, B. A., Jones, R. C., Robilant, B. N. di, Nong, R. Y., Norton, J., Clarke, Michael F., Quake, S. R. (2019). northstar: leveraging cell atlases to identify healthy and neoplastic cells in transcriptomes from human tumors. BioRxiv, 820928. `10.1101/820928 `_ leidenalg-0.11.0/build-doc.yml0000664000175000017510000000032615101156755015442 0ustar nileshnileshname: build-doc channels: - conda-forge dependencies: - python - igraph>=1.0.0,<2 - libleidenalg>=0.12,<0.13 - python-igraph>=1.0.0,<2 - ddt - sphinx - sphinx_rtd_theme - flex - bison - cmake leidenalg-0.11.0/README.rst0000664000175000017510000002156315101156755014552 0ustar nileshnileshleidenalg ============== This package implements the Leiden algorithm in ``C++`` and exposes it to ``python``. It relies on ``(python-)igraph`` for it to function. Besides the relative flexibility of the implementation, it also scales well, and can be run on graphs of millions of nodes (as long as they can fit in memory). The core function is ``find_partition`` which finds the optimal partition using the Leiden algorithm [1]_, which is an extension of the Louvain algorithm [2]_ for a number of different methods. The methods currently implemented are (1) modularity [3]_, (2) Reichardt and Bornholdt's model using the configuration null model and the Erdös-Rényi null model [4]_, (3) the Constant Potts model (CPM) [5]_, (4) Significance [6]_, and finally (5) Surprise [7]_. In addition, it supports multiplex partition optimisation allowing community detection on for example negative links [8]_ or multiple time slices [9]_. There is the possibility of only partially optimising a partition, so that some community assignments remain fixed [10]_. It also provides some support for community detection on bipartite graphs. See the `documentation `_ for more information. .. image:: https://readthedocs.org/projects/leidenalg/badge :target: http://leidenalg.readthedocs.io/en/latest/ :alt: Leiden documentation status .. image:: https://github.com/vtraag/leidenalg/actions/workflows/build.yml/badge.svg?branch=master :target: https://github.com/vtraag/leidenalg/actions/workflows/build.yml :alt: Leiden build status (GitHub Actions) .. image:: https://zenodo.org/badge/146722095.svg :target: https://zenodo.org/badge/latestdoi/146722095 :alt: DOI .. image:: https://anaconda.org/conda-forge/leidenalg/badges/version.svg :target: https://anaconda.org/conda-forge/leidenalg :alt: Anaconda (conda-forge) Installation ------------ In short: ``pip install leidenalg``. All major platforms are supported on Python>=3.9, earlier versions of Python are no longer supported. Alternatively, you can install from Anaconda (channel ``conda-forge``). For Unix like systems it is possible to install from source. For Windows this is more complicated, and you are recommended to use the binary wheels. This Python interface depends on the C++ package ``libleidenalg`` which in turn depends on ``igraph``. You will need to build these packages yourself before you are able to build this Python interface. Make sure you have all necessary tools for compilation. In Ubuntu this can be installed using ``sudo apt-get install build-essential autoconf automake flex bison``, please refer to the documentation for your specific system. Make sure that not only ``gcc`` is installed, but also ``g++``, as the ``leidenalg`` package is programmed in ``C++``. Note that there are build scripts included in the ``scripts/`` directory. These are also used to build the binary wheels. 1. Compile (and install) the C core of ``igraph`` (version >= 1.0.0). You can use the file ``build_igraph.sh`` (on Unix-like systems) or ``build_igraph.bat`` (on Windows) in the ``scripts/`` directory to do this. For more details, see https://igraph.org/c/doc/igraph-Installation.html. 2. Compile (and install) the C core of ``libleidenalg`` (version >= 0.12). You can use the file ``build_libleidenalg.sh`` (on Unix-like systems) or ``build_libleidenalg.bat`` (on Windows) in the ``scripts/`` directory to do this. For more details, see https://github.com/vtraag/libleidenalg. 3. Build the Python interface using ``python setup.py build`` and ``python setup.py install``, or use ``pip install .`` You can check if all went well by running a variety of tests using ``python -m unittest``. Troubleshooting --------------- In case of any problems, best to start over with a clean environment. Make sure you remove the ``igraph`` and ``leidenalg`` package completely. Then, do a complete reinstall starting from ``pip install leidenalg``. In case you installed from source, and built the C libraries of ``igraph`` and ``libleidenalg`` yourself, remove them completely and rebuild and reinstall them. Usage ----- This is the Python interface for the C++ package ``libleidenalg``. There are no plans at the moment for developing an R interface to the package. However, there have been various efforts to port the package to R. These typically do not offer all available functionality or have some other limitations, but nonetheless may be very useful. The available ports are: - https://github.com/cole-trapnell-lab/leidenbase - https://github.com/TomKellyGenetics/leiden - https://github.com/kharchenkolab/leidenAlg Please refer to the documentation for more details on function calls and parameters. This implementation is made for flexibility, but ``igraph`` nowadays also includes an implementation of the Leiden algorithm internally. That implementation is less flexible: the implementation only works on undirected graphs, and only CPM and modularity are supported. It is likely to be substantially faster though. Just to get you started, below the essential parts. To start, make sure to import the packages: >>> import leidenalg >>> import igraph as ig We'll create a random graph for testing purposes: >>> G = ig.Graph.Erdos_Renyi(100, 0.1); For simply finding a partition use: >>> part = leidenalg.find_partition(G, leidenalg.ModularityVertexPartition); Contribute ---------- Source code: https://github.com/vtraag/leidenalg Issue tracking: https://github.com/vtraag/leidenalg/issues See the documentation on `Implementation` for more details on how to contribute new methods. References ---------- Please cite the references appropriately in case they are used. .. [1] Traag, V.A., Waltman. L., Van Eck, N.-J. (2018). From Louvain to Leiden: guaranteeing well-connected communities. Scientific reports, 9(1), 5233. `10.1038/s41598-019-41695-z `_ .. [2] Blondel, V. D., Guillaume, J.-L., Lambiotte, R., & Lefebvre, E. (2008). Fast unfolding of communities in large networks. Journal of Statistical Mechanics: Theory and Experiment, 10008(10), 6. `10.1088/1742-5468/2008/10/P10008 `_ .. [3] Newman, M. E. J., & Girvan, M. (2004). Finding and evaluating community structure in networks. Physical Review E, 69(2), 026113. `10.1103/PhysRevE.69.026113 `_ .. [4] Reichardt, J., & Bornholdt, S. (2006). Statistical mechanics of community detection. Physical Review E, 74(1), 016110. `10.1103/PhysRevE.74.016110 `_ .. [5] Traag, V. A., Van Dooren, P., & Nesterov, Y. (2011). Narrow scope for resolution-limit-free community detection. Physical Review E, 84(1), 016114. `10.1103/PhysRevE.84.016114 `_ .. [6] Traag, V. A., Krings, G., & Van Dooren, P. (2013). Significant scales in community structure. Scientific Reports, 3, 2930. `10.1038/srep02930 `_ .. [7] Traag, V. A., Aldecoa, R., & Delvenne, J.-C. (2015). Detecting communities using asymptotical surprise. Physical Review E, 92(2), 022816. `10.1103/PhysRevE.92.022816 `_ .. [8] Traag, V. A., & Bruggeman, J. (2009). Community detection in networks with positive and negative links. Physical Review E, 80(3), 036115. `10.1103/PhysRevE.80.036115 `_ .. [9] Mucha, P. J., Richardson, T., Macon, K., Porter, M. A., & Onnela, J.-P. (2010). Community structure in time-dependent, multiscale, and multiplex networks. Science, 328(5980), 876–8. `10.1126/science.1184819 `_ .. [10] Zanini, F., Berghuis, B. A., Jones, R. C., Robilant, B. N. di, Nong, R. Y., Norton, J., Clarke, Michael F., Quake, S. R. (2019). northstar: leveraging cell atlases to identify healthy and neoplastic cells in transcriptomes from human tumors. BioRxiv, 820928. `10.1101/820928 `_ Licence ------- Copyright (C) 2020 V.A. Traag This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/. leidenalg-0.11.0/MANIFEST.in0000664000175000017510000000033415101156755014612 0ustar nileshnileshinclude LICENSE include CHANGELOG include README.rst include pyproject.toml include include/ include MANIFEST.in include tests/*.py exclude .git* exclude *.yml exclude release* prune .github graft vendor/source/igraph leidenalg-0.11.0/MANIFEST0000664000175000017510000000142715101156755014211 0ustar nileshnilesh# file GENERATED by distutils, do NOT edit LICENSE README.md setup.py include/CPMVertexPartition.h include/GraphHelper.h include/LinearResolutionParameterVertexPartition.h include/ModularityVertexPartition.h include/MutableVertexPartition.h include/Optimiser.h include/RBConfigurationVertexPartition.h include/RBERVertexPartition.h include/SignificanceVertexPartition.h include/SurpriseVertexPartition.h include/pynterface.h src/CPMVertexPartition.cpp src/GraphHelper.cpp src/LinearResolutionParameterVertexPartition.cpp src/ModularityVertexPartition.cpp src/MutableVertexPartition.cpp src/Optimiser.cpp src/RBConfigurationVertexPartition.cpp src/RBERVertexPartition.cpp src/SignificanceVertexPartition.cpp src/SurpriseVertexPartition.cpp src/__init__.py src/functions.py src/pynterface.cpp leidenalg-0.11.0/LICENSE0000664000175000017510000010450515101156755014066 0ustar nileshnilesh GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . leidenalg-0.11.0/CHANGELOG0000664000175000017510000000550115101156755014267 0ustar nileshnilesh0.11.0 - Update to libleidenalg 0.12.0 - Update to igraph 1.0 0.10.2 - Also allow python-igraph >= 0.11 - Upgrade C core to 0.10.8 for binary wheels 0.10.1 - Switched to pyproject.toml based build - Making use of Limited API - Make available aarch64 wheels 0.10.0 - Changed default refinement to consider a random neighbouring community. - Changed C++ core into external library at https://github.com/vtraag/libleidenalg. 0.9.1 - Allow node sizes to be float (PR #115) - Added correct_self_loops argument to CPMVertexPartition 0.9.0 - Update C core to 0.10.1 0.8.10 - Fixed installation from source package (issue #101) 0.8.9 - Fixed bug with renaming of python-igraph to igraph (issue #93) - Removed irrelevant node_sizes argument for RBConfigurationVertexPartition and ModularityVertexPartition - Improved documentation 0.8.8 - Corrected relabeling bug (PR #82) - Improved error handling, avoiding some crashses (issue #81) 0.8.7 - Improved numerical stability 0.8.6 - Removed accidentally left DEBUG statement 0.8.5 - Corrected iterating over nodes (PR #70). - Fixed segfault with move_nodes_constrained (issue #68) - Fixed problem with initial_membership (issue #66) 0.8.4 - Update C core to 0.9.1 - Fixed caching problem (issue #62) - Fixed missing node_sizes for modularity (issue #60) 0.8.3 - Fixed missing parameter in find_partition_multiplex by @TomKellyGenetics (PR #50) 0.8.2 - New option to constrain community size by @orenbenkiki (PR #46) - Great performance improvement by @ragibson (PR #40) - Minor improvements and clarifications 0.8.1 - Fixed performance problem (issue #35) - Improved documentation 0.8.0 - New option to keep some nodes "fixed" by @iosonofabio (PR #8, #9) - Corrected bipartite clustering - Corrected some documentation - Several minor bugfixes 0.6.1 - Minor corrections to documentation - Added doctest to examples in documentation - Removed trailing semicolons throughout code - Corrected some errors in CPMVertexPartition.Bipartite 0.6.0 - Major API changes, now exposing actual classes and optimisation routine. - Improved algorithm, now runs faster and finds better solutions. - Improved error handling, doing more type checking. - Improved documentation throughout, now done using Sphinx and available from readthedocs.org. - Now includes testing module, available through python setup.py test. 0.5.3 - Fixed bug concerning weights (were rounded to integers). - Improved documentation. - Included an HOWTO on extending the current package. - Fixed some minor bugs. 0.5.2 - Ensured that random neighbour selection works in O(1) rather than O(k), with k the average number of neighbours. - Optimized the calculation of weight from/to community. - Included some missing references. 0.5.1 Corrected some mistakes which prevented it from being properly used on PyPi. No serious changes were made. 0.5 Initial release leidenalg-0.11.0/.readthedocs.yml0000664000175000017510000000116615101156755016146 0ustar nileshnilesh# .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 conda: environment: build-doc.yml # Optionally build your docs in additional formats such as PDF and ePub formats: all # Optionally set the version of Python and requirements required to build your docs build: os: ubuntu-22.04 tools: python: "mambaforge-4.10" python: install: - method: pip path: . submodules: include: all recursive: true # Build documentation in the doc/source directory with Sphinx sphinx: configuration: doc/source/conf.pyleidenalg-0.11.0/.gitmodules0000664000175000017510000000000015101156755015217 0ustar nileshnileshleidenalg-0.11.0/.gitignore0000664000175000017510000000074115101156755015046 0ustar nileshnileshMANIFEST build/ dist/ lib/ nbproject/ leidenalg.egg-info/ wlib/ bin/ obj/ Makefile *.log **/*.o **/*.so **/*.pyc files.txt files_py3.txt callgrind* main.cpp leiden-dists.zip leidenalg.cbp leidenalg.depend leidenalg.layout leiden.log igraphcore/ **/*.swp valgrind-python.supp test/ test_install.sh massif* .vscode/ *.code-workspace PACKAGE.txt vendor/source/igraph vendor/source/install vendor/install src/leidenalg/version.py pip-wheel-metadata .eggs/ doc/source/_build build-deps leidenalg-0.11.0/.gitattributes0000664000175000017510000000005115101156755015743 0ustar nileshnilesh* text=auto src/_version.py export-subst